diff --git a/ExternalRevisions/SPIRV-Cross_repo_revision b/ExternalRevisions/SPIRV-Cross_repo_revision index 7e6ae4958..3f175a1de 100644 --- a/ExternalRevisions/SPIRV-Cross_repo_revision +++ b/ExternalRevisions/SPIRV-Cross_repo_revision @@ -1 +1 @@ -aa217aeb6c9f0ace7a0ab233b28807edf45eb165 +81739edf516c2bef043ac61f510b4bd78e126a22 diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdDebug.h b/MoltenVK/MoltenVK/Commands/MVKCmdDebug.h index 85f6d38b1..80be5e4be 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdDebug.h +++ b/MoltenVK/MoltenVK/Commands/MVKCmdDebug.h @@ -46,6 +46,7 @@ class MVKCmdDebugMarkerBegin : public MVKCmdDebugMarker { public: void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder*) override { return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -62,6 +63,7 @@ class MVKCmdDebugMarkerEnd : public MVKCommand { VkResult setContent(MVKCommandBuffer* cmdBuff); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder*) override { return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -76,6 +78,7 @@ class MVKCmdDebugMarkerInsert : public MVKCmdDebugMarker { public: void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder*) override { return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdDebug.mm b/MoltenVK/MoltenVK/Commands/MVKCmdDebug.mm index 9936a2e04..b6d05e74a 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdDebug.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCmdDebug.mm @@ -46,7 +46,7 @@ if (mtlCmdEnc) { [mtlCmdEnc pushDebugGroup: _markerName]; } else { - [cmdEncoder->_mtlCmdBuffer pushDebugGroup: _markerName]; + cmdEncoder->pushCommandBufferDebugGroup(_markerName); } } @@ -63,7 +63,7 @@ if (mtlCmdEnc) { [mtlCmdEnc popDebugGroup]; } else { - [cmdEncoder->_mtlCmdBuffer popDebugGroup]; + cmdEncoder->popCommandBufferDebugGroup(); } } diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.h b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.h index 9afc3ed8e..73a02e7fb 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.h +++ b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.h @@ -21,9 +21,31 @@ #include "MVKCommand.h" #include "MVKMTLResourceBindings.h" #include "MVKSmallVector.h" +#include "MVKPerVertexReplay.h" #import +class MVKGraphicsPipeline; + +// Preserve the conservative power-of-two scratch ceiling used by recording validation. +static inline bool mvkCanEncodePerVertexDraw(uint32_t vertexCount, uint32_t instanceCount, uint32_t stride, VkPrimitiveTopology topology, uint64_t maxMTLBufferSize) { + if (!mvkPerVertexReplayVertexCount(topology)) { return false; } + if (!vertexCount || !instanceCount) { return true; } + if (!maxMTLBufferSize) { return false; } + uint64_t maxBufferSize = 1; + while (maxBufferSize <= maxMTLBufferSize / 2) { maxBufferSize *= 2; } + uint64_t recordCount = uint64_t(vertexCount) * instanceCount; + if (!stride || recordCount > UINT32_MAX || recordCount > maxBufferSize / stride) { return false; } + uint64_t primitiveCount = uint64_t(mvkPerVertexPrimitiveCount(vertexCount, topology)) * instanceCount; + uint64_t occurrenceCount = primitiveCount * mvkPerVertexReplayVertexCount(topology); + return primitiveCount <= UINT32_MAX / 3 && primitiveCount <= maxBufferSize / (3 * sizeof(uint32_t)) && occurrenceCount <= UINT32_MAX / 2 && occurrenceCount <= maxBufferSize / (2 * sizeof(uint32_t)); +} + +// PerVertex TES plan: status, replay arguments and constants, per-patch counts and offsets, then, 256-byte +// aligned for the fragment binding, the patch index of each generated triangle (at most 13 per patch). +static inline uint64_t mvkPerVertexTessPrimitiveIdOffset(uint64_t patches) { return mvkAlignByteCount(512 + patches * 2 * sizeof(uint32_t), 256); } +static inline uint64_t mvkPerVertexTessPlanSize(uint64_t patches) { return mvkPerVertexTessPrimitiveIdOffset(patches) + patches * 13 * sizeof(uint32_t); } + #pragma mark - #pragma mark MVKCmdBindVertexBuffers @@ -45,6 +67,7 @@ class MVKCmdBindVertexBuffers : public MVKCommand { const VkDeviceSize* pStrides); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -77,6 +100,7 @@ class MVKCmdBindIndexBuffer : public MVKCommand { VkIndexType indexType); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -99,6 +123,8 @@ class MVKCmdDraw : public MVKCommand { uint32_t drawIndex = 0); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override; + void encodePerVertexInput(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline); void encodeIndexedIndirect(MVKCommandEncoder* cmdEncoder); protected: @@ -127,6 +153,8 @@ class MVKCmdDrawIndexed : public MVKCommand { uint32_t drawIndex = 0); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override; + void encodePerVertexInput(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline); protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -218,3 +246,49 @@ class MVKCmdDrawIndexedIndirect : public MVKCommand { id _mtlCountBuffer; VkDeviceSize _mtlCountBufferOffset; }; + + +#pragma mark - +#pragma mark MVKCmdDrawMeshTasks + +/** Vulkan command to draw mesh workgroups (vkCmdDrawMeshTasksEXT). */ +class MVKCmdDrawMeshTasks : public MVKCommand { + +public: + VkResult setContent(MVKCommandBuffer* cmdBuff, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ); + + void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override; + +protected: + MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; + bool encodeMesh(MVKCommandEncoder* cmdEncoder, bool prepass); + + uint32_t _groupCountX; + uint32_t _groupCountY; + uint32_t _groupCountZ; +}; + + +#pragma mark - +#pragma mark MVKCmdDrawMeshTasksIndirect + +/** + * Vulkan command to draw one indirect grid of mesh workgroups (vkCmdDrawMeshTasksIndirectEXT, drawCount 1), with + * MVK_CONFIG_MESH_INDIRECT_GPU=1. The grid is read by Metal from the indirect buffer, with no CPU readback. + */ +class MVKCmdDrawMeshTasksIndirect : public MVKCommand { + +public: + VkResult setContent(MVKCommandBuffer* cmdBuff, VkBuffer buffer, VkDeviceSize offset); + + void encode(MVKCommandEncoder* cmdEncoder) override; + // A native mesh draw has nothing to capture: nothing to do in the PerVertex prepass, which continues. + bool encodePerVertexPrepass(MVKCommandEncoder*) override { return true; } + +protected: + MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; + + id _mtlIndirectBuffer; + VkDeviceSize _mtlIndirectBufferOffset; +}; diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm index d04529738..d7fee6361 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm @@ -17,10 +17,13 @@ */ #include "MVKCmdDraw.h" +#include "MVKPerVertexReplay.h" #include "MVKCommandBuffer.h" #include "MVKCommandPool.h" #include "MVKBuffer.h" #include "MVKPipeline.h" +#include "MVKFramebuffer.h" +#include "MVKImage.h" #include "MVKFoundation.h" #include "mvk_datatypes.hpp" @@ -77,6 +80,7 @@ VkDeviceSize size, VkIndexType indexType) { _binding.vkIndexType = indexType; + cmdBuff->recordIndexType(indexType); _binding.mtlIndexType = mvkMTLIndexTypeFromVkIndexType(indexType); MVKBuffer* mvkBuffer = (MVKBuffer*)buffer; @@ -107,6 +111,8 @@ } +static VkResult recordTessPatchCount(MVKCommandBuffer* cmdBuff, uint32_t vertexCount, uint32_t instanceCount); + #pragma mark - #pragma mark MVKCmdDraw @@ -127,7 +133,8 @@ return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDraw(): The current device does not support drawing with a non-zero base instance."); } - return VK_SUCCESS; + VkResult result = recordTessPatchCount(cmdBuff, _vertexCount, _instanceCount); + return result == VK_SUCCESS ? cmdBuff->recordPerVertexDraw(_vertexCount, _instanceCount) : result; } // Populates and encodes a MVKCmdDrawIndexedIndirect command, after populating indexed indirect buffers. @@ -161,14 +168,409 @@ ibb.size = vtxIdxBuff->_length; MVKCmdDrawIndexedIndirect diiCmd; - diiCmd.setContent(cmdEncoder->_cmdBuffer, - indirectIdxBuff->_mtlBuffer, - indirectIdxBuff->_offset, - 1, - indirectIdxBuffStride); + VkResult result = diiCmd.setContent(cmdEncoder->_cmdBuffer, indirectIdxBuff->_mtlBuffer, indirectIdxBuff->_offset, 1, indirectIdxBuffStride); + if (result != VK_SUCCESS) { cmdEncoder->_cmdBuffer->setConfigurationResult(result); return; } diiCmd.encode(cmdEncoder, ibb); } +static void encodePerVertexInput(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance, uint32_t drawIndex, const MVKIndexMTLBufferBinding* indexBinding = nullptr, uint32_t firstIndex = 0, int32_t baseVertex = 0) { + // A draw refused while encoding also loses the device: vkQueueSubmit() may have returned, and the submission must + // never complete without it. + auto reject = [&](VkResult result, const char* message) { + cmdEncoder->_cmdBuffer->setConfigurationResult(cmdEncoder->_cmdBuffer->reportError(result, "Portable PerVertexKHR %s", message)); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + }; + auto* subpass = cmdEncoder->getSubpass(); + uint32_t viewCount = subpass->isMultiview() ? subpass->getViewCountInMetalPass(cmdEncoder->getMultiviewPassIndex()) : 1; + uint64_t physicalInstances = uint64_t(instanceCount) * viewCount; + if (!viewCount || physicalInstances > UINT32_MAX) { reject(VK_ERROR_OUT_OF_DEVICE_MEMORY, "view-expanded instance count exceeds uint32."); return; } + instanceCount = uint32_t(physicalInstances); + // Vulkan ignores primitiveRestartEnable for nonindexed draws. + bool restart = mvkPerVertexRequiresRestartAssembly(indexBinding != nullptr, cmdEncoder->getVkGraphics().isPrimitiveRestartEnabled()); + if (restart && !mvkCanAssemblePerVertexRestart(vertexCount, instanceCount, cmdEncoder->getMetalFeatures().indirectDrawing)) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "restart requires indirect drawing and uint32 dense capture record IDs."); return; } + // The PerVertex prepass of this subpass already encoded the capture of this scratch slot. + bool prepassed = cmdEncoder->_pEncodingContext->nextPerVertexScratch < cmdEncoder->_perVertexPrepassEnd; + auto* scratch = cmdEncoder->nextPerVertexScratch(); + if (!scratch) { return; } + id directCaptureState = indexBinding ? nil : pipeline->getPerVertexCapturePipelineState(viewCount); + if (!indexBinding && !directCaptureState) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "direct capture view-count pipeline is unavailable."); return; } + id captureState = nil; + id widenState = nil; + id restartState = nil; + id indexBuffer = indexBinding ? indexBinding->mtlBuffer : nil; + VkDeviceSize indexOffset = 0; + if (indexBinding) { + uint32_t indexSize = mvkPerVertexIndexSize(indexBinding->vkIndexType); + if (!indexSize) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "indexed capture requires uint8, uint16 or uint32 indices."); return; } + if (!indexBuffer || !mvkPerVertexIndexRange(vertexCount, firstIndex, indexBinding->vkIndexType, indexBinding->size, indexBinding->offset, indexBuffer.length)) { reject(VK_ERROR_UNKNOWN, "indexed capture exceeds the bound index buffer range."); return; } + indexOffset = indexBinding->offset + uint64_t(firstIndex) * indexSize; + captureState = pipeline->getPerVertexIndexedCapturePipelineState(restart || indexSize == 4, viewCount); + if (restart) { + if (!scratch->buffers[5] || scratch->buffers[5].length < mvkPerVertexRestartIndexScratchSize(vertexCount) || !scratch->buffers[6] || scratch->buffers[6].length < 68 * sizeof(uint32_t)) { reject(VK_ERROR_OUT_OF_DEVICE_MEMORY, "restart assembly scratch is unavailable."); return; } + restartState = scratch->indexPipeline; + if (!restartState || !restartState.threadExecutionWidth || !restartState.maxTotalThreadsPerThreadgroup) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "restart assembly pipeline is unavailable."); return; } + } else if (indexSize == 1) { + if (!scratch->buffers[5] || scratch->buffers[5].length < uint64_t(vertexCount) * sizeof(uint16_t)) { reject(VK_ERROR_OUT_OF_DEVICE_MEMORY, "widened index scratch is unavailable."); return; } + // Restart is disabled: 255 is a vertex index, not a sentinel. Read on GPU. + widenState = scratch->widenUint8Pipeline ?: scratch->indexPipeline; + if (!widenState || !widenState.maxTotalThreadsPerThreadgroup || !widenState.threadExecutionWidth) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "raw uint8 index conversion is unavailable."); return; } + } + if (!captureState || !captureState.maxTotalThreadsPerThreadgroup || !captureState.threadExecutionWidth) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "indexed compute capture pipeline is unavailable."); return; } + } + // Both captures end the render encoder: compute for indexed draws, a render pass boundary for nonindexed ones. + if (const char* error = cmdEncoder->getIndexedPerVertexAttachmentError()) { reject(VK_ERROR_FEATURE_NOT_PRESENT, error); return; } + const auto& layout = pipeline->getPerVertexCapturedLayout(); + auto topology = cmdEncoder->getVkGraphics().getPrimitiveTopology(); + bool provokingLast = cmdEncoder->getVkGraphics().getProvokingVertexMode(); + uint64_t primitivesPerInstance = mvkPerVertexPrimitiveCount(vertexCount, topology); + uint32_t replayVertices = mvkPerVertexReplayVertexCount(topology); + uint64_t primitiveCount = primitivesPerInstance * instanceCount; + uint64_t replayCount = primitivesPerInstance * replayVertices; + if (!mvkCanEncodePerVertexDraw(vertexCount, instanceCount, layout.stride, topology, cmdEncoder->getMetalFeatures().maxMTLBufferSize)) { + reject(VK_ERROR_OUT_OF_DEVICE_MEMORY, "draw exceeds its capture or replay buffer limits."); + return; + } + id captured = scratch->buffers[0]; + id captureParams = scratch->buffers[1]; + id occurrences = scratch->buffers[2]; + id primitiveIndices = scratch->buffers[3]; + *(uint32_t*)captureParams.contents = vertexCount; + cmdEncoder->_isIndexedDraw = indexBinding != nullptr; + if (indexBinding && !prepassed) { + // Capture uses vertexCount records per instance; restart compacts occurrences within that stride. + // Indices are read on the GPU, including writes earlier in this command buffer. + // The prepass shares one compute encoder between captures: track its state as new, as for the encoder of each capture. + if (cmdEncoder->_isPerVertexPrepassing) { cmdEncoder->getState().beginComputeEncoding(); } + cmdEncoder->finalizeDrawState(kMVKGraphicsStageVertex); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + id captureEncoder = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl); + auto& computeState = cmdEncoder->getMtlCompute(); + if (restartState) { + computeState.bindPipeline(captureEncoder, restartState); + computeState.bindBuffer(captureEncoder, indexBuffer, indexOffset, 0); + computeState.bindBuffer(captureEncoder, scratch->buffers[5], 0, 1); + computeState.bindBuffer(captureEncoder, occurrences ?: scratch->buffers[5], 0, 2); + computeState.bindBuffer(captureEncoder, primitiveIndices ?: scratch->buffers[5], 0, 3); + computeState.bindBuffer(captureEncoder, scratch->buffers[4] ?: scratch->buffers[5], 0, 4); + computeState.bindBuffer(captureEncoder, scratch->buffers[6], 0, 5); + uint32_t params[] = {vertexCount, instanceCount, mvkPerVertexIndexSize(indexBinding->vkIndexType), uint32_t(topology), uint32_t(provokingLast), firstInstance, uint32_t(pipeline->usesPortableBarycentrics()), 0, 0, 0}; + NSUInteger width = std::min(restartState.threadExecutionWidth, restartState.maxTotalThreadsPerThreadgroup); + mvkDispatchPerVertexRestart(vertexCount, instanceCount, topology, [&](uint32_t phase, uint32_t step, uint32_t source, uint64_t count) { + params[7] = phase; params[8] = step; params[9] = source; + computeState.bindBytes(captureEncoder, params, sizeof(params), 6); + [captureEncoder dispatchThreadgroups: MTLSizeMake((count + width - 1) / width, 1, 1) threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; + [captureEncoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + }); + indexBuffer = scratch->buffers[5]; + indexOffset = 0; + // Restore application resources after the assembly kernel occupied slots 0..6. + computeState._exists.descriptorSetData.reset(); + cmdEncoder->finalizeDrawState(kMVKGraphicsStageVertex); + computeState.bindPipeline(captureEncoder, captureState); + } + if (widenState) { + computeState.bindPipeline(captureEncoder, widenState); + computeState.bindBuffer(captureEncoder, indexBuffer, indexOffset, 0); + computeState.bindBuffer(captureEncoder, scratch->buffers[5], 0, 1); + NSUInteger width = std::min(widenState.threadExecutionWidth, widenState.maxTotalThreadsPerThreadgroup); + // The existing raw kernel has no bounds check: dispatch exactly vertexCount threads. + NSUInteger groups = vertexCount / width, remainder = vertexCount % width; + if (groups) { [captureEncoder dispatchThreadgroups: MTLSizeMake(groups, 1, 1) threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; } + if (remainder) { + computeState.bindBuffer(captureEncoder, indexBuffer, indexOffset + groups * width, 0); + computeState.bindBuffer(captureEncoder, scratch->buffers[5], groups * width * sizeof(uint16_t), 1); + [captureEncoder dispatchThreadgroups: MTLSizeMake(1, 1, 1) threadsPerThreadgroup: MTLSizeMake(remainder, 1, 1)]; + } + [captureEncoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + indexBuffer = scratch->buffers[5]; + indexOffset = 0; + // Raw conversion overwrote slots 0/1 and the pipeline. Restore application bindings. + computeState._exists.descriptorSetData.reset(); + cmdEncoder->finalizeDrawState(kMVKGraphicsStageVertex); + computeState.bindPipeline(captureEncoder, captureState); + } + computeState.bindBuffer(captureEncoder, captured, 0, pipeline->getPerVertexCaptureBufferIndex()); + computeState.bindBuffer(captureEncoder, indexBuffer, indexOffset, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::Index]); + if (pipeline->needsDrawIdBuffer()) { computeState.bindStructBytes(captureEncoder, &drawIndex, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::DrawId]); } + // Preserve the signed base vertex as the uint32 bit pattern used by SPIR-V. + [captureEncoder setStageInRegion: MTLRegionMake2D(uint32_t(baseVertex), firstInstance, vertexCount, instanceCount)]; + cmdEncoder->getState().offsetZeroDivisorVertexBuffers(*cmdEncoder, kMVKGraphicsStageVertex, pipeline, firstInstance); + NSUInteger width = std::min(captureState.threadExecutionWidth, captureState.maxTotalThreadsPerThreadgroup); + if (restart) { + // Only compact occurrences execute; grid_size (capture stride) remains the original stage-in region. + [captureEncoder dispatchThreadgroupsWithIndirectBuffer: scratch->buffers[6] indirectBufferOffset: 0 threadsPerThreadgroup: MTLSizeMake(1, 1, 1)]; + } else if (cmdEncoder->getMetalFeatures().nonUniformThreadgroups) { + [captureEncoder dispatchThreads: MTLSizeMake(vertexCount, instanceCount, 1) threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; + } else { + [captureEncoder dispatchThreadgroups: MTLSizeMake(vertexCount, instanceCount, 1) threadsPerThreadgroup: MTLSizeMake(1, 1, 1)]; + } + if (cmdEncoder->_isPerVertexPrepassing) { return; } + cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); + } + cmdEncoder->_isIndexedDraw = false; + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + id encoder = cmdEncoder->_mtlRenderEncoder; + auto& metalState = cmdEncoder->getMtlGraphics(); + if (!indexBinding) { + [encoder setRenderPipelineState: directCaptureState]; + MTLPrimitiveType captureType = pipeline->capturesPerVertexTriangleLists() ? MTLPrimitiveTypeTriangle : MTLPrimitiveTypePoint; + metalState.bindVertexBuffer(encoder, captured, 0, pipeline->getPerVertexCaptureBufferIndex()); + metalState.bindVertexBuffer(encoder, captureParams, 0, pipeline->getPerVertexCaptureParamsBufferIndex()); + cmdEncoder->getState().offsetZeroDivisorVertexBuffers(*cmdEncoder, kMVKGraphicsStageRasterization, pipeline, firstInstance); + if (pipeline->needsDrawIdBuffer()) { metalState.bindVertexBytes(encoder, &drawIndex, sizeof(drawIndex), pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::DrawId]); } + if (cmdEncoder->getMetalFeatures().baseVertexInstanceDrawing) { + [encoder drawPrimitives: captureType vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount baseInstance: firstInstance]; + } else { + [encoder drawPrimitives: captureType vertexStart: firstVertex vertexCount: vertexCount instanceCount: instanceCount]; + } + } + if (!primitiveCount) { + [encoder setRenderPipelineState: pipeline->getMainPipelineState()]; + metalState._exists.vertex().descriptorSetData.reset(); + return; + } + if (!indexBinding) { + // The replay reads what the capture wrote: separate them by a render pass boundary. Like MVKCmdPipelineBarrier, + // this avoids memory barriers inside a render pass on Apple GPUs. Metal shader validation on iOS and iPadOS 27 + // silently ignores them, and the replay then draws nothing (Tests/PerVertexBarrierRepro, FB24976351). + cmdEncoder->encodeStoreActions(true); + cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + encoder = cmdEncoder->_mtlRenderEncoder; + } + id corners = scratch->buffers[4]; + MVKPerVertexReplayTables shared; + if (mvkPerVertexReplayIsSizeIndependent(restart, topology, vertexCount, instanceCount)) { shared = cmdEncoder->getCommandEncodingPool()->getPerVertexReplayTables(topology, provokingLast, vertexCount * instanceCount); } + if (shared.pairs) { + // This draw's tables are a prefix of the shared ones: no per-draw CPU population. + occurrences = shared.pairs; + primitiveIndices = shared.indices; + if (corners) { corners = shared.corners; } + } else if (!restart) { + uint64_t occurrenceCount = primitiveCount * replayVertices; + if (occurrences.length < occurrenceCount * 2 * sizeof(uint32_t) || primitiveIndices.length < primitiveCount * 3 * sizeof(uint32_t) || (corners && corners.length < occurrenceCount * sizeof(uint32_t))) { reject(VK_ERROR_OUT_OF_DEVICE_MEMORY, "replay table scratch is too small."); return; } + mvkPopulatePerVertexReplay(vertexCount, instanceCount, topology, provokingLast, (uint32_t*)occurrences.contents, (uint32_t*)primitiveIndices.contents, (uint32_t*)corners.contents); + } + const auto& replay = pipeline->getPerVertexReplayBinding(); + const auto& fragment = pipeline->getPerVertexInputBinding(); + uint32_t replayDraw[] = {0, 0, firstInstance, uint32_t(replayCount)}; + [encoder setRenderPipelineState: pipeline->getMainPipelineState()]; + metalState.bindVertexBuffer(encoder, captured, 0, replay.vertex_buffer_index); + metalState.bindVertexBuffer(encoder, occurrences, 0, replay.occurrence_buffer_index); + if (corners) { metalState.bindVertexBuffer(encoder, corners, 0, pipeline->getPerVertexReplayBarycentricBinding().corner_buffer_index); } + if (restart) { metalState.bindVertexBuffer(encoder, scratch->buffers[6], 256, replay.draw_parameters_buffer_index); } + else { metalState.bindVertexBytes(encoder, replayDraw, sizeof(replayDraw), replay.draw_parameters_buffer_index); } + if (fragment.vertex_buffer_index != ~0u) { + metalState.bindFragmentBuffer(encoder, captured, 0, fragment.vertex_buffer_index); + metalState.bindFragmentBuffer(encoder, primitiveIndices, 0, fragment.primitive_index_buffer_index); + } + if (cmdEncoder->getPhysicalDevice()->shouldEmulateReversedDepthViewport()) { + uint32_t viewportMask = cmdEncoder->getVkGraphics()._implicitBufferData[kMVKShaderStageVertex].emulatedReversedDepthViewportMask; + metalState.bindVertexBytes(encoder, &viewportMask, sizeof(viewportMask), pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::EmulatedReversedDepthViewport]); + } + MTLPrimitiveType replayType = replayVertices == 1 ? MTLPrimitiveTypePoint : replayVertices == 2 ? MTLPrimitiveTypeLine : MTLPrimitiveTypeTriangle; + if (restart) { + [encoder drawPrimitives: replayType indirectBuffer: scratch->buffers[6] indirectBufferOffset: 4 * sizeof(uint32_t)]; + } else if (cmdEncoder->getMetalFeatures().baseVertexInstanceDrawing) { + [encoder drawPrimitives: replayType vertexStart: 0 vertexCount: replayCount instanceCount: instanceCount baseInstance: firstInstance]; + } else { + [encoder drawPrimitives: replayType vertexStart: 0 vertexCount: replayCount instanceCount: instanceCount]; + } + metalState._exists.vertex().descriptorSetData.reset(); + metalState._exists.fragment().descriptorSetData.reset(); +} + +// Portable PerVertexKHR tessellation of triangle patches with equal spacing (test-only admission, see +// MVKPipeline.mm). VS and TCS run in compute as for ordinary tessellation, the TCS writing float32 levels. +// A GPU pass classifies each patch from those levels and lays out its proven Metal topology as ordered +// corner records; TES runs in compute over those records and the replay draws them. The CPU never reads +// or infers a level. The encoder waits for the classification: levels without a proven topology lose the +// device before any later command or signal of the submission is encoded. VS/TCS effects of that draw have +// already executed by then. +static void encodePerVertexTessEval(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline, uint32_t vertexCount, uint32_t firstVertex, uint32_t firstInstance, uint32_t drawIndex) { + auto lose = [&](const char* reason) { + cmdEncoder->reportError(VK_ERROR_DEVICE_LOST, "PerVertex TES draw was not rendered: %s.", reason); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + }; + // Encoding runs after vkQueueSubmit() returned: a draw that cannot be encoded also loses the device, so that + // the submission never completes without it. + auto reject = [&](VkResult result, const char* message) { + cmdEncoder->_cmdBuffer->setConfigurationResult(cmdEncoder->_cmdBuffer->reportError(result, "PerVertex TES %s", message)); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + }; + if (const char* error = cmdEncoder->getIndexedPerVertexAttachmentError()) { reject(VK_ERROR_FEATURE_NOT_PRESENT, error); return; } + uint32_t patches = vertexCount / 3; + auto* scratch = cmdEncoder->nextPerVertexScratch(); + if (!scratch) { return; } + id topology = scratch->tessTopologyPipeline; + id levels = scratch->buffers[11], plan = scratch->buffers[12], invocations = scratch->buffers[7]; + if (patches && (!topology || !levels || !plan || !invocations)) { reject(VK_ERROR_INITIALIZATION_FAILED, "draw has no topology scratch."); return; } + cmdEncoder->_isIndexedDraw = false; + cmdEncoder->finalizeDrawState(kMVKGraphicsStageVertex); + id compute = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl); + auto& computeState = cmdEncoder->getState().mtlCompute(); + computeState.bindBuffer(compute, scratch->buffers[8], 0, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::Output]); + // The VS reads the DrawID of this draw within its vkCmdDrawMulti*EXT() call, as for ordinary tessellation. + if (pipeline->needsDrawIdBuffer()) { computeState.bindStructBytes(compute, &drawIndex, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::DrawId]); } + // As for ordinary tessellation, the stage-in origin offsets attribute fetches and gl_VertexIndex/gl_InstanceIndex; + // outputs stay indexed from 0. Divisors are refused at pipeline creation, so no zero-divisor binding needs an offset. + [compute setStageInRegion:MTLRegionMake2D(firstVertex, firstInstance, vertexCount, 1)]; + [compute dispatchThreads:MTLSizeMake(vertexCount, 1, 1) threadsPerThreadgroup:MTLSizeMake(pipeline->getTessVertexStageState().threadExecutionWidth, 1, 1)]; + // Vulkan runs the VS for every vertex, including those that complete no patch; nothing is tessellated then. + if (!patches) { cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); return; } + [compute memoryBarrierWithScope:MTLBarrierScopeBuffers]; + cmdEncoder->finalizeDrawState(kMVKGraphicsStageTessControl); + const auto& tcs = pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids; + computeState.bindBuffer(compute, scratch->buffers[8], 0, cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessCtlInputBufferBinding)); + computeState.bindBuffer(compute, scratch->buffers[9], 0, tcs[MVKImplicitBuffer::Output]); + computeState.bindBuffer(compute, scratch->buffers[10], 0, tcs[MVKImplicitBuffer::PatchOutput]); + computeState.bindBuffer(compute, levels, 0, tcs[MVKImplicitBuffer::TessLevel]); + uint32_t tessParams[] = {3, patches}; + computeState.bindBytes(compute, tessParams, sizeof(tessParams), tcs[MVKImplicitBuffer::IndirectParams]); + NSUInteger groupSize = mvkLeastCommonMultiple(3u, uint32_t(pipeline->getTessControlStageState().threadExecutionWidth)); + [compute dispatchThreads:MTLSizeMake(uint64_t(patches) * 3, 1, 1) threadsPerThreadgroup:MTLSizeMake(groupSize, 1, 1)]; + [compute memoryBarrierWithScope:MTLBarrierScopeBuffers]; + // Classify patches from the written levels, prefix their triangle counts, then emit ordered corner records. + float maxLevel = float(cmdEncoder->getDeviceProperties().limits.maxTessellationGenerationLevel); + uint32_t primitiveIds = uint32_t(mvkPerVertexTessPrimitiveIdOffset(patches)); + uint32_t maxLevelBits; + memcpy(&maxLevelBits, &maxLevel, sizeof(maxLevelBits)); + uint32_t params[] = {patches, maxLevelBits, uint32_t(pipeline->perVertexTessReversesCorners()), uint32_t(scratch->buffers[4] != nil), 0, primitiveIds / 4}; + NSUInteger width = std::min(topology.threadExecutionWidth, topology.maxTotalThreadsPerThreadgroup); + computeState.bindPipeline(compute, topology); + computeState.bindBuffer(compute, levels, 0, 0); + computeState.bindBuffer(compute, plan, 0, 1); + computeState.bindBuffer(compute, invocations, 0, 2); + computeState.bindBuffer(compute, scratch->buffers[2], 0, 3); + computeState.bindBuffer(compute, scratch->buffers[3], 0, 4); + computeState.bindBuffer(compute, scratch->buffers[4] ?: plan, 0, 5); + for (uint32_t phase = 0; phase < 3; ++phase) { + params[4] = phase; + computeState.bindBytes(compute, params, sizeof(params), 6); + [compute dispatchThreads:MTLSizeMake(phase == 1 ? 1 : patches, 1, 1) threadsPerThreadgroup:MTLSizeMake(phase == 1 ? 1 : width, 1, 1)]; + [compute memoryBarrierWithScope:MTLBarrierScopeBuffers]; + } + computeState._exists.descriptorSetData.reset(); + if ( !cmdEncoder->awaitEncodedWork() ) { + if ( !cmdEncoder->getDevice()->isLosing() ) { lose("its topology pass did not complete"); } + return; + } + if (scratch->getTessEvalStatus()) { lose("a patch has tessellation levels without a proven topology"); return; } + compute = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl); + // TES reads the record count the generator wrote; surplus threads of the maximum grid return at once. + cmdEncoder->getState().prepareTessEvalDispatch(compute, *cmdEncoder); + const auto& tes = pipeline->getImplicitBuffers(kMVKShaderStageTessEval).ids; + computeState.bindBuffer(compute, scratch->buffers[9], 0, cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalInputBufferBinding)); + computeState.bindBuffer(compute, scratch->buffers[10], 0, cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalPatchInputBufferBinding)); + computeState.bindBuffer(compute, levels, 0, cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalLevelBufferBinding)); + computeState.bindBuffer(compute, invocations, 0, tes[MVKImplicitBuffer::IndirectParams]); + computeState.bindBuffer(compute, scratch->buffers[0], 0, tes[MVKImplicitBuffer::Output]); + [compute dispatchThreads:MTLSizeMake(uint64_t(patches) * 39, 1, 1) threadsPerThreadgroup:MTLSizeMake(pipeline->getPerVertexTessEvalPipelineState().threadExecutionWidth, 1, 1)]; + // Ending compute establishes visibility for the tracked scratch buffers in render. + cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + id render = cmdEncoder->_mtlRenderEncoder; + auto& graphics = cmdEncoder->getMtlGraphics(); + const auto& replay = pipeline->getPerVertexReplayBinding(); + const auto& fragment = pipeline->getPerVertexInputBinding(); + graphics.bindVertexBuffer(render, scratch->buffers[0], 0, replay.vertex_buffer_index); + graphics.bindVertexBuffer(render, scratch->buffers[2], 0, replay.occurrence_buffer_index); + graphics.bindVertexBuffer(render, plan, 256, replay.draw_parameters_buffer_index); + if (pipeline->usesPortableBarycentrics()) { graphics.bindVertexBuffer(render, scratch->buffers[4], 0, pipeline->getPerVertexReplayBarycentricBinding().corner_buffer_index); } + if (fragment.vertex_buffer_index != ~0u) { + graphics.bindFragmentBuffer(render, scratch->buffers[0], 0, fragment.vertex_buffer_index); + graphics.bindFragmentBuffer(render, scratch->buffers[3], 0, fragment.primitive_index_buffer_index); + // Vulkan's fragment PrimitiveId after tessellation is the patch index, not the replayed triangle's. + if (fragment.primitive_id_buffer_index != ~0u) { graphics.bindFragmentBuffer(render, plan, primitiveIds, fragment.primitive_id_buffer_index); } + } + if (cmdEncoder->getPhysicalDevice()->shouldEmulateReversedDepthViewport()) { + uint32_t mask = cmdEncoder->getVkGraphics()._implicitBufferData[kMVKShaderStageVertex].emulatedReversedDepthViewportMask; + graphics.bindVertexBytes(render, &mask, sizeof(mask), pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::EmulatedReversedDepthViewport]); + } + [render drawPrimitives:MTLPrimitiveTypeTriangle indirectBuffer:plan indirectBufferOffset:16]; + graphics._exists.vertex().descriptorSetData.reset(); + graphics._exists.fragment().descriptorSetData.reset(); +} + +// Ordinary tessellation that keeps float32 TCS levels: after the TCS, Vulkan classifies them into the separate half +// factors Metal's tessellator reads (tessLevelsToHalfFactors), while the TES reads the float32 levels. The caller +// binds the TCS parameters (patch count in word 1) at index 3, then dispatches at least one thread per patch. +static MVKMetalComputeCommandEncoderState& bindTessLevelsToHalfFactors(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline, id encoder, + const MVKMTLBufferAllocation* levels, const MVKMTLBufferAllocation* factors) { + [encoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + auto& state = cmdEncoder->getMtlCompute(); + state.bindPipeline(encoder, pipeline->getTessLevelsToHalfFactorsPipelineState()); + state.bindBuffer(encoder, levels->_mtlBuffer, levels->_offset, 0); + state.bindBuffer(encoder, factors->_mtlBuffer, factors->_offset, 1); + float maxLevel = float(cmdEncoder->getDeviceProperties().limits.maxTessellationGenerationLevel); + state.bindStructBytes(encoder, &maxLevel, 2); + state._exists.descriptorSetData.reset(); + return state; +} + +// Float32 TCS levels: outer[4], inner[2] per patch. +static constexpr size_t kMVKFloat32TessLevelSize = 6 * sizeof(float); + +static constexpr bool mvkFitsFloat32TessLevels(uint64_t patchCount, uint64_t maxBufferSize) { + return patchCount <= UINT32_MAX && patchCount <= maxBufferSize / kMVKFloat32TessLevelSize; +} + +static_assert(mvkFitsFloat32TessLevels(178956970, UINT32_MAX)); +static_assert(!mvkFitsFloat32TessLevels(178956971, UINT32_MAX)); + +static constexpr bool mvkFitsTessPatchCount(uint64_t patchCount, bool float32Levels, uint64_t maxBufferSize) { + return patchCount <= UINT32_MAX && (!float32Levels || mvkFitsFloat32TessLevels(patchCount, maxBufferSize)); +} + +// Without prefill, encoding runs during vkQueueSubmit, possibly after it returned: recordTessPatchCount() refuses +// direct and indexed draws while recording, so that this check cannot silently omit an accepted draw. +static bool validateTessPatchCount(MVKCommandEncoder* cmdEncoder, uint64_t patchCount, bool float32Levels) { + if (mvkFitsTessPatchCount(patchCount, float32Levels, cmdEncoder->getMetalFeatures().maxMTLBufferSize)) { return true; } + auto* cmdBuffer = cmdEncoder->_cmdBuffer; + cmdBuffer->setConfigurationResult(cmdBuffer->reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Tessellation level scratch exceeds Metal buffer limits.")); + return false; +} + +// The check of validateTessPatchCount(), while recording a direct or indexed draw of an ordinary tessellation pipeline. +// A failure stops the command from being recorded and fails vkEndCommandBuffer. Unknown dynamic control points are left +// to encoding. +static VkResult recordTessPatchCount(MVKCommandBuffer* cmdBuff, uint32_t vertexCount, uint32_t instanceCount) { + auto* pipeline = cmdBuff->getRecordedTessellationPipeline(); + uint32_t controlPoints = cmdBuff->getRecordedPatchControlPoints(); + if (!pipeline || !controlPoints || !vertexCount || !instanceCount) { return VK_SUCCESS; } + uint64_t patchCount = mvkCeilingDivide(uint64_t(vertexCount), controlPoints) * instanceCount; + if (mvkFitsTessPatchCount(patchCount, pipeline->usesFloat32TessLevels(), cmdBuff->getMetalFeatures().maxMTLBufferSize)) { return VK_SUCCESS; } + return cmdBuff->reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Tessellation level scratch exceeds Metal buffer limits."); +} + +void MVKCmdDraw::encodePerVertexInput(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline) { + ::encodePerVertexInput(cmdEncoder, pipeline, _vertexCount, _instanceCount, _firstVertex, _firstInstance, _drawIndex); +} + +void MVKCmdDrawIndexed::encodePerVertexInput(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline) { + ::encodePerVertexInput(cmdEncoder, pipeline, _indexCount, _instanceCount, 0, _firstInstance, _drawIndex, &cmdEncoder->getVkGraphics()._indexBuffer, _firstIndex, _vertexOffset); +} + +// Draws without PerVertex capture take no scratch slot and are left to the subpass encoding. +bool MVKCmdDraw::encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) { + if (_vertexCount == 0 || _instanceCount == 0) { return true; } + auto* pipeline = cmdEncoder->getGraphicsPipeline(); + // A nonindexed capture draws in the render pass, and a TES capture waits for the GPU: both end the window. + return !pipeline->usesPerVertexInputBuffer() && !pipeline->usesPerVertexTessEval(); +} + +bool MVKCmdDrawIndexed::encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) { + if (_indexCount == 0 || _instanceCount == 0) { return true; } + auto* pipeline = cmdEncoder->getGraphicsPipeline(); + if (pipeline->usesPerVertexInputBuffer()) { encodePerVertexInput(cmdEncoder, pipeline); } + return true; +} + void MVKCmdDraw::encode(MVKCommandEncoder* cmdEncoder) { if (_vertexCount == 0 || _instanceCount == 0) { return; } // Nothing to do. @@ -176,11 +578,22 @@ cmdEncoder->restartMetalRenderPassIfNeeded(); auto* pipeline = cmdEncoder->getGraphicsPipeline(); + if (pipeline->usesPerVertexTessEval()) { + if (_instanceCount != 1) { + cmdEncoder->_cmdBuffer->setConfigurationResult(cmdEncoder->_cmdBuffer->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "PerVertex TES draw requires one instance.")); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + return; + } + encodePerVertexTessEval(cmdEncoder, pipeline, _vertexCount, _firstVertex, _firstInstance, _drawIndex); + return; + } + if (pipeline->usesPerVertexInputBuffer()) { encodePerVertexInput(cmdEncoder, pipeline); return; } auto& mtlFeats = cmdEncoder->getMetalFeatures(); auto& dvcLimits = cmdEncoder->getDeviceProperties().limits; // Metal doesn't support triangle fans, so encode it as triangles via an indexed indirect triangles command instead. - if (pipeline->getVkPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { + if (cmdEncoder->getVkGraphics().getPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { encodeIndexedIndirect(cmdEncoder); return; } @@ -194,6 +607,7 @@ const MVKMTLBufferAllocation* tcOutBuff = nullptr; const MVKMTLBufferAllocation* tcPatchOutBuff = nullptr; const MVKMTLBufferAllocation* tcLevelBuff = nullptr; + const MVKMTLBufferAllocation* tcFloatLevelBuff = nullptr; const MVKMTLBufferAllocation* tempDrawIDBuff = nullptr; struct { uint32_t inControlPointCount = 0; @@ -203,7 +617,9 @@ if (pipeline->isTessellationPipeline()) { tessParams.inControlPointCount = cmdEncoder->getVkGraphics().getPatchControlPoints(); outControlPointCount = pipeline->getOutputControlPointCount(); - tessParams.patchCount = mvkCeilingDivide(_vertexCount, tessParams.inControlPointCount) * _instanceCount; + uint64_t patchCount = mvkCeilingDivide(uint64_t(_vertexCount), tessParams.inControlPointCount) * _instanceCount; + if (!validateTessPatchCount(cmdEncoder, patchCount, pipeline->usesFloat32TessLevels())) { return; } + tessParams.patchCount = uint32_t(patchCount); } if (pipeline->needsDrawIdBuffer()) { tempDrawIDBuff = cmdEncoder->getTempMTLBuffer(sizeof(uint32_t)); @@ -262,8 +678,9 @@ atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::PatchOutput]]; } tcLevelBuff = cmdEncoder->getTempMTLBuffer(tessParams.patchCount * sizeof(MTLQuadTessellationFactorsHalf), true); - [mtlTessCtlEncoder setBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + if (pipeline->usesFloat32TessLevels()) { tcFloatLevelBuff = cmdEncoder->getTempMTLBuffer(tessParams.patchCount * kMVKFloat32TessLevelSize, true); } + [mtlTessCtlEncoder setBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::TessLevel]]; cmdEncoder->setComputeBytes(mtlTessCtlEncoder, &tessParams, @@ -288,6 +705,12 @@ [mtlTessCtlEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(tessParams.patchCount * outControlPointCount, wgSize), 1, 1) threadsPerThreadgroup: MTLSizeMake(wgSize, 1, 1)]; } + if (tcFloatLevelBuff) { + auto& state = bindTessLevelsToHalfFactors(cmdEncoder, pipeline, mtlTessCtlEncoder, tcFloatLevelBuff, tcLevelBuff); + state.bindBytes(mtlTessCtlEncoder, &tessParams, sizeof(tessParams), 3); + [mtlTessCtlEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(tessParams.patchCount, 64u), 1, 1) + threadsPerThreadgroup: MTLSizeMake(64, 1, 1)]; + } // Running this stage prematurely ended the render pass, so we have to start it up again. // TODO: On iOS, maybe we could use a tile shader to avoid this. cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); @@ -305,8 +728,8 @@ offset: tcPatchOutBuff->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalPatchInputBufferBinding)]; } - [cmdEncoder->_mtlRenderEncoder setVertexBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [cmdEncoder->_mtlRenderEncoder setVertexBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalLevelBufferBinding)]; [cmdEncoder->_mtlRenderEncoder setTessellationFactorBuffer: tcLevelBuff->_mtlBuffer offset: tcLevelBuff->_offset @@ -373,7 +796,8 @@ return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawIndexed(): The current device does not support drawing with a non-zero base vertex."); } - return VK_SUCCESS; + VkResult result = recordTessPatchCount(cmdBuff, _indexCount, _instanceCount); + return result == VK_SUCCESS ? cmdBuff->recordPerVertexDraw(_indexCount, _instanceCount, true) : result; } // Populates and encodes a MVKCmdDrawIndexedIndirect command, after populating an indexed indirect buffer. @@ -390,11 +814,8 @@ pIndArg->baseInstance = _firstInstance; MVKCmdDrawIndexedIndirect diiCmd; - diiCmd.setContent(cmdEncoder->_cmdBuffer, - indirectIdxBuff->_mtlBuffer, - indirectIdxBuff->_offset, - 1, - indirectIdxBuffStride); + VkResult result = diiCmd.setContent(cmdEncoder->_cmdBuffer, indirectIdxBuff->_mtlBuffer, indirectIdxBuff->_offset, 1, indirectIdxBuffStride); + if (result != VK_SUCCESS) { cmdEncoder->_cmdBuffer->setConfigurationResult(result); return; } diiCmd.encode(cmdEncoder); } @@ -446,15 +867,15 @@ void MVKCmdDrawIndexed::encode(MVKCommandEncoder* cmdEncoder) { if (_indexCount == 0 || _instanceCount == 0) { return; } // Nothing to do. - cmdEncoder->restartMetalRenderPassIfNeeded(); + if (cmdEncoder->getGraphicsPipeline()->usesPerVertexInputBuffer()) { encodePerVertexInput(cmdEncoder, cmdEncoder->getGraphicsPipeline()); return; } auto* pipeline = cmdEncoder->getGraphicsPipeline(); auto& mtlFeats = cmdEncoder->getMetalFeatures(); auto& dvcLimits = cmdEncoder->getDeviceProperties().limits; // Metal doesn't support triangle fans, so encode it as triangles via an indexed indirect triangles command instead. - if (pipeline->getVkPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { + if (cmdEncoder->getVkGraphics().getPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { encodeIndexedIndirect(cmdEncoder); return; } @@ -478,6 +899,7 @@ const MVKMTLBufferAllocation* tcOutBuff = nullptr; const MVKMTLBufferAllocation* tcPatchOutBuff = nullptr; const MVKMTLBufferAllocation* tcLevelBuff = nullptr; + const MVKMTLBufferAllocation* tcFloatLevelBuff = nullptr; const MVKMTLBufferAllocation* tempDrawIDBuff = nullptr; struct { uint32_t inControlPointCount = 0; @@ -487,7 +909,9 @@ if (pipeline->isTessellationPipeline()) { tessParams.inControlPointCount = cmdEncoder->getVkGraphics().getPatchControlPoints(); outControlPointCount = pipeline->getOutputControlPointCount(); - tessParams.patchCount = mvkCeilingDivide(_indexCount, tessParams.inControlPointCount) * _instanceCount; + uint64_t patchCount = mvkCeilingDivide(uint64_t(_indexCount), tessParams.inControlPointCount) * _instanceCount; + if (!validateTessPatchCount(cmdEncoder, patchCount, pipeline->usesFloat32TessLevels())) { return; } + tessParams.patchCount = uint32_t(patchCount); } if (pipeline->needsDrawIdBuffer()) { tempDrawIDBuff = cmdEncoder->getTempMTLBuffer(sizeof(uint32_t)); @@ -548,8 +972,9 @@ atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::PatchOutput]]; } tcLevelBuff = cmdEncoder->getTempMTLBuffer(tessParams.patchCount * sizeof(MTLQuadTessellationFactorsHalf), true); - [mtlTessCtlEncoder setBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + if (pipeline->usesFloat32TessLevels()) { tcFloatLevelBuff = cmdEncoder->getTempMTLBuffer(tessParams.patchCount * kMVKFloat32TessLevelSize, true); } + [mtlTessCtlEncoder setBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::TessLevel]]; cmdEncoder->setComputeBytes(mtlTessCtlEncoder, &tessParams, @@ -575,6 +1000,12 @@ [mtlTessCtlEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(tessParams.patchCount * outControlPointCount, wgSize), 1, 1) threadsPerThreadgroup: MTLSizeMake(wgSize, 1, 1)]; } + if (tcFloatLevelBuff) { + auto& state = bindTessLevelsToHalfFactors(cmdEncoder, pipeline, mtlTessCtlEncoder, tcFloatLevelBuff, tcLevelBuff); + state.bindBytes(mtlTessCtlEncoder, &tessParams, sizeof(tessParams), 3); + [mtlTessCtlEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(tessParams.patchCount, 64u), 1, 1) + threadsPerThreadgroup: MTLSizeMake(64, 1, 1)]; + } // Running this stage prematurely ended the render pass, so we have to start it up again. // TODO: On iOS, maybe we could use a tile shader to avoid this. cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); @@ -592,8 +1023,8 @@ offset: tcPatchOutBuff->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalPatchInputBufferBinding)]; } - [cmdEncoder->_mtlRenderEncoder setVertexBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [cmdEncoder->_mtlRenderEncoder setVertexBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalLevelBufferBinding)]; [cmdEncoder->_mtlRenderEncoder setTessellationFactorBuffer: tcLevelBuff->_mtlBuffer offset: tcLevelBuff->_offset @@ -646,6 +1077,9 @@ // TODO: Consider breaking up such draws using different base instance values. But this will // require yet more munging of the indirect buffers... static const uint32_t kMVKMaxDrawIndirectVertexCount = 1024 * KIBI; +// Indirect tessellation draws size their scratch for this fixed vertex count: even with one control point per patch, +// its float32 levels fit any Metal buffer limit of at least 256 MiB, so validateTessPatchCount() cannot omit them. +static_assert(mvkFitsTessPatchCount(kMVKMaxDrawIndirectVertexCount, true, 256 * MEBI)); static const MVKMTLBufferAllocation* encodeIndirectCountConversion( MVKCommandEncoder* cmdEncoder, @@ -682,6 +1116,214 @@ return convertedBuffer; } +// Portable PerVertexKHR indirect draws. Arguments and Count may be written earlier in the same submission, +// so only the GPU reads them. Phase 0 freezes every draw's arguments and the Count on the GPU and computes the +// records they need, with the largest count a uint32 record ID can represent as its only ceiling. The encoder then +// waits for that plan, reserves the exact replay scratch from the frozen counts, and encodes the replay on a new +// Metal command buffer. A command that cannot be replayed (unrepresentable counts, indices beyond the bound +// index buffer, or scratch beyond Metal limits or memory) executes no draw and loses the device, so that nothing +// that follows it is reported complete. Apple3-10 GPUs cannot order fragment reads before later vertex writes +// inside a render encoder, so each draw ends the render encoder around its compute planning and the next draw +// reuses the hazard-tracked scratch only after Metal orders the previous replay. +static void encodePerVertexIndirect(MVKCommandEncoder* cmdEncoder, MVKGraphicsPipeline* pipeline, id indirectBuffer, VkDeviceSize indirectOffset, uint32_t stride, uint32_t drawCount, id countBuffer, VkDeviceSize countOffset, const MVKIndexMTLBufferBinding* indexBinding) { + // Encoding runs after vkQueueSubmit() returned: a command that cannot be encoded also loses the device, so that + // the submission never completes without it. + auto reject = [&](VkResult result, const char* message) { + cmdEncoder->_cmdBuffer->setConfigurationResult(cmdEncoder->_cmdBuffer->reportError(result, "Portable PerVertexKHR indirect %s", message)); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + }; + auto lose = [&](const char* reason) { + cmdEncoder->reportError(VK_ERROR_DEVICE_LOST, "Portable PerVertexKHR indirect command was not executed: %s.", reason); + cmdEncoder->getDevice()->markLost(); + cmdEncoder->stopEncoding(); + }; + auto* subpass = cmdEncoder->getSubpass(); + uint32_t viewCount = subpass->isMultiview() ? subpass->getViewCountInMetalPass(cmdEncoder->getMultiviewPassIndex()) : 1; + auto* scratch = cmdEncoder->nextPerVertexScratch(); + if (!scratch) { return; } + id plan = scratch->buffers[12]; + id helper = scratch->indirectPipeline; + if (!plan || !helper || !scratch->indirectCapacity || !viewCount) { reject(VK_ERROR_INITIALIZATION_FAILED, "draw has no planning scratch."); return; } + if (const char* error = cmdEncoder->getIndexedPerVertexAttachmentError()) { reject(VK_ERROR_FEATURE_NOT_PRESENT, error); return; } + uint32_t indexSize = indexBinding ? mvkPerVertexIndexSize(indexBinding->vkIndexType) : 0; + if (indexBinding && (!indexSize || !indexBinding->mtlBuffer)) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "indexed draw requires a bound uint8, uint16 or uint32 index buffer."); return; } + // Vulkan ignores restart for nonindexed draws. Restart assembly runs on the gathered indices. + bool restart = mvkPerVertexRequiresRestartAssembly(indexBinding != nullptr, cmdEncoder->getVkGraphics().isPrimitiveRestartEnabled()); + id restartState = restart ? scratch->indexPipeline : nil; + if (restart && (!restartState || !restartState.threadExecutionWidth || !restartState.maxTotalThreadsPerThreadgroup)) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "restart assembly helper is unavailable."); return; } + id directCaptureState = indexBinding ? nil : pipeline->getPerVertexCapturePipelineState(viewCount); + id captureState = indexBinding ? pipeline->getPerVertexIndexedCapturePipelineState(true, viewCount) : nil; + if (indexBinding ? (!captureState || !captureState.threadExecutionWidth || !captureState.maxTotalThreadsPerThreadgroup) : !directCaptureState) { reject(VK_ERROR_FEATURE_NOT_PRESENT, "capture pipeline is unavailable."); return; } + // Indices the GPU may gather: the Vulkan binding clipped to its Metal allocation. + uint64_t indexCapacity = 0; + if (indexBinding && indexBinding->offset <= indexBinding->mtlBuffer.length) { indexCapacity = std::min(indexBinding->size, indexBinding->mtlBuffer.length - indexBinding->offset) / indexSize; } + auto topology = cmdEncoder->getVkGraphics().getPrimitiveTopology(); + uint32_t replayVertices = mvkPerVertexReplayVertexCount(topology); + NSUInteger width = std::max(std::min(helper.threadExecutionWidth, helper.maxTotalThreadsPerThreadgroup), 1); + if (restartState) { width = std::min(width, std::min(restartState.threadExecutionWidth, restartState.maxTotalThreadsPerThreadgroup)); } + // The plan layout, including where phase 0 freezes the arguments, follows the plan's record ceiling. + uint32_t planScanSteps = restart ? mvkPerVertexIndirectRestartScanSteps(scratch->indirectCapacity) : 0; + if (restart && plan.length < 1024 + (planScanSteps + 3) * 256) { reject(VK_ERROR_INITIALIZATION_FAILED, "restart planning scratch is too small."); return; } + // A single draw freezes its arguments in the plan, several draws in their own buffer (see the perVertexIndirect kernel). + uint32_t snapshotWords = drawCount == 1 ? 48 : 0; + id snapshot = drawCount == 1 ? plan : scratch->buffers[13]; + if (drawCount > 1 && (!snapshot || snapshot.length < uint64_t(drawCount) * (indexBinding ? 5 : 4) * sizeof(uint32_t))) { reject(VK_ERROR_INITIALIZATION_FAILED, "argument snapshot scratch is too small."); return; } + NSUInteger captureWidth = captureState ? std::max(std::min(captureState.threadExecutionWidth, captureState.maxTotalThreadsPerThreadgroup), 1) : 1; + bool corners = pipeline->usesPortableBarycentrics(); + uint32_t params[19] = {drawCount, stride, indexSize, uint32_t(indexCapacity), uint32_t(indexCapacity >> 32), scratch->indirectCapacity, viewCount, uint32_t(topology), + uint32_t(cmdEncoder->getVkGraphics().getProvokingVertexMode()), uint32_t(corners), uint32_t(countBuffer != nil), 0, uint32_t(width), uint32_t(width), uint32_t(captureWidth), 0, uint32_t(restart), planScanSteps, snapshotWords}; + auto& computeState = cmdEncoder->getMtlCompute(); + auto& metalState = cmdEncoder->getMtlGraphics(); + + // Phase 0 freezes every draw of the command, then the GPU plan must complete before the replay can be sized. + cmdEncoder->encodeStoreActions(true); + id planEncoder = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDrawIndirectConvertBuffers); + computeState.bindPipeline(planEncoder, helper); + computeState.bindBuffer(planEncoder, indirectBuffer, indirectOffset, 0); + computeState.bindBuffer(planEncoder, countBuffer ?: plan, countBuffer ? countOffset : 0, 1); + computeState.bindBuffer(planEncoder, plan, 0, 2); + computeState.bindBuffer(planEncoder, indexBinding ? indexBinding->mtlBuffer : plan, indexBinding ? indexBinding->offset : 0, 3); + for (uint32_t unused = 4; unused < 8; ++unused) { computeState.bindBuffer(planEncoder, plan, 0, unused); } + computeState.bindBytes(planEncoder, params, sizeof(params), 8); + computeState.bindBuffer(planEncoder, snapshot, 0, 9); + [planEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(drawCount, width), 1, 1) threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; + computeState._exists.descriptorSetData.reset(); + if (!cmdEncoder->awaitEncodedWork()) { + if (!cmdEncoder->getDevice()->isLosing()) { lose("its GPU plan did not complete"); } + return; + } + MVKPerVertexScratchRequest replayRequest{}; + NSUInteger gatheredOffset = 0; + uint32_t scanSteps = 0; + // Sized with the captured record stride, not the stride of the indirect arguments. + NSUInteger snapshotOffset = NSUInteger(snapshotWords) * sizeof(uint32_t); + if (!mvkSizePerVertexIndirectReplay(plan.contents, plan.length, static_cast(snapshot.contents) + snapshotOffset, snapshot.length - snapshotOffset, drawCount, viewCount, pipeline->getPerVertexCapturedLayout().stride, topology, indexBinding != nullptr, restart, corners, + cmdEncoder->getMetalFeatures().maxMTLBufferSize, replayRequest, gatheredOffset, scanSteps)) { + uint32_t status = *(volatile const uint32_t*)plan.contents; + lose((status & 1) ? "a draw needs more records than uint32 record IDs can represent" : (status & 2) ? "a draw reads indices beyond the bound index buffer" : "its replay scratch would exceed Metal buffer limits"); + return; + } + // No draw records a vertex: Count zero, or zero vertices or instances in every draw. + if (!replayRequest.captureSize) { return; } + if (!scratch->reserveReplay(cmdEncoder->getMTLDevice(), replayRequest)) { lose("its replay scratch could not be allocated"); return; } + cmdEncoder->keepPerVertexScratchResident(); + params[17] = scanSteps; + + id captured = scratch->buffers[0]; + id occurrences = scratch->buffers[2]; + id primitiveIndices = scratch->buffers[3]; + id cornerBuffer = scratch->buffers[4] ?: plan; + id gathered = scratch->buffers[5] ?: plan; + const auto& replay = pipeline->getPerVertexReplayBinding(); + const auto& fragment = pipeline->getPerVertexInputBinding(); + // Only the draws the GPU froze (Count, clamped to maxDrawCount): mvkSizePerVertexIndirectReplay checked the word. + uint32_t frozenDraws = std::min(((const uint32_t*)plan.contents)[56], drawCount); + for (uint32_t draw = 0; draw < frozenDraws; ++draw) { + params[11] = draw; + // Plan this draw, build its replay tables and gather its indices from the frozen arguments. + cmdEncoder->encodeStoreActions(true); + planEncoder = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDrawIndirectConvertBuffers); + computeState.bindPipeline(planEncoder, helper); + computeState.bindBuffer(planEncoder, indirectBuffer, indirectOffset, 0); + computeState.bindBuffer(planEncoder, countBuffer ?: plan, countBuffer ? countOffset : 0, 1); + computeState.bindBuffer(planEncoder, plan, 0, 2); + computeState.bindBuffer(planEncoder, indexBinding ? indexBinding->mtlBuffer : plan, indexBinding ? indexBinding->offset : 0, 3); + computeState.bindBuffer(planEncoder, gathered, gatheredOffset, 4); + computeState.bindBuffer(planEncoder, occurrences, 0, 5); + computeState.bindBuffer(planEncoder, primitiveIndices, 0, 6); + computeState.bindBuffer(planEncoder, cornerBuffer, 0, 7); + computeState.bindBuffer(planEncoder, snapshot, 0, 9); + auto dispatch = [&](uint32_t phase, NSUInteger groups) { + params[15] = phase; + computeState.bindBytes(planEncoder, params, sizeof(params), 8); + [planEncoder dispatchThreadgroups: MTLSizeMake(groups, 1, 1) threadsPerThreadgroup: MTLSizeMake(phase == 1 ? 1 : width, 1, 1)]; + [planEncoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + }; + auto dispatchIndirect = [&](uint32_t phase, NSUInteger offset) { + params[15] = phase; + computeState.bindBytes(planEncoder, params, sizeof(params), 8); + [planEncoder dispatchThreadgroupsWithIndirectBuffer: plan indirectBufferOffset: offset threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; + }; + dispatch(1, 1); + if (!restart) { dispatchIndirect(2, 96); } + if (indexBinding) { dispatchIndirect(3, 112); } + [planEncoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + if (restart) { + // perVertexRestart as for direct draws, with GPU-planned parameters and dispatch sizes. + computeState.bindPipeline(planEncoder, restartState); + computeState.bindBuffer(planEncoder, scratch->buffers[5], gatheredOffset, 0); + computeState.bindBuffer(planEncoder, scratch->buffers[5], 0, 1); + computeState.bindBuffer(planEncoder, occurrences, 0, 2); + computeState.bindBuffer(planEncoder, primitiveIndices, 0, 3); + computeState.bindBuffer(planEncoder, scratch->buffers[4] ?: scratch->buffers[5], 0, 4); + computeState.bindBuffer(planEncoder, scratch->buffers[6], 0, 5); + for (uint32_t block = 0; block < scanSteps + 3; ++block) { + computeState.bindBuffer(planEncoder, plan, 1024 + 256 * block, 6); + [planEncoder dispatchThreadgroupsWithIndirectBuffer: plan indirectBufferOffset: block == scanSteps + 2 ? 160 : 144 threadsPerThreadgroup: MTLSizeMake(width, 1, 1)]; + [planEncoder memoryBarrierWithScope: MTLBarrierScopeBuffers]; + } + } + // Restore application resources displaced by the planning kernel. + computeState._exists.descriptorSetData.reset(); + if (indexBinding) { + cmdEncoder->_isIndexedDraw = true; + cmdEncoder->finalizeDrawState(kMVKGraphicsStageVertex); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + id captureEncoder = cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl); + computeState.bindPipeline(captureEncoder, captureState); + computeState.bindBuffer(captureEncoder, captured, 0, pipeline->getPerVertexCaptureBufferIndex()); + // Restart captures the compacted indices; its grid_size is still the stage-in region. + computeState.bindBuffer(captureEncoder, gathered, restart ? 0 : gatheredOffset, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::Index]); + if (pipeline->needsDrawIdBuffer()) { computeState.bindStructBytes(captureEncoder, &draw, pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::DrawId]); } + // Origin is (vertexOffset bit pattern, firstInstance); the capture guard stops at the region size. + [captureEncoder setStageInRegionWithIndirectBuffer: plan indirectBufferOffset: 48]; + if (restart) { + [captureEncoder dispatchThreadgroupsWithIndirectBuffer: scratch->buffers[6] indirectBufferOffset: 0 threadsPerThreadgroup: MTLSizeMake(1, 1, 1)]; + } else { + [captureEncoder dispatchThreadgroupsWithIndirectBuffer: plan indirectBufferOffset: 32 threadsPerThreadgroup: MTLSizeMake(captureWidth, 1, 1)]; + } + } + cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); + cmdEncoder->_isIndexedDraw = false; + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + id encoder = cmdEncoder->_mtlRenderEncoder; + if (!indexBinding) { + [encoder setRenderPipelineState: directCaptureState]; + metalState.bindVertexBuffer(encoder, captured, 0, pipeline->getPerVertexCaptureBufferIndex()); + metalState.bindVertexBuffer(encoder, plan, 512, pipeline->getPerVertexCaptureParamsBufferIndex()); + if (pipeline->needsDrawIdBuffer()) { metalState.bindVertexBytes(encoder, &draw, sizeof(draw), pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::DrawId]); } + MTLPrimitiveType captureType = pipeline->capturesPerVertexTriangleLists() ? MTLPrimitiveTypeTriangle : MTLPrimitiveTypePoint; + [encoder drawPrimitives: captureType indirectBuffer: plan indirectBufferOffset: 16]; + // A render pass boundary, not an in-pass barrier, orders the capture before the replay (see the direct draw). + cmdEncoder->encodeStoreActions(true); + cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + encoder = cmdEncoder->_mtlRenderEncoder; + } + [encoder setRenderPipelineState: pipeline->getMainPipelineState()]; + metalState.bindVertexBuffer(encoder, captured, 0, replay.vertex_buffer_index); + metalState.bindVertexBuffer(encoder, occurrences, 0, replay.occurrence_buffer_index); + if (scratch->buffers[4]) { metalState.bindVertexBuffer(encoder, scratch->buffers[4], 0, pipeline->getPerVertexReplayBarycentricBinding().corner_buffer_index); } + id replayArguments = restart ? scratch->buffers[6] : plan; + metalState.bindVertexBuffer(encoder, replayArguments, 256, replay.draw_parameters_buffer_index); + if (fragment.vertex_buffer_index != ~0u) { + metalState.bindFragmentBuffer(encoder, captured, 0, fragment.vertex_buffer_index); + metalState.bindFragmentBuffer(encoder, primitiveIndices, 0, fragment.primitive_index_buffer_index); + } + if (cmdEncoder->getPhysicalDevice()->shouldEmulateReversedDepthViewport()) { + uint32_t viewportMask = cmdEncoder->getVkGraphics()._implicitBufferData[kMVKShaderStageVertex].emulatedReversedDepthViewportMask; + metalState.bindVertexBytes(encoder, &viewportMask, sizeof(viewportMask), pipeline->getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::EmulatedReversedDepthViewport]); + } + MTLPrimitiveType replayType = replayVertices == 1 ? MTLPrimitiveTypePoint : replayVertices == 2 ? MTLPrimitiveTypeLine : MTLPrimitiveTypeTriangle; + [encoder drawPrimitives: replayType indirectBuffer: replayArguments indirectBufferOffset: restart ? 16 : 80]; + metalState._exists.vertex().descriptorSetData.reset(); + metalState._exists.fragment().descriptorSetData.reset(); + } +} + typedef struct MVKIndirectZeroDivisorVertexBuffer { uint32_t mtlBufferIndex; uint32_t stride; @@ -795,7 +1437,8 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawIndirect(): The current device does not support indirect tessellated drawing."); } - return VK_SUCCESS; + // Arguments stay on the GPU; only a bounded capture capacity is reserved here. + return drawCount ? cmdBuff->recordPerVertexIndirectDraw(false, drawCount) : VK_SUCCESS; } VkResult MVKCmdDrawIndirect::setContent(MVKCommandBuffer* cmdBuff, @@ -856,17 +1499,19 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); MVKCmdDrawIndexedIndirect diiCmd; - diiCmd.setContent(cmdEncoder->_cmdBuffer, - indirectIdxBuff->_mtlBuffer, - indirectIdxBuff->_offset, - _drawCount, - indirectIdxBuffStride); + VkResult result = diiCmd.setContent(cmdEncoder->_cmdBuffer, indirectIdxBuff->_mtlBuffer, indirectIdxBuff->_offset, _drawCount, indirectIdxBuffStride); + if (result != VK_SUCCESS) { cmdEncoder->_cmdBuffer->setConfigurationResult(result); return; } diiCmd.encode(cmdEncoder, ibb); } void MVKCmdDrawIndirect::encode(MVKCommandEncoder* cmdEncoder) { + if (!_drawCount) { return; } cmdEncoder->restartMetalRenderPassIfNeeded(); + if (auto* pipeline = cmdEncoder->getGraphicsPipeline(); pipeline->usesPerVertexInputBuffer()) { + encodePerVertexIndirect(cmdEncoder, pipeline, _mtlIndirectBuffer, _mtlIndirectBufferOffset, _mtlIndirectBufferStride, _drawCount, _mtlCountBuffer, _mtlCountBufferOffset, nullptr); + return; + } id indirectBuffer = _mtlIndirectBuffer; VkDeviceSize indirectBufferOffset = _mtlIndirectBufferOffset; uint32_t indirectBufferStride = _mtlIndirectBufferStride; @@ -889,7 +1534,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, auto& mtlFeats = cmdEncoder->getMetalFeatures(); auto& dvcLimits = cmdEncoder->getDeviceProperties().limits; // Metal doesn't support triangle fans, so encode it as indexed indirect triangles instead. - if (pipeline->getVkPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { + if (cmdEncoder->getVkGraphics().getPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { encodeIndexedIndirect(cmdEncoder, indirectBuffer, indirectBufferOffset, indirectBufferStride); return; } @@ -916,6 +1561,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, const MVKMTLBufferAllocation* tcOutBuff = nullptr; const MVKMTLBufferAllocation* tcPatchOutBuff = nullptr; const MVKMTLBufferAllocation* tcLevelBuff = nullptr; + const MVKMTLBufferAllocation* tcFloatLevelBuff = nullptr; const MVKMTLBufferAllocation* tempDrawIDBuff = nullptr; uint32_t patchCount = 0, vertexCount = 0; uint32_t inControlPointCount = 0, outControlPointCount = 0; @@ -936,6 +1582,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, outControlPointCount = pipeline->getOutputControlPointCount(); vertexCount = kMVKMaxDrawIndirectVertexCount; patchCount = mvkCeilingDivide(vertexCount, inControlPointCount); + if (!validateTessPatchCount(cmdEncoder, patchCount, pipeline->usesFloat32TessLevels())) { return; } VkDeviceSize indirectSize = (2 * sizeof(MTLDispatchThreadgroupsIndirectArguments) + sizeof(MTLDrawPatchIndirectArguments) + sizeof(MTLStageInRegionIndirectArguments)) * _drawCount; paramsIncr = std::max((size_t)dvcLimits.minUniformBufferOffsetAlignment, sizeof(uint32_t) * 2); VkDeviceSize paramsSize = paramsIncr * _drawCount; @@ -954,6 +1601,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, tcPatchOutBuff = cmdEncoder->getTempMTLBuffer(patchCount * 4 * dvcLimits.maxTessellationControlPerPatchOutputComponents, true); } tcLevelBuff = cmdEncoder->getTempMTLBuffer(patchCount * sizeof(MTLQuadTessellationFactorsHalf), true); + if (pipeline->usesFloat32TessLevels()) { tcFloatLevelBuff = cmdEncoder->getTempMTLBuffer(patchCount * kMVKFloat32TessLevelSize, true); } vtxThreadExecWidth = pipeline->getTessVertexStageState().threadExecutionWidth; NSUInteger sgSize = pipeline->getTessControlStageState().threadExecutionWidth; @@ -1004,6 +1652,9 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, state.bindStructBytes(mtlTessCtlEncoder, &_drawCount, 6); state.bindStructBytes(mtlTessCtlEncoder, &vtxThreadExecWidth, 7); state.bindStructBytes(mtlTessCtlEncoder, &tcWorkgroupSize, 8); + // The TCS reads each draw's parameters at this stride, which follows minUniformBufferOffsetAlignment. + uint32_t paramsStride = uint32_t(paramsIncr); + state.bindStructBytes(mtlTessCtlEncoder, ¶msStride, 9); if (mtlFeats.nonUniformThreadgroups) { [mtlTessCtlEncoder dispatchThreads: MTLSizeMake(_drawCount, 1, 1) threadsPerThreadgroup: MTLSizeMake(mtlConvertState.threadExecutionWidth, 1, 1)]; @@ -1079,8 +1730,8 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, offset: tcPatchOutBuff->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::PatchOutput]]; } - [mtlTessCtlEncoder setBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [mtlTessCtlEncoder setBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::TessLevel]]; [mtlTessCtlEncoder setBuffer: tcParamsBuff->_mtlBuffer offset: mtlParmBuffOfst @@ -1095,6 +1746,14 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, indirectBufferOffset: mtlIndBuffOfst threadsPerThreadgroup: MTLSizeMake(tcWorkgroupSize, 1, 1)]; mtlIndBuffOfst += sizeof(MTLDispatchThreadgroupsIndirectArguments); + if (tcFloatLevelBuff) { + // The TCS grid of this draw has at least one thread per patch. + auto& state = bindTessLevelsToHalfFactors(cmdEncoder, pipeline, mtlTessCtlEncoder, tcFloatLevelBuff, tcLevelBuff); + state.bindBuffer(mtlTessCtlEncoder, tcParamsBuff->_mtlBuffer, mtlParmBuffOfst - paramsIncr, 3); + [mtlTessCtlEncoder dispatchThreadgroupsWithIndirectBuffer: mtlIndBuff + indirectBufferOffset: mtlIndBuffOfst - sizeof(MTLDispatchThreadgroupsIndirectArguments) + threadsPerThreadgroup: MTLSizeMake(tcWorkgroupSize, 1, 1)]; + } // Running this stage prematurely ended the render pass, so we have to start it up again. // TODO: On iOS, maybe we could use a tile shader to avoid this. cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); @@ -1113,8 +1772,8 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, offset: tcPatchOutBuff->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalPatchInputBufferBinding)]; } - [cmdEncoder->_mtlRenderEncoder setVertexBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [cmdEncoder->_mtlRenderEncoder setVertexBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalLevelBufferBinding)]; [cmdEncoder->_mtlRenderEncoder setTessellationFactorBuffer: tcLevelBuff->_mtlBuffer offset: tcLevelBuff->_offset @@ -1164,12 +1823,17 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, VkDeviceSize offset, uint32_t drawCount, uint32_t stride) { + if (cmdBuff->_lastTessellationPipeline && !cmdBuff->getMetalFeatures().indirectTessellationDrawing) { + return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawIndexedIndirect(): The current device does not support indirect tessellated drawing."); + } auto* mvkBuff = (MVKBuffer*)buffer; - return setContent(cmdBuff, - mvkBuff->getMTLBuffer(), - mvkBuff->getMTLBufferOffset() + offset, - drawCount, - stride); + VkResult result = setContent(cmdBuff, + mvkBuff->getMTLBuffer(), + mvkBuff->getMTLBufferOffset() + offset, + drawCount, + stride); + // Arguments stay on the GPU; only a bounded capture capacity is reserved here. + return result == VK_SUCCESS && drawCount ? cmdBuff->recordPerVertexIndirectDraw(true, drawCount) : result; } VkResult MVKCmdDrawIndexedIndirect::setContent(MVKCommandBuffer* cmdBuff, @@ -1177,6 +1841,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, VkDeviceSize indirectMTLBuffOffset, uint32_t drawCount, uint32_t stride) { + // Also used during fan encoding: recording state may describe a later pipeline. _mtlIndirectBuffer = indirectMTLBuff; _mtlIndirectBufferOffset = indirectMTLBuffOffset; _mtlIndirectBufferStride = stride; @@ -1189,9 +1854,6 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, if ( !mtlFeats.indirectDrawing ) { return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawIndexedIndirect(): The current device does not support indirect drawing."); } - if (cmdBuff->_lastTessellationPipeline && !mtlFeats.indirectTessellationDrawing) { - return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawIndexedIndirect(): The current device does not support indirect tessellated drawing."); - } return VK_SUCCESS; } @@ -1214,11 +1876,17 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, } void MVKCmdDrawIndexedIndirect::encode(MVKCommandEncoder* cmdEncoder) { + if (!_drawCount) { return; } cmdEncoder->restartMetalRenderPassIfNeeded(); encode(cmdEncoder, cmdEncoder->getVkGraphics()._indexBuffer); } void MVKCmdDrawIndexedIndirect::encode(MVKCommandEncoder* cmdEncoder, const MVKIndexMTLBufferBinding& ibbOrig) { + if (!_drawCount) { return; } + if (auto* pipeline = cmdEncoder->getGraphicsPipeline(); pipeline->usesPerVertexInputBuffer()) { + encodePerVertexIndirect(cmdEncoder, pipeline, _mtlIndirectBuffer, _mtlIndirectBufferOffset, _mtlIndirectBufferStride, _drawCount, _mtlCountBuffer, _mtlCountBufferOffset, &ibbOrig); + return; + } id indirectBuffer = _mtlIndirectBuffer; VkDeviceSize indirectBufferOffset = _mtlIndirectBufferOffset; @@ -1262,7 +1930,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, vtxAdjmts.mtlIndexType = ibb.mtlIndexType; vtxAdjmts.isMultiView = (cmdEncoder->getSubpass()->isMultiview() && cmdEncoder->getPhysicalDevice()->canUseInstancingForMultiview()); - vtxAdjmts.isTriangleFan = pipeline->getVkPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + vtxAdjmts.isTriangleFan = cmdEncoder->getVkGraphics().getPrimitiveTopology() == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; #if MVK_USE_METAL_PRIVATE_API // With private APIs for primitive restart, we need to handle disabled restart and raw Uint8 indices. vtxAdjmts.isPrimRestart = cmdEncoder->getState().vkGraphics().isPrimitiveRestartEnabled(); @@ -1280,6 +1948,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, const MVKMTLBufferAllocation* tcOutBuff = nullptr; const MVKMTLBufferAllocation* tcPatchOutBuff = nullptr; const MVKMTLBufferAllocation* tcLevelBuff = nullptr; + const MVKMTLBufferAllocation* tcFloatLevelBuff = nullptr; const MVKMTLBufferAllocation* vtxIndexBuff = nullptr; const MVKMTLBufferAllocation* tempDrawIDBuff = nullptr; uint32_t patchCount = 0, vertexCount = 0; @@ -1302,6 +1971,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, outControlPointCount = pipeline->getOutputControlPointCount(); vertexCount = kMVKMaxDrawIndirectVertexCount; patchCount = mvkCeilingDivide(vertexCount, inControlPointCount); + if (!validateTessPatchCount(cmdEncoder, patchCount, pipeline->usesFloat32TessLevels())) { return; } VkDeviceSize indirectSize = (sizeof(MTLDispatchThreadgroupsIndirectArguments) + sizeof(MTLDrawPatchIndirectArguments) + sizeof(MTLStageInRegionIndirectArguments)) * _drawCount; paramsIncr = std::max((size_t)dvcLimits.minUniformBufferOffsetAlignment, sizeof(uint32_t) * 2); VkDeviceSize paramsSize = paramsIncr * _drawCount; @@ -1320,6 +1990,7 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, tcPatchOutBuff = cmdEncoder->getTempMTLBuffer(patchCount * 4 * dvcLimits.maxTessellationControlPerPatchOutputComponents, true); } tcLevelBuff = cmdEncoder->getTempMTLBuffer(patchCount * sizeof(MTLQuadTessellationFactorsHalf), true); + if (pipeline->usesFloat32TessLevels()) { tcFloatLevelBuff = cmdEncoder->getTempMTLBuffer(patchCount * kMVKFloat32TessLevelSize, true); } vtxIndexBuff = cmdEncoder->getTempMTLBuffer(ibb.size, true); id vtxState; @@ -1380,6 +2051,9 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, state.bindStructBytes(mtlTessCtlEncoder, &_drawCount, 6); state.bindStructBytes(mtlTessCtlEncoder, &vtxThreadExecWidth, 7); state.bindStructBytes(mtlTessCtlEncoder, &tcWorkgroupSize, 8); + // The TCS reads each draw's parameters at this stride, which follows minUniformBufferOffsetAlignment. + uint32_t paramsStride = uint32_t(paramsIncr); + state.bindStructBytes(mtlTessCtlEncoder, ¶msStride, 9); [mtlTessCtlEncoder dispatchThreadgroups: MTLSizeMake(mvkCeilingDivide(_drawCount, mtlConvertState.threadExecutionWidth), 1, 1) threadsPerThreadgroup: MTLSizeMake(mtlConvertState.threadExecutionWidth, 1, 1)]; } @@ -1468,8 +2142,8 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, offset: tcPatchOutBuff->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::PatchOutput]]; } - [mtlTessCtlEncoder setBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [mtlTessCtlEncoder setBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: pipeline->getImplicitBuffers(kMVKShaderStageTessCtl).ids[MVKImplicitBuffer::TessLevel]]; [mtlTessCtlEncoder setBuffer: tcParamsBuff->_mtlBuffer offset: mtlParmBuffOfst @@ -1484,6 +2158,14 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, indirectBufferOffset: mtlTempIndBuffOfst threadsPerThreadgroup: MTLSizeMake(tcWorkgroupSize, 1, 1)]; mtlTempIndBuffOfst += sizeof(MTLDispatchThreadgroupsIndirectArguments); + if (tcFloatLevelBuff) { + // The TCS grid of this draw has at least one thread per patch. + auto& state = bindTessLevelsToHalfFactors(cmdEncoder, pipeline, mtlTessCtlEncoder, tcFloatLevelBuff, tcLevelBuff); + state.bindBuffer(mtlTessCtlEncoder, tcParamsBuff->_mtlBuffer, mtlParmBuffOfst - paramsIncr, 3); + [mtlTessCtlEncoder dispatchThreadgroupsWithIndirectBuffer: mtlIndBuff + indirectBufferOffset: mtlTempIndBuffOfst - sizeof(MTLDispatchThreadgroupsIndirectArguments) + threadsPerThreadgroup: MTLSizeMake(tcWorkgroupSize, 1, 1)]; + } // Running this stage prematurely ended the render pass, so we have to start it up again. // TODO: On iOS, maybe we could use a tile shader to avoid this. cmdEncoder->beginMetalRenderPass(kMVKCommandUseRestartSubpass); @@ -1501,8 +2183,8 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, offset: tcPatchOutBuff->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalPatchInputBufferBinding)]; } - [cmdEncoder->_mtlRenderEncoder setVertexBuffer: tcLevelBuff->_mtlBuffer - offset: tcLevelBuff->_offset + [cmdEncoder->_mtlRenderEncoder setVertexBuffer: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_mtlBuffer + offset: (tcFloatLevelBuff ? tcFloatLevelBuff : tcLevelBuff)->_offset atIndex: cmdEncoder->getDevice()->getMetalBufferIndexForVertexAttributeBinding(kMVKTessEvalLevelBufferBinding)]; [cmdEncoder->_mtlRenderEncoder setTessellationFactorBuffer: tcLevelBuff->_mtlBuffer offset: tcLevelBuff->_offset @@ -1535,3 +2217,68 @@ static void bindIndirectZeroDivisorVertexBuffers(MVKCommandEncoder* cmdEncoder, } } } + + +#pragma mark - +#pragma mark MVKCmdDrawMeshTasks + +VkResult MVKCmdDrawMeshTasks::setContent(MVKCommandBuffer* cmdBuff, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { + // Metal executes nothing, and reports no error, for a mesh grid beyond its limit: refuse such a draw here rather than + // omit it. The Metal Feature Set Tables list 1024 threadgroups on Apple7 and Apple8, 1,048,575 on Apple9 and + // 4,194,303 on Apple10, all below Vulkan's minimum maxMeshWorkGroupTotalCount of 2^22. On Apple9 the pipeline + // reports 1,048,576, and a grid of exactly that many threadgroups draws nothing: the reported value is exclusive. + uint64_t groups = uint64_t(groupCountX) * groupCountY * groupCountZ; + if (auto* mesh = cmdBuff->getRecordedMeshPipeline(); mesh && groups >= mesh->getMaxMeshThreadgroupsPerGrid()) { + return cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCmdDrawMeshTasksEXT(): %llu workgroups reach the %lu this Metal mesh pipeline reports, beyond which it draws nothing.", (unsigned long long)groups, (unsigned long)mesh->getMaxMeshThreadgroupsPerGrid()); + } + _groupCountX = groupCountX; + _groupCountY = groupCountY; + _groupCountZ = groupCountZ; + return VK_SUCCESS; +} + +// A mesh pipeline without task shader: one mesh threadgroup per workgroup, of the mesh shader's workgroup size. +void MVKCmdDrawMeshTasks::encode(MVKCommandEncoder* cmdEncoder) { encodeMesh(cmdEncoder, false); } + +bool MVKCmdDrawMeshTasks::encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) { return encodeMesh(cmdEncoder, true); } + +// This branch has no emulated mesh capture: the prepass has nothing to capture for a native mesh draw. +bool MVKCmdDrawMeshTasks::encodeMesh(MVKCommandEncoder* cmdEncoder, bool prepass) { + if (!_groupCountX || !_groupCountY || !_groupCountZ) { return true; } + if (!prepass) { cmdEncoder->restartMetalRenderPassIfNeeded(); } + auto* pipeline = cmdEncoder->getGraphicsPipeline(); + if (!pipeline->isMeshPipeline()) { return true; } + if (prepass) { return true; } + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return true; } + [cmdEncoder->_mtlRenderEncoder drawMeshThreadgroups: MTLSizeMake(_groupCountX, _groupCountY, _groupCountZ) + threadsPerObjectThreadgroup: MTLSizeMake(1, 1, 1) + threadsPerMeshThreadgroup: pipeline->getMeshThreadgroupSize()]; + return true; +} + + +#pragma mark - +#pragma mark MVKCmdDrawMeshTasksIndirect + +VkResult MVKCmdDrawMeshTasksIndirect::setContent(MVKCommandBuffer* cmdBuff, VkBuffer buffer, VkDeviceSize offset) { + MVKBuffer* mvkBuffer = (MVKBuffer*)buffer; + _mtlIndirectBuffer = mvkBuffer->getMTLBuffer(); + _mtlIndirectBufferOffset = mvkBuffer->getMTLBufferOffset() + offset; + return VK_SUCCESS; +} + +// Native mesh pipelines only. Metal reads the same +// three words as Vulkan's VkDrawMeshTasksIndirectCommandEXT, so the grid is never read back on the CPU. Beyond its grid +// limit Metal draws nothing and reports no error, which the direct draw refuses at record time but this one cannot. +void MVKCmdDrawMeshTasksIndirect::encode(MVKCommandEncoder* cmdEncoder) { + cmdEncoder->restartMetalRenderPassIfNeeded(); + auto* pipeline = cmdEncoder->getGraphicsPipeline(); + if (!pipeline->isMeshPipeline()) { return; } + cmdEncoder->finalizeDrawState(kMVKGraphicsStageRasterization); + if (!pipeline->hasValidMTLPipelineStates()) { return; } + [cmdEncoder->_mtlRenderEncoder drawMeshThreadgroupsWithIndirectBuffer: _mtlIndirectBuffer + indirectBufferOffset: _mtlIndirectBufferOffset + threadsPerObjectThreadgroup: MTLSizeMake(1, 1, 1) + threadsPerMeshThreadgroup: pipeline->getMeshThreadgroupSize()]; +} diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.h b/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.h index 5119f8568..ae8354721 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.h +++ b/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.h @@ -25,6 +25,7 @@ class MVKCommandBuffer; class MVKPipeline; +class MVKGraphicsPipeline; class MVKPipelineLayout; class MVKDescriptorSet; class MVKDescriptorUpdateTemplate; @@ -109,6 +110,8 @@ class MVKCmdBindPipeline : public MVKCommand { VkResult setContent(MVKCommandBuffer* cmdBuff, VkPipeline pipeline); virtual bool isTessellationPipeline() { return false; }; + virtual MVKGraphicsPipeline* getGraphicsPipeline() { return nullptr; } + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKPipeline* _pipeline; @@ -125,6 +128,7 @@ class MVKCmdBindGraphicsPipeline : public MVKCmdBindPipeline { void encode(MVKCommandEncoder* cmdEncoder) override; bool isTessellationPipeline() override; + MVKGraphicsPipeline* getGraphicsPipeline() override { return (MVKGraphicsPipeline*)_pipeline; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -165,6 +169,7 @@ class MVKCmdBindDescriptorSetsStatic : public MVKCommand { const VkDescriptorSet* pDescriptorSets); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } ~MVKCmdBindDescriptorSetsStatic() override; @@ -236,6 +241,7 @@ class MVKCmdPushConstants : public MVKCommand { const void* pValues); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -265,6 +271,7 @@ class MVKCmdPushDescriptorSet : public MVKCommand { const VkWriteDescriptorSet* pDescriptorWrites); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } ~MVKCmdPushDescriptorSet() override; @@ -292,6 +299,7 @@ class MVKCmdPushDescriptorSetWithTemplate : public MVKCommand { const void* pData); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } ~MVKCmdPushDescriptorSetWithTemplate() override; diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.mm b/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.mm index 3e9fc79f2..e8e57e459 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCmdPipeline.mm @@ -39,9 +39,7 @@ for (uint32_t cbIdx = 0; cbIdx < commandBuffersCount; cbIdx++) { _secondaryCommandBuffers.push_back(MVKCommandBuffer::getMVKCommandBuffer(pCommandBuffers[cbIdx])); } - cmdBuff->recordExecuteCommands(_secondaryCommandBuffers.contents()); - - return VK_SUCCESS; + return cmdBuff->recordExecuteCommands(_secondaryCommandBuffers.contents()); } template diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h index 142499dd2..8233e85a6 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h +++ b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h @@ -235,6 +235,7 @@ class MVKCmdSetSampleLocations : public MVKCommand { const VkSampleLocationsInfoEXT* pSampleLocationsInfo); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -273,6 +274,7 @@ class MVKCmdSetViewport : public MVKCommand { const VkViewport* pViewports); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -303,6 +305,7 @@ class MVKCmdSetScissor : public MVKCommand { const VkRect2D* pScissors); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -473,6 +476,7 @@ class MVKCmdSetStencilOp : public MVKCommand { VkCompareOp compareOp); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -496,6 +500,7 @@ class MVKCmdSetStencilCompareMask : public MVKCommand { uint32_t stencilCompareMask); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -516,6 +521,7 @@ class MVKCmdSetStencilWriteMask : public MVKCommand { uint32_t stencilWriteMask); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -536,6 +542,7 @@ class MVKCmdSetStencilReference : public MVKCommand { uint32_t stencilReference); void encode(MVKCommandEncoder* cmdEncoder) override; + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } protected: MVKCommandTypePool* getTypePool(MVKCommandPool* cmdPool) override; @@ -577,6 +584,7 @@ class MVKCmdSetFrontFace : public MVKSingleValueCommand { class MVKCmdSetPatchControlPoints : public MVKSingleValueCommand { public: + VkResult setContent(MVKCommandBuffer* cmdBuff, uint32_t patchControlPoints); void encode(MVKCommandEncoder* cmdEncoder) override; protected: @@ -629,6 +637,7 @@ class MVKCmdSetLineWidth : public MVKSingleValueCommand { class MVKCmdSetPrimitiveTopology : public MVKSingleValueCommand { public: + VkResult setContent(MVKCommandBuffer* cmdBuff, VkPrimitiveTopology topology); void encode(MVKCommandEncoder* cmdEncoder) override; protected: diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm index f04a56ee7..ba1755538 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm @@ -54,6 +54,7 @@ MVKCmdBeginRenderPassBase::setContent(cmdBuff, pRenderPassBegin, pSubpassBeginInfo); _attachments.assign(attachments.begin(), attachments.end()); + cmdBuff->recordRenderPass(_attachments.contents()); _clearValues.assign(pRenderPassBegin->pClearValues, pRenderPassBegin->pClearValues + pRenderPassBegin->clearValueCount); @@ -116,6 +117,7 @@ VkResult MVKCmdEndRenderPass::setContent(MVKCommandBuffer* cmdBuff) { cmdBuff->_currentSubpassInfo = {}; + cmdBuff->recordRenderPass({}, false); return VK_SUCCESS; } @@ -151,6 +153,7 @@ } cmdBuff->_currentSubpassInfo.beginRendering(pRenderingInfo->viewMask); + cmdBuff->recordRendering(pRenderingInfo); return VK_SUCCESS; } @@ -220,6 +223,7 @@ void mvkPopulateFromOrFillAscending(Vec& dst, const uint32_t* pSrc, size_t count VkResult MVKCmdEndRendering::setContent(MVKCommandBuffer* cmdBuff) { cmdBuff->_currentSubpassInfo = {}; + cmdBuff->recordRenderPass({}, false); return VK_SUCCESS; } @@ -524,6 +528,12 @@ void mvkPopulateFromOrFillAscending(Vec& dst, const uint32_t* pSrc, size_t count #pragma mark - #pragma mark MVKCmdSetPatchControlPoints +// Recorded too, so that tessellation draws can be checked while recording. +VkResult MVKCmdSetPatchControlPoints::setContent(MVKCommandBuffer* cmdBuff, uint32_t patchControlPoints) { + cmdBuff->recordPatchControlPoints(patchControlPoints); + return MVKSingleValueCommand::setContent(cmdBuff, patchControlPoints); +} + void MVKCmdSetPatchControlPoints::encode(MVKCommandEncoder* cmdEncoder) { cmdEncoder->getState().updateDynamicState(MVKRenderStateFlag::PatchControlPoints)._renderState.patchControlPoints = static_cast(_value); } @@ -556,8 +566,16 @@ void mvkPopulateFromOrFillAscending(Vec& dst, const uint32_t* pSrc, size_t count #pragma mark - #pragma mark MVKCmdSetPrimitiveTopology +// Recorded too, so that portable PerVertexKHR draws can size their scratch while recording. +VkResult MVKCmdSetPrimitiveTopology::setContent(MVKCommandBuffer* cmdBuff, VkPrimitiveTopology topology) { + cmdBuff->recordPrimitiveTopology(topology); + return MVKSingleValueCommand::setContent(cmdBuff, topology); +} + void MVKCmdSetPrimitiveTopology::encode(MVKCommandEncoder* cmdEncoder) { - cmdEncoder->getState().updateDynamicState(MVKRenderStateFlag::PrimitiveTopology)._renderState.primitiveType = mvkMTLPrimitiveTypeFromVkPrimitiveTopology(_value); + auto& renderState = cmdEncoder->getState().updateDynamicState(MVKRenderStateFlag::PrimitiveTopology)._renderState; + renderState.primitiveType = mvkMTLPrimitiveTypeFromVkPrimitiveTopology(_value); + renderState.vkPrimitiveTopology = static_cast(_value); } diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm index ab318c57d..ee7a309eb 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm @@ -541,7 +541,7 @@ static inline MTLPixelFormat getDepthStencilAspectFormat(const MTLPixelFormat fo levels:NSMakeRange(0, srcMTLTex.mipmapLevelCount) slices:NSMakeRange(0, srcMTLTex.arrayLength) swizzle:_srcImage->getPixelFormats()->getMTLTextureSwizzleChannels(_srcImage->getVkFormat())]; - [cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id) { [srcMTLTex release]; }]; + cmdEncoder->getDevice()->addMTLCommandBufferHandler(cmdEncoder->_mtlCmdBuffer, ^(id) { [srcMTLTex release]; }); } else { srcSwizzle = mvkPackSwizzle(_srcImage->getPixelFormats()->getVkComponentMapping(_srcImage->getVkFormat())); } @@ -587,7 +587,7 @@ static inline MTLPixelFormat getDepthStencilAspectFormat(const MTLPixelFormat fo textureType: MTLTextureType2DArray levels: NSMakeRange(0, srcMTLTex.mipmapLevelCount) slices: NSMakeRange(0, srcMTLTex.arrayLength)]; - [cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id) { [srcMTLTex release]; }]; + cmdEncoder->getDevice()->addMTLCommandBufferHandler(cmdEncoder->_mtlCmdBuffer, ^(id) { [srcMTLTex release]; }); } blitKey.dstMTLPixelFormat = _dstImage->getMTLPixelFormat(dstPlaneIndex); blitKey.srcFilter = mvkMTLSamplerMinMagFilterFromVkFilter(_filter); @@ -703,7 +703,7 @@ static inline MTLPixelFormat getDepthStencilAspectFormat(const MTLPixelFormat fo #endif } id stencilMTLTex = [srcMTLTex newTextureViewWithPixelFormat: stencilFmt]; - [cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id) { [stencilMTLTex release]; }]; + cmdEncoder->getDevice()->addMTLCommandBufferHandler(cmdEncoder->_mtlCmdBuffer, ^(id) { [stencilMTLTex release]; }); [mtlRendEnc setFragmentTexture: stencilMTLTex atIndex: 1]; } else { [mtlRendEnc setFragmentTexture: srcMTLTex atIndex: 1]; @@ -1792,8 +1792,8 @@ static inline MTLPixelFormat getDepthStencilAspectFormat(const MTLPixelFormat fo size: _dataSize]; // Return the MTLBuffer allocation to the pool once the command buffer is done with it - [cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id mcb) { + cmdEncoder->getDevice()->addMTLCommandBufferHandler(cmdEncoder->_mtlCmdBuffer, ^(id mcb) { srcMTLBufferAlloc->returnToPool(); - }]; + }); } diff --git a/MoltenVK/MoltenVK/Commands/MVKCommand.h b/MoltenVK/MoltenVK/Commands/MVKCommand.h index ca029013c..10bda956a 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommand.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommand.h @@ -67,6 +67,12 @@ class MVKCommand : public MVKBaseObject, public MVKLinkableMixin { /** Encodes this command on the specified command encoder. */ virtual void encode(MVKCommandEncoder* cmdEncoder) = 0; + /** + * PerVertex prepass (MVK_CONFIG_PERVERTEX_PREPASS=1), before the render encoder of a subpass opens: applies + * Vulkan state or encodes a PerVertex capture, and returns true to continue, or false to end the prepass window. + */ + virtual bool encodePerVertexPrepass(MVKCommandEncoder*) { return false; } + protected: friend MVKCommandBuffer; @@ -90,6 +96,9 @@ class MVKSingleValueCommand : public MVKCommand { return VK_SUCCESS; } + /** Every subclass only sets dynamic Vulkan state (MVKCmdRendering.h). */ + bool encodePerVertexPrepass(MVKCommandEncoder* cmdEncoder) override { encode(cmdEncoder); return true; } + protected: Tv _value; }; diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h index f6c946254..d183c9545 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h @@ -26,7 +26,9 @@ #include "MVKCmdPipeline.h" #include "MVKQueryPool.h" #include "MVKSmallVector.h" +#include "MVKPerVertexScratch.h" #include +#include class MVKCommandPool; class MVKQueueCommandBufferSubmission; @@ -57,6 +59,10 @@ typedef struct MVKCommandEncodingContext { uint32_t firstVisibilityResultOffsetInRenderPass = 0; MVKVisibilityBuffer visibilityResultBuffer; BarrierFenceSlots fenceSlots; + const MVKPerVertexScratchReservations* perVertexScratch = nullptr; + size_t nextPerVertexScratch = 0; + /** The queue submission being encoded, which can continue on a new Metal command buffer, or null when prefilling. */ + MVKQueueCommandBufferSubmission* submission = nullptr; void syncFences(MVKDevice *device, id mtlCommandBuffer); MVKRenderPass* getRenderPass() { return _renderPass; } @@ -126,7 +132,8 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, MVKCommandPool* getCommandPool() { return _commandPool; } /** Submit the commands in this buffer as part of the queue submission. */ - void submit(MVKQueueCommandBufferSubmission* cmdBuffSubmit, MVKCommandEncodingContext* pEncodingContext); + void submit(MVKQueueCommandBufferSubmission* cmdBuffSubmit, MVKCommandEncodingContext* pEncodingContext, const MVKPerVertexScratchReservations& scratch); + VkResult reservePerVertexScratch(MVKPerVertexScratchReservations& scratch, std::unordered_set& prefilledExecutions); /** Returns whether this command buffer can be submitted to a queue more than once. */ bool getIsReusable() { return _isReusable; } @@ -136,6 +143,7 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, * from the primary command buffer. If this is a primary command buffer, returns 1. */ uint32_t getViewCount() const; + uint32_t getViewMask() const; /** Updated as renderpass commands are added. */ MVKCurrentSubpassInfo _currentSubpassInfo; @@ -149,7 +157,15 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, bool _needsVisibilityResultMTLBuffer; /** Called when a MVKCmdExecuteCommands is added to this command buffer. */ - void recordExecuteCommands(MVKArrayRef secondaryCommandBuffers); + VkResult recordExecuteCommands(MVKArrayRef secondaryCommandBuffers); + + void recordRenderPass(MVKArrayRef attachments, bool attachmentsKnown = true); + void recordRendering(const VkRenderingInfo* renderingInfo); + void recordIndexType(VkIndexType indexType) { _recordedIndexType = indexType; } + VkResult recordPerVertexDraw(uint32_t vertexCount, uint32_t instanceCount, bool indexed = false); + /** Reserves the bounded scratch of a portable PerVertexKHR indirect command; its arguments are only read on the GPU. */ + VkResult recordPerVertexIndirectDraw(bool indexed, uint32_t drawCount); + VkResult recordPerVertexTessEvalDraw(uint32_t vertexCount, uint32_t instanceCount, bool indexed); /** Called when a timestamp command is added. */ void recordTimestampCommand(); @@ -162,6 +178,22 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, /** The most recent recorded tessellation pipeline */ MVKCmdBindPipeline* _lastTessellationPipeline; + bool recordedGraphicsPipelineUsesPerVertexInput() const { return _recordedPerVertexPipeline != nullptr; } + bool recordedGraphicsPipelineUsesPerVertexTessEval() const; + /** The mesh pipeline bound for the draws being recorded, if any. */ + MVKGraphicsPipeline* getRecordedMeshPipeline() const { return _recordedMeshPipeline; } + + /** + * The recorded graphics pipeline if its draws take the ordinary tessellation path, and the patch control points + * they will encode with: the recorded dynamic value when the pipeline declares it dynamic, as while encoding. + */ + MVKGraphicsPipeline* getRecordedTessellationPipeline() const { return _recordedTessellationPipeline; } + uint32_t getRecordedPatchControlPoints() const; + void recordPatchControlPoints(uint32_t count) { _recordedPatchControlPoints = static_cast(count); } + + /** The topology the recorded portable PerVertexKHR pipeline draws with: the recorded dynamic value when the pipeline declares it dynamic, as while encoding. */ + VkPrimitiveTopology getRecordedPerVertexTopology() const; + void recordPrimitiveTopology(VkPrimitiveTopology topology) { _recordedPrimitiveTopology = topology; } #pragma mark Construction @@ -197,6 +229,21 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, void flushImmediateCmdEncoder(); void checkDeferredEncoding(); void beginSecondaryEncoding(MVKCommandEncoder* cmdEncoder); + VkResult validateIndexedPerVertexAttachments(); + VkResult reservePrefilledPerVertexScratch(size_t firstRequest); + VkResult reservePerVertexScratch(const std::vector& requests, MVKPerVertexScratchReservations& scratch); + std::vector _perVertexScratchRequests; + MVKPerVertexScratchReservations _prefilledPerVertexScratch; + MVKGraphicsPipeline* _recordedPerVertexPipeline = nullptr; + MVKGraphicsPipeline* _recordedTessellationPipeline = nullptr; + MVKGraphicsPipeline* _recordedMeshPipeline = nullptr; + uint8_t _recordedPatchControlPoints = 0; + VkPrimitiveTopology _recordedPrimitiveTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; + + bool _recordedAttachmentsKnown = false; + const char* _recordedRenderingError = nullptr; + VkIndexType _recordedIndexType = VK_INDEX_TYPE_MAX_ENUM; + bool _needsInheritedPerVertexAttachments = false; MVKCommand* _head = nullptr; MVKCommand* _tail = nullptr; @@ -237,6 +284,9 @@ class MVKCommandBuffer : public MVKDispatchableVulkanAPIObject, /*** Holds a collection of active queries for each query pool. */ typedef std::unordered_map> MVKActivatedQueries; +/** MVK_CONFIG_MESH_INDIRECT_GPU=1: vkCmdDrawMeshTasksIndirectEXT with drawCount 1 (MVKCmdDrawMeshTasksIndirect). */ +bool mvkMeshIndirectGpu(); + /** * MVKCommandEncoder uses a visitor design pattern iterate the commands in a MVKCommandBuffer, * tracking and caching dynamic encoding state, and encoding the commands onto Metal MTLCommandBuffers. @@ -261,6 +311,23 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { /** Encode commands from the specified secondary command buffer onto the Metal command buffer. */ void encodeSecondary(MVKCommandBuffer* secondaryCmdBuffer); + /** + * Commits the work encoded so far, waits until it completes on the GPU, then continues encoding on a new Metal + * command buffer of the same queue submission. Ends any active Metal encoder; the caller restarts its render pass. + * Returns false, and stops encoding this submission, if the submission cannot continue. + */ + bool awaitEncodedWork(); + + /** Returns whether encoding stopped after a failed awaitEncodedWork(). */ + bool isEncodingStopped() { return _isEncodingStopped; } + + /** Stops encoding the rest of this submission, after its device was lost while encoding it. */ + void stopEncoding() { _isEncodingStopped = true; } + + /** Pushes or pops a debug group of the Metal command buffer, carried over to later Metal command buffers. */ + void pushCommandBufferDebugGroup(NSString* name); + void popCommandBufferDebugGroup(); + /** Begins a render pass and establishes initial draw state. */ void beginRenderpass(MVKCommand* passCmd, VkSubpassContents subpassContents, @@ -298,6 +365,9 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { /** Returns the render subpass that is currently active. */ MVKRenderSubpass* getSubpass(); + /** Checks the actual views that must survive an indexed PerVertex compute capture. */ + const char* getIndexedPerVertexAttachmentError(); + /** Returns the sample count of the current render subpass. */ VkSampleCountFlagBits getSampleCount() { return getSubpass()->getSampleCount(); } @@ -508,6 +578,22 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { /** Indicates whether the current draw is an indexed draw. */ bool _isIndexedDraw; + /** Consume a preflight reservation and retain its resources until Metal completes. */ + MVKPerVertexScratch* nextPerVertexScratch(); + + /** Keeps every buffer of the last reservation, including ones added since, resident and alive for the current Metal command buffer. */ + void keepPerVertexScratchResident(); + + /** True while the PerVertex prepass encodes the captures of a subpass, before its render encoder opens. */ + bool _isPerVertexPrepassing = false; + + /** The PerVertex scratch slots below this index were captured by the prepass of the current subpass. */ + size_t _perVertexPrepassEnd = 0; + + /** Records a mesh capture of the prepass, then returns its output buffer when the subpass encodes that draw, or null. */ + void addPerVertexPrepassMeshOutput(MVKCommand* draw, const MVKMTLBufferAllocation* output); + const MVKMTLBufferAllocation* takePerVertexPrepassMeshOutput(MVKCommand* draw); + #pragma mark Construction MVKCommandEncoder(MVKCommandBuffer* cmdBuffer, @@ -523,6 +609,7 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { bool hasMoreMultiviewPasses(); void beginNextMultiviewPass(); void encodeCommandsImpl(MVKCommand* command); + void encodePerVertexPrepass(MVKCommand* command); void encodeGPUCounterSample(MVKGPUCounterQueryPool* mvkQryPool, uint32_t sampleIndex, MVKCounterSamplingFlags samplingPoints); void encodeTimestampStageCounterSamples(); id getStageCountersMTLFence(); @@ -538,6 +625,7 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { VkRect2D _renderArea; MVKCommand* _lastMultiviewPassCmd; + size_t _firstSubpassPerVertexScratch = 0; MVKActivatedQueries* _pActivatedQueries; MVKSmallVector _timestampStageCounterQueries; MVKSmallVector _clearValues; @@ -553,8 +641,13 @@ class MVKCommandEncoder : public MVKBaseDeviceObject { MVKCommandUse _mtlComputeEncoderUse; uint32_t _mtlComputeEncoderStages; MVKCommandUse _mtlBlitEncoderUse; + MVKSmallVector _commandBufferDebugGroups; + std::shared_ptr _lastPerVertexScratch; + MVKSmallVector, 8> _perVertexPrepassMesh; + size_t _perVertexPrepassMeshNext = 0; bool _isRenderingEntireAttachment; bool _hasMTLRenderEncoderVisibilityResultBuffer; + bool _isEncodingStopped = false; }; diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm index de39c9616..bec2d83ce 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm @@ -25,10 +25,31 @@ #include "MVKFoundation.h" #include "MVKCmdDraw.h" #include "MVKCmdRendering.h" +#include "MVKOSExtensions.h" #include using namespace std; +// MVK_CONFIG_PERVERTEX_PREPASS=1 encodes +// the PerVertex captures of each subpass before its render encoder opens. +static int mvkPerVertexPrepassMode() { + static const int mode = int(mvkGetEnvVarNumber("MVK_CONFIG_PERVERTEX_PREPASS", -1)); + return mode; +} + +// MVK_CONFIG_PERVERTEX_BATCH_RESIDENCY=1: the PerVertex scratch of a Metal command buffer enters and leaves the +// residency set with one commit each, when that command buffer is committed and once it completed, not per draw. +static bool mvkPerVertexBatchResidency() { + static const bool isEnabled = mvkGetEnvVarNumber("MVK_CONFIG_PERVERTEX_BATCH_RESIDENCY", 0) == 1; + return isEnabled; +} + +// MVK_CONFIG_MESH_INDIRECT_GPU=1: vkCmdDrawMeshTasksIndirectEXT with drawCount 1 (MVKCmdDrawMeshTasksIndirect). +bool mvkMeshIndirectGpu() { + static const bool isEnabled = mvkGetEnvVarNumber("MVK_CONFIG_MESH_INDIRECT_GPU", 0) == 1; + return isEnabled; +} + #pragma mark - #pragma mark MVKCommandEncodingContext @@ -143,6 +164,16 @@ _secondaryInheritanceRenderingInfo.pColorAttachmentFormats = _secondaryInheritanceColorAttachmentFormats.data(); } + if (_doesContinueRenderPass) { + _currentSubpassInfo.renderpass = (MVKRenderPass*)_secondaryInheritanceInfo.renderPass; + _currentSubpassInfo.subpassIndex = _secondaryInheritanceInfo.subpass; + auto* framebuffer = (MVKFramebuffer*)_secondaryInheritanceInfo.framebuffer; + auto attachments = framebuffer ? framebuffer->getAttachments() : MVKArrayRef(); + // An empty inherited framebuffer may be imageless. Its actual views are only known by the primary. + recordRenderPass(attachments, attachments.size() != 0); + // Inherited sample count alone cannot tell us whether the primary has resolve views. + } + if (pInheritAttLocInfo) { _secondaryInheritanceColorAttachmentLocations.assign(pInheritAttLocInfo->pColorAttachmentLocations, pInheritAttLocInfo->pColorAttachmentLocations + pInheritAttLocInfo->colorAttachmentCount); @@ -167,13 +198,18 @@ } } - if(_device->shouldPrefillMTLCommandBuffers() && !(_isSecondary || _supportsConcurrentExecution)) { + // Immediate prefill encodes each command while it is recorded, before a queue submission exists to wait for the + // GPU plan of a PerVertexKHR indirect draw. Devices that may capture PerVertexKHR draws encode at submission instead. + auto prefillStyle = getMVKConfig().prefillMetalCommandBuffers; + bool isImmediatePrefill = (prefillStyle == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_IMMEDIATE_ENCODING || + prefillStyle == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_IMMEDIATE_ENCODING_NO_AUTORELEASE); + bool mayAwaitGPUPlan = getPhysicalDevice()->isPortablePerVertexEnabled() && getEnabledFragmentShaderBarycentricFeatures().fragmentShaderBarycentric; + if(_device->shouldPrefillMTLCommandBuffers() && !(_isSecondary || _supportsConcurrentExecution) && !(isImmediatePrefill && mayAwaitGPUPlan)) { @autoreleasepool { _prefilledMTLCmdBuffer = [_commandPool->getMTLCommandBuffer(kMVKCommandUseBeginCommandBuffer, 0) retain]; // retained - auto prefillStyle = getMVKConfig().prefillMetalCommandBuffers; - if (prefillStyle == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_IMMEDIATE_ENCODING || - prefillStyle == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_IMMEDIATE_ENCODING_NO_AUTORELEASE ) { + if (isImmediatePrefill) { _immediateCmdEncodingContext = new MVKCommandEncodingContext; + _immediateCmdEncodingContext->perVertexScratch = &_prefilledPerVertexScratch; _immediateCmdEncoder = new MVKCommandEncoder(this, prefillStyle); _immediateCmdEncoder->beginEncoding(_prefilledMTLCmdBuffer, _immediateCmdEncodingContext); } @@ -213,6 +249,7 @@ delete _immediateCmdEncodingContext; _immediateCmdEncodingContext = nullptr; + if (!_isReusable) { releaseRecordedCommands(); } } } @@ -244,6 +281,17 @@ _needsVisibilityResultMTLBuffer = false; _hasStageCounterTimestampCommand = false; _lastTessellationPipeline = nullptr; + _recordedPerVertexPipeline = nullptr; + _recordedTessellationPipeline = nullptr; + _recordedMeshPipeline = nullptr; + _recordedPatchControlPoints = 0; + _recordedPrimitiveTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; + _recordedAttachmentsKnown = false; + _recordedRenderingError = nullptr; + _recordedIndexType = VK_INDEX_TYPE_MAX_ENUM; + _needsInheritedPerVertexAttachments = false; + _perVertexScratchRequests.clear(); + _prefilledPerVertexScratch.clear(); setConfigurationResult(VK_NOT_READY); if (mvkAreAllFlagsEnabled(flags, VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT)) { @@ -263,11 +311,21 @@ } void MVKCommandBuffer::checkDeferredEncoding() { - if ( !_prefilledMTLCmdBuffer ) { return; } - - if (getMVKConfig().prefillMetalCommandBuffers == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_DEFERRED_ENCODING) { + if (_prefilledMTLCmdBuffer && getMVKConfig().prefillMetalCommandBuffers == MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS_STYLE_DEFERRED_ENCODING) { + // An indirect PerVertexKHR draw or a PerVertex TES draw waits for its GPU plan or topology while encoding, + // which needs its queue submission. Nothing is encoded yet: release the empty prefilled Metal command buffer + // and encode at submission. + for (const auto& request : _perVertexScratchRequests) { + if (request.indirectCapacity || request.tessSizes[5]) { + clearPrefilledMTLCommandBuffer(); + return; + } + } + setConfigurationResult(reservePrefilledPerVertexScratch(0)); + if (!wasConfigurationSuccessful()) { return; } @autoreleasepool { MVKCommandEncodingContext encodingContext; + encodingContext.perVertexScratch = &_prefilledPerVertexScratch; MVKCommandEncoder encoder(this); encoder.encode(_prefilledMTLCmdBuffer, &encodingContext); if (isUsingMetalArgumentBuffers()) { @@ -289,30 +347,30 @@ _commandCount++; - if(_immediateCmdEncoder) { - _immediateCmdEncoder->encodeCommands(command); - if( !_isReusable ) { - releaseCommands(command); - return; - } - } - + // Multiview rewinds through _next while encoding the subpass terminator. + // Link first and retain even one-time commands until flush so replay can traverse them. if (_tail) { _tail->_next = command; } command->_next = nullptr; _tail = command; if ( !_head ) { _head = command; } + if (_immediateCmdEncoder) { _immediateCmdEncoder->encodeCommands(command); } } -void MVKCommandBuffer::submit(MVKQueueCommandBufferSubmission* cmdBuffSubmit, - MVKCommandEncodingContext* pEncodingContext) { +void MVKCommandBuffer::submit(MVKQueueCommandBufferSubmission* cmdBuffSubmit, MVKCommandEncodingContext* pEncodingContext, const MVKPerVertexScratchReservations& scratch) { if ( !canExecute() ) { return; } if (_prefilledMTLCmdBuffer) { cmdBuffSubmit->setActiveMTLCommandBuffer(_prefilledMTLCmdBuffer); clearPrefilledMTLCommandBuffer(); + _prefilledPerVertexScratch.clear(); } else { + pEncodingContext->perVertexScratch = &scratch; + pEncodingContext->nextPerVertexScratch = 0; + pEncodingContext->submission = cmdBuffSubmit; MVKCommandEncoder encoder(this); encoder.encode(cmdBuffSubmit->getActiveMTLCommandBuffer(), pEncodingContext); + pEncodingContext->submission = nullptr; + pEncodingContext->perVertexScratch = nullptr; } if ( !_supportsConcurrentExecution ) { _isExecutingNonConcurrently.clear(); } @@ -340,6 +398,10 @@ // Return the number of bits set in the view mask, with a minimum value of 1. uint32_t MVKCommandBuffer::getViewCount() const { + return max(__builtin_popcount(getViewMask()), 1); +} + +uint32_t MVKCommandBuffer::getViewMask() const { uint32_t viewMask = 0; if (_doesContinueRenderPass) { MVKRenderPass* inheritedRenderPass = (MVKRenderPass*)_secondaryInheritanceInfo.renderPass; @@ -351,19 +413,16 @@ } else { viewMask = _currentSubpassInfo.subpassViewMask; } - return max(__builtin_popcount(viewMask), 1); + return viewMask; } void MVKCommandBuffer::clearPrefilledMTLCommandBuffer() { - // Metal command buffers do not return to their pool on release, nor do they support the - // concept of a reset. In order to become available again in their pool, they must pass - // through the commit step. This is unfortunate because if the app adds commands to this - // command buffer and then chooses to reset it instead of submit it, we risk committing - // a prefilled Metal command buffer that the app did not intend to submit, potentially - // causing unexpected side effects. But unfortunately there is nothing else we can do. + // A prefilled Metal command buffer that was never submitted is released without being committed: Metal then + // discards the commands the application abandoned, and frees its place in its queue, which an enqueued command + // buffer would keep. Its handlers run now, as cleanup, while the pools they return buffers to still exist. if (_prefilledMTLCmdBuffer && _prefilledMTLCmdBuffer.status == MTLCommandBufferStatusNotEnqueued) { - [_prefilledMTLCmdBuffer commit]; + getDevice()->abandonMTLCommandBuffer(_prefilledMTLCmdBuffer); } [_prefilledMTLCmdBuffer release]; @@ -385,13 +444,254 @@ } // Promote the initial visibility buffer and indication of timestamp use from the secondary buffers. -void MVKCommandBuffer::recordExecuteCommands(MVKArrayRef secondaryCommandBuffers) { - for (MVKCommandBuffer* cmdBuff : secondaryCommandBuffers) { - if (cmdBuff->_needsVisibilityResultMTLBuffer) { _needsVisibilityResultMTLBuffer = true; } - if (cmdBuff->_hasStageCounterTimestampCommand) { _hasStageCounterTimestampCommand = true; } +VkResult MVKCommandBuffer::recordExecuteCommands(MVKArrayRef secondaryCommandBuffers) { + size_t firstRequest = _perVertexScratchRequests.size(); + try { + for (MVKCommandBuffer* cmdBuff : secondaryCommandBuffers) { + if (cmdBuff->_needsVisibilityResultMTLBuffer) { _needsVisibilityResultMTLBuffer = true; } + if (cmdBuff->_hasStageCounterTimestampCommand) { _hasStageCounterTimestampCommand = true; } + if (cmdBuff->getConfigurationResult() != VK_SUCCESS) { return cmdBuff->getConfigurationResult(); } + if (cmdBuff->_needsInheritedPerVertexAttachments) { + VkResult result = validateIndexedPerVertexAttachments(); + if (result != VK_SUCCESS) { return result; } + } + _perVertexScratchRequests.insert(_perVertexScratchRequests.end(), cmdBuff->_perVertexScratchRequests.begin(), cmdBuff->_perVertexScratchRequests.end()); + } + } catch (const std::bad_alloc&) { return reportError(VK_ERROR_OUT_OF_HOST_MEMORY, "Portable PerVertexKHR secondary scratch recording allocation failed."); } + if (_immediateCmdEncoder) { + for (size_t i = firstRequest; i < _perVertexScratchRequests.size(); ++i) { + if (_perVertexScratchRequests[i].indirectCapacity) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indirect draws cannot be encoded while immediately prefilling Metal command buffers."); } + } + } + return _immediateCmdEncoder ? reservePrefilledPerVertexScratch(firstRequest) : VK_SUCCESS; +} + +void MVKCommandBuffer::recordRenderPass(MVKArrayRef attachments, bool attachmentsKnown) { + _recordedAttachmentsKnown = attachmentsKnown; + _recordedRenderingError = nullptr; + for (auto* attachment : attachments) { + if (attachment && attachment->getImage()->getMTLStorageMode() == MTLStorageModeMemoryless) { _recordedRenderingError = "indexed compute capture cannot preserve memoryless attachments."; } } } +void MVKCommandBuffer::recordRendering(const VkRenderingInfo* renderingInfo) { + recordRenderPass({}); + MVKRenderingAttachmentIterator attachments(renderingInfo); + attachments.iterate([&](const VkRenderingAttachmentInfo* info, VkImageAspectFlagBits aspect, MVKImageView* attachment, bool isResolveAttachment) { + if (!attachment) { return; } + // Match MVKRenderSubpass: color uses non-null resolve views; depth/stencil also require a resolve mode. + if (isResolveAttachment && info->imageView && (aspect == VK_IMAGE_ASPECT_COLOR_BIT || info->resolveMode != VK_RESOLVE_MODE_NONE)) { + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT && info->resolveMode != VK_RESOLVE_MODE_AVERAGE_BIT) { _recordedRenderingError = "indexed capture requires average-mode dynamic color resolve."; } + if (const char* error = mvkGetPerVertexResolveError(getMetalFeatures(), getPixelFormats(), attachment->getVkFormat(), aspect, info->resolveMode, renderingInfo->flags)) { _recordedRenderingError = error; } + } + if (attachment->getImage()->getMTLStorageMode() == MTLStorageModeMemoryless) { _recordedRenderingError = "indexed compute capture cannot preserve memoryless attachments."; } + }); +} + +VkResult MVKCommandBuffer::validateIndexedPerVertexAttachments() { + const char* error = _recordedRenderingError; + if (!_recordedAttachmentsKnown && !_doesContinueRenderPass) { error = "indexed compute capture requires known rendering attachments."; } + if (_currentSubpassInfo.renderpass) { + if (const char* resolveError = _currentSubpassInfo.renderpass->getSubpass(_currentSubpassInfo.subpassIndex)->getPerVertexResolveError()) { error = resolveError; } + } + return error ? reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR %s", error) : VK_SUCCESS; +} + +VkResult MVKCommandBuffer::recordPerVertexDraw(uint32_t vertexCount, uint32_t instanceCount, bool indexed) { + if (!_recordedPerVertexPipeline || !vertexCount || !instanceCount) { return VK_SUCCESS; } + if (_recordedPerVertexPipeline->usesPerVertexTessEval()) { return recordPerVertexTessEvalDraw(vertexCount, instanceCount, indexed); } + // Dynamic restart can change at execution. Reserve both helper paths before submission. + bool dynamicRestart = indexed && _recordedPerVertexPipeline->getDynamicStateFlags().has(MVKRenderStateFlag::PrimitiveRestartEnable); + bool restart = mvkPerVertexRequiresRestartAssembly(indexed, dynamicRestart || _recordedPerVertexPipeline->getStaticStateData().enable.has(MVKRenderStateEnableFlag::PrimitiveRestart)); + if (restart && !mvkCanAssemblePerVertexRestart(vertexCount, instanceCount, getMetalFeatures().indirectDrawing)) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR restart requires indirect drawing and uint32 dense capture record IDs."); } + if (indexed && !mvkPerVertexIndexSize(_recordedIndexType)) { + return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indexed capture requires uint8, uint16 or uint32 indices."); + } + // Indexed capture runs in compute, and a render pass boundary separates a nonindexed capture from its replay: + // either ends and restarts the render encoder. A secondary may omit its framebuffer or inherit an imageless one. + // Revalidate in every primary. + if (_doesContinueRenderPass) { _needsInheritedPerVertexAttachments = true; } + VkResult result = validateIndexedPerVertexAttachments(); + if (result != VK_SUCCESS) { return result; } + auto topology = getRecordedPerVertexTopology(); + // A dynamic topology may be any of its class, adjacency included, which capture and replay do not support. + if (!mvkPerVertexReplayVertexCount(topology)) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR draws do not support this primitive topology."); } + uint64_t indexScratchBytes = restart ? mvkPerVertexRestartIndexScratchSize(vertexCount) : indexed && _recordedIndexType == VK_INDEX_TYPE_UINT8 ? uint64_t(vertexCount) * sizeof(uint16_t) : 0; + if (indexScratchBytes > getMetalFeatures().maxMTLBufferSize) { return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Portable PerVertexKHR index scratch buffer exceeds Metal limits."); } + uint32_t viewMask = getViewMask(); + uint32_t passCount = std::max(getDevice()->getMultiviewMetalPassCount(viewMask), 1u); + MVKPerVertexScratchRequest requests[32]; + for (uint32_t pass = 0; pass < passCount; ++pass) { + uint32_t views = std::max(getDevice()->getViewCountInMetalPass(viewMask, pass), 1u); + uint64_t expandedInstances = uint64_t(instanceCount) * views; + if (expandedInstances > UINT32_MAX || !mvkCanEncodePerVertexDraw(vertexCount, uint32_t(expandedInstances), _recordedPerVertexPipeline->getPerVertexCapturedLayout().stride, topology, getMetalFeatures().maxMTLBufferSize)) { + return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Portable PerVertexKHR draw exceeds its capture or replay buffer limits."); + } + if (restart && !mvkCanAssemblePerVertexRestart(vertexCount, uint32_t(expandedInstances), getMetalFeatures().indirectDrawing)) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR restart requires indirect drawing and uint32 dense capture record IDs."); } + uint64_t primitiveCount = uint64_t(mvkPerVertexPrimitiveCount(vertexCount, topology)) * expandedInstances; + uint64_t occurrences = primitiveCount * mvkPerVertexReplayVertexCount(topology); + requests[pass] = {NSUInteger(uint64_t(vertexCount) * expandedInstances * _recordedPerVertexPipeline->getPerVertexCapturedLayout().stride), NSUInteger(occurrences * 2 * sizeof(uint32_t)), NSUInteger(primitiveCount * 3 * sizeof(uint32_t)), _recordedPerVertexPipeline->usesPortableBarycentrics() ? NSUInteger(occurrences * sizeof(uint32_t)) : 0, NSUInteger(indexScratchBytes), restart, dynamicRestart && _recordedIndexType == VK_INDEX_TYPE_UINT8}; + } + size_t firstRequest = _perVertexScratchRequests.size(); + try { + // Draw-major storage: every pass owns immutable CPU tables and independent GPU scratch. + _perVertexScratchRequests.insert(_perVertexScratchRequests.end(), requests, requests + passCount); + } catch (const std::bad_alloc&) { return reportError(VK_ERROR_OUT_OF_HOST_MEMORY, "Portable PerVertexKHR scratch recording allocation failed."); } + return _immediateCmdEncoder ? reservePrefilledPerVertexScratch(firstRequest) : VK_SUCCESS; +} + +// Vulkan places no bound on indirect vertex and instance counts, and they may be written on the GPU earlier in +// the same submission. Only a GPU plan, sized for the largest record count a uint32 record ID can represent, +// is reserved before submission. Once the plan completes, the queue reserves the exact replay scratch from the +// counts it froze, then encodes the replay (encodePerVertexIndirect). +VkResult MVKCommandBuffer::recordPerVertexIndirectDraw(bool indexed, uint32_t drawCount) { + if (!_recordedPerVertexPipeline) { return VK_SUCCESS; } + // Immediate prefill encodes each command as it is recorded, before a queue submission exists to wait for the plan. + if (_immediateCmdEncoder) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indirect draws cannot be encoded while immediately prefilling Metal command buffers."); } + auto* pipeline = _recordedPerVertexPipeline; + if (pipeline->getZeroDivisorVertexBindings().size()) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indirect draws do not support zero-divisor vertex bindings yet."); } + if (indexed && !mvkPerVertexIndexSize(_recordedIndexType)) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indexed capture requires uint8, uint16 or uint32 indices."); } + // Dynamic restart can change at execution. Reserve the restart assembly whenever it is possible. + bool restart = indexed && (pipeline->getDynamicStateFlags().has(MVKRenderStateFlag::PrimitiveRestartEnable) || pipeline->getStaticStateData().enable.has(MVKRenderStateEnableFlag::PrimitiveRestart)); + if (restart && !getMetalFeatures().indirectDrawing) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR restart requires indirect drawing."); } + // Each indirect draw plans on the GPU in compute, which ends and restarts the render encoder. + if (_doesContinueRenderPass) { _needsInheritedPerVertexAttachments = true; } + VkResult result = validateIndexedPerVertexAttachments(); + if (result != VK_SUCCESS) { return result; } + auto topology = getRecordedPerVertexTopology(); + if (!mvkPerVertexReplayVertexCount(topology)) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indirect draws do not support this primitive topology."); } + MVKPerVertexScratchRequest request{}; + request.restart = restart; + request.indirectCapacity = UINT32_MAX; + // Plan words, then one 256-byte perVertexRestart parameter block per dispatch, laid out for the plan's ceiling. + uint64_t planSize = 1024 + (restart ? (mvkPerVertexIndirectRestartScanSteps(UINT32_MAX) + 3) * 256 : 0); + // Several draws freeze their Vulkan commands in a buffer of their own, at their size. Vulkan bounds maxDrawCount by + // the argument buffer, whose stride is at least that size: a legal command never exceeds the Metal limits here. + uint64_t snapshotSize = drawCount > 1 ? uint64_t(drawCount) * (indexed ? 5 : 4) * sizeof(uint32_t) : 0; + if (planSize > getMetalFeatures().maxMTLBufferSize || snapshotSize > getMetalFeatures().maxMTLBufferSize) { return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Portable PerVertexKHR indirect argument snapshot exceeds Metal buffer limits."); } + request.planSize = NSUInteger(planSize); + request.snapshotSize = NSUInteger(snapshotSize); + // Every multiview pass plans and replays separately. + uint32_t passCount = std::max(getDevice()->getMultiviewMetalPassCount(getViewMask()), 1u); + size_t firstRequest = _perVertexScratchRequests.size(); + try { + _perVertexScratchRequests.insert(_perVertexScratchRequests.end(), passCount, request); + } catch (const std::bad_alloc&) { return reportError(VK_ERROR_OUT_OF_HOST_MEMORY, "Portable PerVertexKHR scratch recording allocation failed."); } + return _immediateCmdEncoder ? reservePrefilledPerVertexScratch(firstRequest) : VK_SUCCESS; +} + +// The GPU generator emits at most the uniform level-3 topology per triangle patch: 13 triangles, +// captured once per corner. Levels outside the proven topologies refuse the draw on the GPU. +static constexpr uint64_t kMVKPerVertexTessMaxRecordsPerPatch = 39; + +VkResult MVKCommandBuffer::recordPerVertexTessEvalDraw(uint32_t vertexCount, uint32_t instanceCount, bool indexed) { + if (indexed || instanceCount != 1 || getViewMask()) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "PerVertex TES test requires direct non-indexed draws of one instance without views."); } + // Admission fixes three control points. Vertices that do not complete a patch are not tessellated, but + // Vulkan still runs the VS for each of them: without a complete patch, only the VS output is reserved. + uint64_t patches = vertexCount / 3; + // VS/TCS/TES run in compute, which ends and restarts the render encoder. + if (_doesContinueRenderPass) { _needsInheritedPerVertexAttachments = true; } + VkResult result = validateIndexedPerVertexAttachments(); + if (result != VK_SUCCESS) { return result; } + uint64_t records = patches * kMVKPerVertexTessMaxRecordsPerPatch; + uint32_t stride = _recordedPerVertexPipeline->getPerVertexCapturedLayout().stride; + if (records > UINT32_MAX || !mvkCanEncodePerVertexDraw(uint32_t(records), 1, stride, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, getMetalFeatures().maxMTLBufferSize)) { + return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "PerVertex TES draw exceeds its capture or replay buffer limits."); + } + MVKPerVertexScratchRequest request = {NSUInteger(records * stride), NSUInteger(records * 2 * sizeof(uint32_t)), NSUInteger(records * sizeof(uint32_t)), _recordedPerVertexPipeline->usesPortableBarycentrics() ? NSUInteger(records * sizeof(uint32_t)) : 0}; + const auto& limits = getDeviceProperties().limits; + // Invocation records, VS output, TCS vertex and patch output, float32 levels (outer[4], inner[2]), + // then the generator plan: status, replay arguments and constants, per-patch counts and offsets, patch indices. + uint64_t sizes[] = {patches ? 16 + records * 32 : 0, uint64_t(vertexCount) * 4 * limits.maxVertexOutputComponents, patches * 3 * 4 * limits.maxTessellationControlPerVertexOutputComponents, patches * 4 * limits.maxTessellationControlPerPatchOutputComponents, patches * 6 * sizeof(float), patches ? mvkPerVertexTessPlanSize(patches) : 0}; + for (size_t i = 0; i < std::size(sizes); ++i) { + if (sizes[i] > getMetalFeatures().maxMTLBufferSize) { return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "PerVertex TES scratch exceeds Metal buffer limits."); } + request.tessSizes[i] = NSUInteger(sizes[i]); + } + size_t firstRequest = _perVertexScratchRequests.size(); + try { + _perVertexScratchRequests.push_back(request); + } catch (const std::bad_alloc&) { return reportError(VK_ERROR_OUT_OF_HOST_MEMORY, "Portable PerVertexKHR scratch recording allocation failed."); } + return _immediateCmdEncoder ? reservePrefilledPerVertexScratch(firstRequest) : VK_SUCCESS; +} + +VkResult MVKCommandBuffer::reservePrefilledPerVertexScratch(size_t firstRequest) { + try { + std::vector requests(_perVertexScratchRequests.begin() + firstRequest, _perVertexScratchRequests.end()); + MVKPerVertexScratchReservations scratch; + VkResult result = reservePerVertexScratch(requests, scratch); + if (result != VK_SUCCESS) { return result; } + _prefilledPerVertexScratch.insert(_prefilledPerVertexScratch.end(), scratch.begin(), scratch.end()); + return VK_SUCCESS; + } catch (const std::bad_alloc&) { return VK_ERROR_OUT_OF_HOST_MEMORY; } +} + +VkResult MVKCommandBuffer::reservePerVertexScratch(MVKPerVertexScratchReservations& scratch, std::unordered_set& prefilledExecutions) { + if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); } + if (!wasConfigurationSuccessful()) { return getConfigurationResult(); } + if (_perVertexScratchRequests.empty()) { return VK_SUCCESS; } + // Prefilled buffers are consumed once. Do not consume them during preflight: a later + // batch can still fail, and the application must be able to retry the whole submit. + if (_prefilledMTLCmdBuffer && prefilledExecutions.insert(this).second) { + scratch = _prefilledPerVertexScratch; + return VK_SUCCESS; + } + return reservePerVertexScratch(_perVertexScratchRequests, scratch); +} + +VkResult MVKCommandBuffer::reservePerVertexScratch(const std::vector& requests, MVKPerVertexScratchReservations& scratch) { + if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); } + MVKPerVertexScratchReservations pending; + if (!mvkReservePerVertexScratch(getMTLDevice(), requests, pending, _device->getPerVertexScratchPool())) { + VkResult result = _device->getConfigurationResult(); + return result != VK_SUCCESS ? result : reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Portable PerVertexKHR scratch reservation failed."); + } + auto* pool = _commandPool->getCommandEncodingPool(); + for (size_t i = 0; i < requests.size(); ++i) { + if (requests[i].indirectCapacity) { + id state = pool->getPerVertexIndirectMTLComputePipelineState(); + VkResult result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + if (!state || !state.maxTotalThreadsPerThreadgroup || !state.threadExecutionWidth) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR indirect planning pipeline is unavailable or has invalid dispatch limits."); } + pending[i]->indirectPipeline = [state retain]; + if (!requests[i].restart) { continue; } + // The restart helper is prepared now; its scratch is reserved with the replay. + id restartState = pool->getPerVertexRestartMTLComputePipelineState(); + result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + if (!restartState || !restartState.maxTotalThreadsPerThreadgroup || !restartState.threadExecutionWidth) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR index helper pipeline is unavailable or has invalid dispatch limits."); } + pending[i]->indexPipeline = [restartState retain]; + continue; + } + if (requests[i].tessSizes[5]) { + id state = pool->getPerVertexTessTopologyMTLComputePipelineState(); + VkResult result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + if (!state || !state.maxTotalThreadsPerThreadgroup || !state.threadExecutionWidth) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "PerVertex TES topology pipeline is unavailable or has invalid dispatch limits."); } + pending[i]->tessTopologyPipeline = [state retain]; + continue; + } + if (!requests[i].indexSize) { continue; } + // Secondary requests are flattened into the primary. Prepare in its pool + // and retain with scratch so encoding never compiles a helper after success. + id state = requests[i].restart ? pool->getPerVertexRestartMTLComputePipelineState() : pool->getConvertUint8IndicesMTLComputePipelineState(true); + VkResult result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + if (!state || !state.maxTotalThreadsPerThreadgroup || !state.threadExecutionWidth) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR index helper pipeline is unavailable or has invalid dispatch limits."); } + pending[i]->indexPipeline = [state retain]; + if (requests[i].widenUint8) { + id widen = pool->getConvertUint8IndicesMTLComputePipelineState(true); + result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + if (!widen || !widen.maxTotalThreadsPerThreadgroup || !widen.threadExecutionWidth) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR uint8 helper pipeline is unavailable or has invalid dispatch limits."); } + pending[i]->widenUint8Pipeline = [widen retain]; + } + } + VkResult result = _device->getConfigurationResult(); + if (result != VK_SUCCESS) { return result; } + scratch = std::move(pending); + return VK_SUCCESS; +} + // Track whether a stage-based timestamp command has been added, so we know // to update the timestamp command fence when ending a Metal command encoder. void MVKCommandBuffer::recordTimestampCommand() { @@ -402,8 +702,28 @@ #pragma mark - #pragma mark Tessellation constituent command management +// Compute binds preserve the graphics pipeline used to validate recorded draws. +bool MVKCommandBuffer::recordedGraphicsPipelineUsesPerVertexTessEval() const { return _recordedPerVertexPipeline && _recordedPerVertexPipeline->usesPerVertexTessEval(); } + void MVKCommandBuffer::recordBindPipeline(MVKCmdBindPipeline* mvkBindPipeline) { _lastTessellationPipeline = mvkBindPipeline->isTessellationPipeline() ? mvkBindPipeline : nullptr; + if (auto* graphics = mvkBindPipeline->getGraphicsPipeline()) { + _recordedPerVertexPipeline = graphics->usesPerVertexInputBuffer() ? graphics : nullptr; + bool ordinaryTessellation = graphics->isTessellationPipeline() && !graphics->usesPerVertexTessEval() && !graphics->usesPerVertexInputBuffer(); + _recordedTessellationPipeline = ordinaryTessellation ? graphics : nullptr; + _recordedMeshPipeline = graphics->isMeshPipeline() ? graphics : nullptr; + } +} + +VkPrimitiveTopology MVKCommandBuffer::getRecordedPerVertexTopology() const { + bool dynamic = _recordedPerVertexPipeline->getDynamicStateFlags().has(MVKRenderStateFlag::PrimitiveTopology); + return dynamic ? _recordedPrimitiveTopology : _recordedPerVertexPipeline->getVkPrimitiveTopology(); +} + +uint32_t MVKCommandBuffer::getRecordedPatchControlPoints() const { + if (!_recordedTessellationPipeline) { return 0; } + bool dynamic = _recordedTessellationPipeline->getDynamicStateFlags().has(MVKRenderStateFlag::PatchControlPoints); + return dynamic ? _recordedPatchControlPoints : _recordedTessellationPipeline->getStaticStateData().patchControlPoints; } @@ -449,7 +769,7 @@ } void MVKCommandEncoder::encodeCommandsImpl(MVKCommand* command) { - while(command) { + while(command && !_isEncodingStopped) { uint32_t prevMVPassIdx = _multiviewPassIndex; command->encode(this); @@ -472,12 +792,124 @@ void MVKCommandEncoder::encodeSecondary(MVKCommandBuffer* secondaryCmdBuffer) { secondaryCmdBuffer->beginSecondaryEncoding(this); MVKCommand* cmd = secondaryCmdBuffer->_head; - while (cmd) { + while (cmd && !_isEncodingStopped) { cmd->encode(this); cmd = cmd->_next; } } +bool MVKCommandEncoder::awaitEncodedWork() { + endCurrentMetalEncoding(); + auto* submission = _pEncodingContext->submission; + if (submission) { + for (size_t i = 0; i < _commandBufferDebugGroups.size(); ++i) { [_mtlCmdBuffer popDebugGroup]; } + // Released by a completed handler of the committed Metal command buffer. + _stageCountersMTLFence = nil; + _mtlCmdBuffer = submission->continueOnNewMTLCommandBuffer(); + } else { + _mtlCmdBuffer = nil; + } + if ( !_mtlCmdBuffer ) { + _isEncodingStopped = true; + return false; + } + _cmdBuffer->setMetalObjectLabel(_mtlCmdBuffer, _cmdBuffer->_debugName); + for (NSString* name : _commandBufferDebugGroups) { [_mtlCmdBuffer pushDebugGroup: name]; } + return true; +} + +void MVKCommandEncoder::pushCommandBufferDebugGroup(NSString* name) { + [_mtlCmdBuffer pushDebugGroup: name]; + _commandBufferDebugGroups.push_back([name retain]); +} + +void MVKCommandEncoder::popCommandBufferDebugGroup() { + [_mtlCmdBuffer popDebugGroup]; + if (_commandBufferDebugGroups.empty()) { return; } + [_commandBufferDebugGroups.back() release]; + _commandBufferDebugGroups.pop_back(); +} + +const char* MVKCommandEncoder::getIndexedPerVertexAttachmentError() { + if (!isInRenderPass()) { return "indexed compute capture requires known rendering attachments."; } + if (const char* error = getSubpass()->getPerVertexResolveError()) { return error; } + // These are the begin-render-pass views, including imageless and dynamic rendering attachments. + // An empty list in an active render pass is valid; there is no image content to preserve. + for (auto* attachment : _attachments) { + if (attachment && attachment->getImage()->getMTLStorageMode() == MTLStorageModeMemoryless) { return "indexed compute capture cannot preserve memoryless attachments."; } + } + return nullptr; +} + +MVKPerVertexScratch* MVKCommandEncoder::nextPerVertexScratch() { + auto* reservations = _pEncodingContext->perVertexScratch; + size_t first = _pEncodingContext->nextPerVertexScratch; + uint32_t passCount = std::max(getSubpass()->getMultiviewMetalPassCount(), 1u); + if (!reservations || first > reservations->size() || passCount > reservations->size() - first || _multiviewPassIndex >= passCount) { + // The draw cannot be encoded: lose the device rather than complete the submission without it. + _cmdBuffer->setConfigurationResult(_cmdBuffer->reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR draw has no scratch reservation.")); + getDevice()->markLost(); + stopEncoding(); + return nullptr; + } + _lastPerVertexScratch = (*reservations)[first + _multiviewPassIndex]; + _pEncodingContext->nextPerVertexScratch += passCount; + // The prepass already kept a captured slot resident for this Metal command buffer. + if (first >= _perVertexPrepassEnd) { keepPerVertexScratchResident(); } + return _lastPerVertexScratch.get(); +} + +void MVKCommandEncoder::addPerVertexPrepassMeshOutput(MVKCommand* draw, const MVKMTLBufferAllocation* output) { + _perVertexPrepassMesh.push_back({draw, output}); +} + +const MVKMTLBufferAllocation* MVKCommandEncoder::takePerVertexPrepassMeshOutput(MVKCommand* draw) { + if (_perVertexPrepassMeshNext >= _perVertexPrepassMesh.size() || _perVertexPrepassMesh[_perVertexPrepassMeshNext].first != draw) { return nullptr; } + return _perVertexPrepassMesh[_perVertexPrepassMeshNext++].second; +} + +// Before the render encoder of a subpass opens, applies the Vulkan state commands that follow it and encodes their +// PerVertex captures on one compute encoder, up to the first command that ends the window: barrier, end of the +// subpass, copy, query, secondary, or a draw whose capture must stay in place (nonindexed, TES, indirect). Vulkan +// orders any write a later vertex shader of the subpass reads before the subpass, or behind such a barrier. +// The subpass then encodes from its first command again, from the restored state and scratch cursor: the draws +// below _perVertexPrepassEnd replay without capture, so the render encoder no longer restarts for them. +void MVKCommandEncoder::encodePerVertexPrepass(MVKCommand* command) { + _perVertexPrepassEnd = 0; + _perVertexPrepassMesh.clear(); + _perVertexPrepassMeshNext = 0; + // Prefill encodes each command as it is recorded; multiview encodes the subpass once per view group. + if (mvkPerVertexPrepassMode() != 1 || !command || _cmdBuffer->_immediateCmdEncoder || _subpassContents != VK_SUBPASS_CONTENTS_INLINE || getSubpass()->isMultiview()) { return; } + size_t first = _pEncodingContext->nextPerVertexScratch; + MVKVulkanStateSnapshot saved = _state.saveVulkanState(); + _isPerVertexPrepassing = true; + while (command && !_isEncodingStopped && command->encodePerVertexPrepass(this)) { command = command->_next; } + _isPerVertexPrepassing = false; + _state.restoreVulkanState(std::move(saved)); + _perVertexPrepassEnd = _pEncodingContext->nextPerVertexScratch; + _pEncodingContext->nextPerVertexScratch = first; +} + +void MVKCommandEncoder::keepPerVertexScratchResident() { + auto scratch = _lastPerVertexScratch; + auto* device = getDevice(); + // Pool buffers stay resident from their creation (MVKPerVertexScratchPool): the reference below only delays their return. + bool changesResidency = !scratch->pool; + bool isBatched = changesResidency && mvkPerVertexBatchResidency(); + if (isBatched) { + for (auto buffer : scratch->buffers) { if (buffer) { device->addMTLCommandBufferResidency(_mtlCmdBuffer, buffer); } } + } else if (changesResidency) { + for (auto buffer : scratch->buffers) { if (buffer) { device->makeResident(buffer); } } + } + // This also protects buffers encoded with unretained Metal command buffers and + // prefills that outlive their Vulkan command buffer's recorded command list. + // Its release, once this command buffer completed, is what returns pool buffers. + device->addMTLCommandBufferHandler(_mtlCmdBuffer, ^(id) { + if ( !changesResidency || isBatched ) { return; } + for (auto buffer : scratch->buffers) { if (buffer) { device->removeResidency(buffer); } } + }); +} + void MVKCommandEncoder::beginRendering(MVKCommand* rendCmd, const VkRenderingInfo* pRenderingInfo) { VkSubpassContents contents = (mvkIsAnyFlagEnabled(pRenderingInfo->flags, VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) @@ -575,6 +1007,7 @@ if (renderPass) { renderPass->encodeSubpassDependencyBarriers(this, subpassIndex); } _lastMultiviewPassCmd = subpassCmd; + _firstSubpassPerVertexScratch = _pEncodingContext->nextPerVertexScratch; _subpassContents = subpassContents; _renderSubpassIndex = subpassIndex; _multiviewPassIndex = 0; @@ -582,6 +1015,7 @@ auto& mtlFeats = getMetalFeatures(); _canUseLayeredRendering = mtlFeats.layeredRendering && (mtlFeats.multisampleLayeredRendering || getSubpass()->getSampleCount() == VK_SAMPLE_COUNT_1_BIT); + encodePerVertexPrepass(subpassCmd ? subpassCmd->_next : nullptr); beginMetalRenderPass(cmdUse); } @@ -590,6 +1024,7 @@ void MVKCommandEncoder::beginNextMultiviewPass() { encodeStoreActions(); _multiviewPassIndex++; + _pEncodingContext->nextPerVertexScratch = _firstSubpassPerVertexScratch; beginMetalRenderPass(kMVKCommandUseNextSubpass); } @@ -1195,7 +1630,7 @@ static bool shouldStartNewEncoder(MVKCommandUse prev, MVKCommandUse next) { // Return the MTLBuffer allocation to the pool once the command buffer is done with it const MVKMTLBufferAllocation* MVKCommandEncoder::getTempMTLBuffer(NSUInteger length, bool isPrivate, bool isDedicated) { MVKMTLBufferAllocation* mtlBuffAlloc = getCommandEncodingPool()->acquireMTLBufferAllocation(length, isPrivate, isDedicated); - [_mtlCmdBuffer addCompletedHandler: ^(id mcb) { mtlBuffAlloc->returnToPool(); }]; + getDevice()->addMTLCommandBufferHandler(_mtlCmdBuffer, ^(id mcb) { mtlBuffAlloc->returnToPool(); }); return mtlBuffAlloc; } @@ -1313,7 +1748,7 @@ static bool shouldStartNewEncoder(MVKCommandUse prev, MVKCommandUse next) { // Create MTLFence as local ref and pass to completion handler // block to release once MTLCommandBuffer no longer needs it. id mtlFence = [getMTLDevice() newFence]; - [_mtlCmdBuffer addCompletedHandler: ^(id mcb) { [mtlFence release]; }]; + getDevice()->addMTLCommandBufferHandler(_mtlCmdBuffer, ^(id mcb) { [mtlFence release]; }); _stageCountersMTLFence = mtlFence; // retained } @@ -1345,14 +1780,22 @@ static bool shouldStartNewEncoder(MVKCommandUse prev, MVKCommandUse next) { if ( !_pActivatedQueries ) { return; } MVKActivatedQueries* pAQs = _pActivatedQueries; - [_mtlCmdBuffer addCompletedHandler: ^(id mtlCmdBuff) { + _pActivatedQueries = nullptr; + // After a failed continuation, no Metal command buffer remains to complete the queries. + if ( !_mtlCmdBuffer ) { + for (auto& qryPair : *pAQs) { qryPair.first->release(); } + delete pAQs; + return; + } + getDevice()->addMTLCommandBufferHandler(_mtlCmdBuffer, ^(id mtlCmdBuff) { + // An abandoned command buffer never ran its queries. + bool ran = mtlCmdBuff.status != MTLCommandBufferStatusNotEnqueued; for (auto& qryPair : *pAQs) { - qryPair.first->finishQueries(qryPair.second.contents()); + if (ran) { qryPair.first->finishQueries(qryPair.second.contents()); } qryPair.first->release(); } delete pAQs; - }]; - _pActivatedQueries = nullptr; + }); } @@ -1377,6 +1820,7 @@ static bool shouldStartNewEncoder(MVKCommandUse prev, MVKCommandUse next) { } MVKCommandEncoder::~MVKCommandEncoder() { + for (NSString* name : _commandBufferDebugGroups) { [name release]; } [_mtlRenderEncoder release]; [_mtlComputeEncoder release]; [_mtlBlitEncoder release]; diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h index 0a4d180e1..4bc193a0f 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h @@ -212,6 +212,9 @@ struct MVKVulkanGraphicsCommandEncoderState: public MVKVulkanCommonEncoderState bool getProvokingVertexMode() const { return pickRenderState(MVKRenderStateFlag::ProvokingVertexMode).provokingVertexMode; } + VkPrimitiveTopology getPrimitiveTopology() const { + return static_cast(pickRenderState(MVKRenderStateFlag::PrimitiveTopology).vkPrimitiveTopology); + } /** Bind the given descriptor sets, placing their bindings into `_descriptorSetBindings`. */ void bindDescriptorSets(MVKPipelineLayout* layout, @@ -424,6 +427,13 @@ struct MVKMetalComputeCommandEncoderState { #pragma mark - MVKCommandEncoderState +/** The Vulkan state the PerVertex prepass applies ahead of a subpass, then restores before encoding it. */ +struct MVKVulkanStateSnapshot { + MVKVulkanSharedCommandEncoderState shared; + MVKVulkanGraphicsCommandEncoderState graphics; + MVKVulkanComputeCommandEncoderState compute; +}; + /** Holds both Metal and Vulkan state for both compute and graphics. */ class MVKCommandEncoderState { MVKVulkanSharedCommandEncoderState _vkShared; @@ -456,6 +466,13 @@ class MVKCommandEncoderState { MVKMetalGraphicsCommandEncoderState& mtlGraphics() { return _mtlGraphics; } /** Returns a reference to the Metal compute state. */ MVKMetalComputeCommandEncoderState& mtlCompute() { return _mtlCompute; } + /** Copies the Vulkan state, to restore it with restoreVulkanState(). Metal state is not saved: a new encoder resets it. */ + MVKVulkanStateSnapshot saveVulkanState() const { return {_vkShared, _vkGraphics, _vkCompute}; } + void restoreVulkanState(MVKVulkanStateSnapshot&& saved) { + _vkShared = std::move(saved.shared); + _vkGraphics = std::move(saved.graphics); + _vkCompute = std::move(saved.compute); + } /** * Updates the given dynamic state, invalidating the passed flags on the Metal graphics state. @@ -480,6 +497,10 @@ class MVKCommandEncoderState { MVKShaderStage shaderStage = stage == kMVKGraphicsStageVertex ? kMVKShaderStageVertex : kMVKShaderStageTessCtl; _mtlCompute.prepareRenderDispatch(encoder, mvkEncoder, _vkGraphics, _vkShared, shaderStage); } + void prepareTessEvalDispatch(id encoder, MVKCommandEncoder& mvkEncoder) { + _mtlCompute.prepareRenderDispatch(encoder, mvkEncoder, _vkGraphics, _vkShared, kMVKShaderStageTessEval); + } + /** Binds everything needed to dispatch a Vulkan compute shader on the current Metal compute state. */ void prepareComputeDispatch(id encoder, MVKCommandEncoder& mvkEncoder) { _mtlCompute.prepareComputeDispatch(encoder, mvkEncoder, _vkCompute, _vkShared); diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm index 199172a04..a659964ee 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm @@ -966,17 +966,21 @@ static bool isCompatible(MVKUseResourceHelper::ResourceInfo current, MVKUseResou } MVKVulkanCommonEncoderState::MVKVulkanCommonEncoderState(const MVKVulkanCommonEncoderState& other) { - memcpy(_descriptorSets, other._descriptorSets, sizeof(_descriptorSets)); - _pushDescriptor = other._pushDescriptor; - setLayout(other._layout); - memcpy(_pushDescriptor.cpuBuffer, other._pushDescriptor.cpuBuffer, _pushDescriptor.cpuBufferSize); + *this = other; } +// Unused before the PerVertex prepass. The copy owns its push descriptor storage: the previous version kept cpuBuffer +// pointing into the source unless the copy had to grow, so a restored state could alias a destroyed snapshot. MVKVulkanCommonEncoderState& MVKVulkanCommonEncoderState::operator=(const MVKVulkanCommonEncoderState& other) { - memmove(_descriptorSets, other._descriptorSets, sizeof(_descriptorSets)); + if (this == &other) { return *this; } + _layout = other._layout; _pushDescriptor = other._pushDescriptor; - setLayout(other._layout); - memmove(_pushDescriptor.cpuBuffer, other._pushDescriptor.cpuBuffer, _pushDescriptor.cpuBufferSize); + _pushDescData.resize(other._pushDescData.size()); + if (other._pushDescData.size()) { memcpy(_pushDescData.data(), other._pushDescData.data(), other._pushDescData.size()); } + if (other._pushDescriptor.cpuBuffer) { _pushDescriptor.cpuBuffer = reinterpret_cast(_pushDescData.data()); } + for (uint32_t i = 0; i < kMVKMaxDescriptorSetCount; i++) { + _descriptorSets[i] = other._descriptorSets[i] == &other._pushDescriptor ? &_pushDescriptor : other._descriptorSets[i]; + } return *this; } @@ -1469,7 +1473,11 @@ static bool shouldEmulateReversedDepthViewport(const MVKPhysicalDevice* physical bindState(encoder, mvkEncoder, vk); // Resources - if (pipeline->isTessellationPipeline()) { + if (pipeline->usesPerVertexTessEval()) { + // Replay has its own buffers; TES resources were consumed by the compute stage. + } else if (pipeline->isMeshPipeline()) { + // Admitted mesh pipelines have neither vertex input nor mesh-stage resources. + } else if (pipeline->isTessellationPipeline()) { bindVulkanGraphicsToMetalGraphics(encoder, mvkEncoder, vk, vkShared, *this, pipeline, kMVKShaderStageTessEval, MVKMetalGraphicsStage::Vertex); } else { bindVulkanGraphicsToMetalGraphics(encoder, mvkEncoder, vk, vkShared, *this, pipeline, kMVKShaderStageVertex, MVKMetalGraphicsStage::Vertex); @@ -1643,7 +1651,11 @@ static bool shouldEmulateReversedDepthViewport(const MVKPhysicalDevice* physical id mtlPipeline = nil; if (stage == kMVKShaderStageVertex) { - if (!mvkEncoder._isIndexedDraw) { + if (pipeline->usesPerVertexInputBuffer() && !pipeline->usesPerVertexTessEval()) { + auto* subpass = mvkEncoder.getSubpass(); + uint32_t viewCount = subpass->isMultiview() ? subpass->getViewCountInMetalPass(mvkEncoder.getMultiviewPassIndex()) : 1; + mtlPipeline = pipeline->getPerVertexIndexedCapturePipelineState(vk.isPrimitiveRestartEnabled() || vk._indexBuffer.vkIndexType == VK_INDEX_TYPE_UINT32, viewCount); + } else if (!mvkEncoder._isIndexedDraw) { mtlPipeline = pipeline->getTessVertexStageState(); } else if (vk._indexBuffer.mtlIndexType == MTLIndexTypeUInt16) { mtlPipeline = pipeline->getTessVertexStageIndex16State(); @@ -1652,6 +1664,8 @@ static bool shouldEmulateReversedDepthViewport(const MVKPhysicalDevice* physical } } else if (stage == kMVKShaderStageTessCtl) { mtlPipeline = pipeline->getTessControlStageState(); + } else if (stage == kMVKShaderStageTessEval) { + mtlPipeline = pipeline->getPerVertexTessEvalPipelineState(); } else { assert(0); } @@ -1854,17 +1868,34 @@ static void invalidateImplicitBuffer(MVKCommandEncoderState& state, VkPipelineBi } void MVKCommandEncoderState::offsetZeroDivisorVertexBuffers(MVKCommandEncoder& mvkEncoder, MVKGraphicsStage stage, MVKGraphicsPipeline* pipeline, uint32_t firstInstance) { + bool dynamicStride = pipeline->getDynamicStateFlags().has(MVKRenderStateFlag::VertexStride); for (const auto& binding : pipeline->getZeroDivisorVertexBindings()) { uint32_t mtlBuffIdx = pipeline->getMetalBufferIndexForVertexAttributeBinding(binding.first); - auto& buffer = _vkGraphics._vertexBuffers[binding.first]; + uint32_t sourceBinding = binding.first; + VkDeviceSize offset = 0; + for (const auto& translated : pipeline->getTranslatedVertexBindings()) { + if (translated.translationBinding != binding.first) { continue; } + sourceBinding = translated.binding; + offset += translated.translationOffset; + break; + } + const auto& buffer = _vkGraphics._vertexBuffers[sourceBinding]; + VkDeviceSize stride = dynamicStride ? buffer.stride : binding.second; + offset += buffer.offset + VkDeviceSize(firstInstance) * stride; switch (stage) { case kMVKGraphicsStageVertex: - [mvkEncoder.getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBufferOffset: buffer.offset + firstInstance * binding.second - atIndex: mtlBuffIdx]; + if (dynamicStride) { + [mvkEncoder.getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBufferOffset:offset attributeStride:stride atIndex:mtlBuffIdx]; + } else { + [mvkEncoder.getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBufferOffset: offset atIndex: mtlBuffIdx]; + } break; case kMVKGraphicsStageRasterization: - [mvkEncoder._mtlRenderEncoder setVertexBufferOffset:buffer.offset + firstInstance * binding.second - atIndex:mtlBuffIdx]; + if (dynamicStride) { + [mvkEncoder._mtlRenderEncoder setVertexBufferOffset:offset attributeStride:stride atIndex:mtlBuffIdx]; + } else { + [mvkEncoder._mtlRenderEncoder setVertexBufferOffset:offset atIndex:mtlBuffIdx]; + } break; default: assert(false); // If we hit this, something went wrong. diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.h b/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.h index e8b7f3e1c..4c190a596 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.h @@ -22,12 +22,16 @@ #include "MVKMTLBufferAllocation.h" #include #include +#include #import class MVKCommandPool; +/** Replay tables shared by the single-instance portable PerVertexKHR draws of a topology. Immutable once returned. */ +struct MVKPerVertexReplayTables { id pairs = nil; id indices = nil; id corners = nil; }; + #pragma mark - #pragma mark MVKCommandEncodingPool @@ -140,7 +144,19 @@ class MVKCommandEncodingPool : public MVKBaseObject { id getAccumulateOcclusionQueryResultsMTLComputePipelineState(); /** Returns a MTLComputePipelineState for converting a Uint8 index buffer to Uint16. */ - id getConvertUint8IndicesMTLComputePipelineState(); + id getConvertUint8IndicesMTLComputePipelineState(bool preserveValues = false); + + id getPerVertexRestartMTLComputePipelineState(); + + /** Returns the GPU admission and planning pipeline for portable PerVertexKHR indirect draws. */ + id getPerVertexIndirectMTLComputePipelineState(); + /** Returns the GPU topology generator for portable PerVertexKHR tessellation. */ + id getPerVertexTessTopologyMTLComputePipelineState(); + + /** Returns resident replay tables covering vertexCount single-instance vertices, or nil buffers if allocation fails. */ + MVKPerVertexReplayTables getPerVertexReplayTables(VkPrimitiveTopology topology, bool provokingLast, uint32_t vertexCount); + + /** Returns a compute pipeline state that classifies float32 TCS levels into Metal half tessellation factors. */ /** Deletes all the internal resources. */ void clear(); @@ -183,5 +199,10 @@ class MVKCommandEncodingPool : public MVKBaseObject { id _mtlDrawIndexedCopyIndexBufferComputePipelineState[2] = {nil, nil}; id _mtlCopyQueryPoolResultsComputePipelineState = nil; id _mtlAccumOcclusionQueryResultsComputePipelineState = nil; - id _mtlConvertUint8IndicesComputePipelineState = nil; + id _mtlPerVertexRestartComputePipelineState = nil; + id _mtlPerVertexIndirectComputePipelineState = nil; + id _mtlPerVertexTessTopologyComputePipelineState = nil; + id _mtlConvertUint8IndicesComputePipelineState[2] = {nil, nil}; + std::unordered_map> _perVertexReplayTables; + std::vector> _perVertexReplayBuffers; }; diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.mm b/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.mm index d7fa57c39..4c9f74f60 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncodingPool.mm @@ -19,6 +19,7 @@ #include "MVKCommandEncodingPool.h" #include "MVKCommandPool.h" #include "MVKImage.h" +#include "MVKPerVertexReplay.h" using namespace std; @@ -168,8 +169,53 @@ static constexpr uint32_t getRenderpassLoadStoreStateIndex(MVKFormatType type, b MVK_ENC_REZ_ACCESS(_mtlAccumOcclusionQueryResultsComputePipelineState, newAccumulateOcclusionQueryResultsMTLComputePipelineState(_commandPool)); } -id MVKCommandEncodingPool::getConvertUint8IndicesMTLComputePipelineState() { - MVK_ENC_REZ_ACCESS(_mtlConvertUint8IndicesComputePipelineState, newConvertUint8IndicesMTLComputePipelineState(_commandPool)); +id MVKCommandEncodingPool::getConvertUint8IndicesMTLComputePipelineState(bool preserveValues) { + // Preflight can overlap encoding on another queue. Synchronize warm reads too. + lock_guard lock(_lock); + auto& state = _mtlConvertUint8IndicesComputePipelineState[preserveValues]; + if (!state) { state = _commandPool->getDevice()->getCommandResourceFactory()->newConvertUint8IndicesMTLComputePipelineState(_commandPool, preserveValues); } + return state; +} + +id MVKCommandEncodingPool::getPerVertexRestartMTLComputePipelineState() { + lock_guard lock(_lock); + if (!_mtlPerVertexRestartComputePipelineState) { _mtlPerVertexRestartComputePipelineState = _commandPool->getDevice()->getCommandResourceFactory()->newPerVertexRestartMTLComputePipelineState(_commandPool); } + return _mtlPerVertexRestartComputePipelineState; +} + +id MVKCommandEncodingPool::getPerVertexIndirectMTLComputePipelineState() { + lock_guard lock(_lock); + if (!_mtlPerVertexIndirectComputePipelineState) { _mtlPerVertexIndirectComputePipelineState = _commandPool->getDevice()->getCommandResourceFactory()->newPerVertexIndirectMTLComputePipelineState(_commandPool); } + return _mtlPerVertexIndirectComputePipelineState; +} + +id MVKCommandEncodingPool::getPerVertexTessTopologyMTLComputePipelineState() { + lock_guard lock(_lock); + if (!_mtlPerVertexTessTopologyComputePipelineState) { _mtlPerVertexTessTopologyComputePipelineState = _commandPool->getDevice()->getCommandResourceFactory()->newPerVertexTessTopologyMTLComputePipelineState(_commandPool); } + return _mtlPerVertexTessTopologyComputePipelineState; +} + +MVKPerVertexReplayTables MVKCommandEncodingPool::getPerVertexReplayTables(VkPrimitiveTopology topology, bool provokingLast, uint32_t vertexCount) { + lock_guard lock(_lock); + auto& entry = _perVertexReplayTables[uint32_t(topology) * 2 + provokingLast]; + if (entry.second.pairs && entry.first >= vertexCount) { return entry.second; } + uint32_t capacity = mvkPerVertexSharedReplayVertexCount(vertexCount); + uint64_t primitives = std::max(mvkPerVertexPrimitiveCount(capacity, topology), 1); + uint64_t occurrences = primitives * mvkPerVertexReplayVertexCount(topology); + if (!occurrences || occurrences * 8 > _commandPool->getMetalFeatures().maxMTLBufferSize || primitives * 12 > _commandPool->getMetalFeatures().maxMTLBufferSize) { return {}; } + id mtlDev = _commandPool->getMTLDevice(); + MVKPerVertexReplayTables tables = {[mtlDev newBufferWithLength: occurrences * 8 options: MTLResourceStorageModeShared], + [mtlDev newBufferWithLength: primitives * 12 options: MTLResourceStorageModeShared], + [mtlDev newBufferWithLength: occurrences * 4 options: MTLResourceStorageModeShared]}; + auto* mvkDev = _commandPool->getDevice(); + for (id buffer : {tables.pairs, tables.indices, tables.corners}) { + if (buffer) { mvkDev->makeResident(buffer); _perVertexReplayBuffers.push_back(buffer); } + } + if (!tables.pairs.contents || !tables.indices.contents || !tables.corners.contents) { return {}; } + mvkPopulatePerVertexReplay(capacity, 1, topology, provokingLast, (uint32_t*)tables.pairs.contents, (uint32_t*)tables.indices.contents, (uint32_t*)tables.corners.contents); + // Keep superseded tables resident until pool destruction because pending command buffers may still read them. + entry = {capacity, tables}; + return tables; } void MVKCommandEncodingPool::clear() { @@ -270,6 +316,11 @@ static constexpr uint32_t getRenderpassLoadStoreStateIndex(MVKFormatType type, b [_mtlAccumOcclusionQueryResultsComputePipelineState release]; _mtlAccumOcclusionQueryResultsComputePipelineState = nil; - [_mtlConvertUint8IndicesComputePipelineState release]; - _mtlConvertUint8IndicesComputePipelineState = nil; + [_mtlPerVertexRestartComputePipelineState release]; _mtlPerVertexRestartComputePipelineState = nil; + [_mtlPerVertexIndirectComputePipelineState release]; _mtlPerVertexIndirectComputePipelineState = nil; + [_mtlPerVertexTessTopologyComputePipelineState release]; _mtlPerVertexTessTopologyComputePipelineState = nil; + for (auto& state : _mtlConvertUint8IndicesComputePipelineState) { [state release]; state = nil; } + for (id buffer : _perVertexReplayBuffers) { mvkDev->removeResidency(buffer); [buffer release]; } + _perVertexReplayBuffers.clear(); + _perVertexReplayTables.clear(); } diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h b/MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h index 9a72cc2ac..6c1e97a3c 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h @@ -386,11 +386,12 @@ kernel void cmdDrawIndirectTessConvertBuffers(const device char* srcBuff [[buffe constant uint32_t& drawCount [[buffer(6)]], constant uint32_t& vtxThreadExecWidth [[buffer(7)]], constant uint32_t& tcWorkgroupSize [[buffer(8)]], + constant uint32_t& paramsStride [[buffer(9)]], uint idx [[thread_position_in_grid]]) { if (idx >= drawCount) { return; } const device auto& src = *reinterpret_cast(srcBuff + idx * srcStride); device char* dest; - device auto* params = reinterpret_cast(paramsBuff + idx * 256); + device auto* params = reinterpret_cast(paramsBuff + idx * paramsStride); dest = destBuff + idx * (sizeof(MTLStageInRegionIndirectArguments) + sizeof(MTLDispatchThreadgroupsIndirectArguments) * 2 + sizeof(MTLDrawPatchIndirectArguments)); device auto& destSI = *(device MTLStageInRegionIndirectArguments*)dest; dest += sizeof(MTLStageInRegionIndirectArguments); @@ -425,11 +426,12 @@ kernel void cmdDrawIndexedIndirectTessConvertBuffers(const device char* srcBuff constant uint32_t& drawCount [[buffer(6)]], constant uint32_t& vtxThreadExecWidth [[buffer(7)]], constant uint32_t& tcWorkgroupSize [[buffer(8)]], + constant uint32_t& paramsStride [[buffer(9)]], uint idx [[thread_position_in_grid]]) { if (idx >= drawCount) { return; } const device auto& src = *reinterpret_cast(srcBuff + idx * srcStride); device char* dest; - device auto* params = reinterpret_cast(paramsBuff + idx * 256); + device auto* params = reinterpret_cast(paramsBuff + idx * paramsStride); dest = destBuff + idx * (sizeof(MTLStageInRegionIndirectArguments) + sizeof(MTLDispatchThreadgroupsIndirectArguments) * 2 + sizeof(MTLDrawPatchIndirectArguments)); device auto& destSI = *(device MTLStageInRegionIndirectArguments*)dest; dest += sizeof(MTLStageInRegionIndirectArguments); @@ -547,4 +549,292 @@ kernel void convertUint8IndicesRaw(device uint8_t* src [[ buffer(0) ]], uint8_t idx = src[pos]; dst[pos] = idx; } +uint perVertexRestartPrimitiveCount(uint count, uint topology) { + return topology == 0 ? count : topology == 1 ? count / 2 : topology == 2 ? (count > 1 ? count - 1 : 0) : topology == 3 ? count / 3 : (count > 2 ? count - 2 : 0); +} +// Parallel prefix over contiguous segments: (valid indices, leading run, trailing run, primitives). +// Merging adjacent intervals only changes primitives where their boundary runs join. +// p: indexCount, instances, indexBytes, topology, provokingLast, firstInstance, corners, phase, scanStep, scanSource. +// Phases: initialize, ping-pong prefix, compact/base triplets, expand instances. Barrier between every dispatch. +// args: capture dispatch at byte 0, replay draw at byte 16, replay constants at byte 256. +kernel void perVertexRestart(const device uint8_t* source [[buffer(0)]], device uint* compact [[buffer(1)]], device uint* pairs [[buffer(2)]], device uint* triplets [[buffer(3)]], device uint* corners [[buffer(4)]], device uint* args [[buffer(5)]], constant uint* p [[buffer(6)]], uint i [[thread_position_in_grid]]) { + uint topology = p[3], rasterVertices = topology == 0 ? 1 : topology <= 2 ? 2 : 3; + if (!p[0]) { + if (i) { return; } + args[0] = 0; args[1] = 0; args[2] = 1; args[3] = 0; + args[4] = 0; args[5] = 0; args[6] = 0; args[7] = p[5]; + args[64] = 0; args[65] = 0; args[66] = p[5]; args[67] = 0; + return; + } + if (p[7] == 3) { + uint primitives = args[4] / rasterVertices; + if (i >= primitives * p[1]) { return; } + uint instance = i / primitives, primitive = i % primitives; + for (uint corner = 0; corner < 3; ++corner) { + // Capture's grid_size is the original stage-in region, not the compact dispatch size. + uint record = instance * p[0] + triplets[3 * primitive + corner]; + // Keep the base triplets read-only while other instances read them. + if (instance) { triplets[3 * i + corner] = record; } + if (corner >= rasterVertices) { continue; } + uint occurrence = i * rasterVertices + corner; + pairs[2 * occurrence] = record; + pairs[2 * occurrence + 1] = i; + if (p[6]) { corners[occurrence] = corner; } + } + return; + } + if (i >= p[0]) { return; } + uint64_t offset = uint64_t(p[0]) + uint64_t(p[9]) * p[0] * 4 + uint64_t(i) * 4; + if (p[7] == 0) { + uint index = 0; + for (uint b = 0; b < p[2]; ++b) { index |= uint(source[uint64_t(i) * p[2] + b]) << (8 * b); } + uint sentinel = p[2] == 1 ? 255u : p[2] == 2 ? 65535u : 0xffffffffu; + uint valid = uint(index != sentinel); + compact[offset] = valid; compact[offset + 1] = valid; compact[offset + 2] = valid; + compact[offset + 3] = perVertexRestartPrimitiveCount(valid, topology); + return; + } + uint captured = compact[offset], head = compact[offset + 1], tail = compact[offset + 2], primitives = compact[offset + 3]; + if (p[7] == 1) { + uint step = p[8]; + if (i >= step) { + uint64_t left = offset - uint64_t(step) * 4; + uint leftCaptured = compact[left], leftHead = compact[left + 1], leftTail = compact[left + 2]; + primitives += compact[left + 3] - perVertexRestartPrimitiveCount(leftTail, topology); + primitives -= perVertexRestartPrimitiveCount(head, topology); + primitives += perVertexRestartPrimitiveCount(leftTail + head, topology); + tail = captured == step ? leftTail + tail : tail; + head = leftCaptured == min(step, i - step + 1) ? leftHead + head : leftHead; + captured += leftCaptured; + } + uint64_t destination = uint64_t(p[0]) + uint64_t(p[9] ^ 1) * p[0] * 4 + uint64_t(i) * 4; + compact[destination] = captured; compact[destination + 1] = head; compact[destination + 2] = tail; compact[destination + 3] = primitives; + return; + } + if (i == p[0] - 1) { + args[0] = captured; args[1] = captured ? p[1] : 0; args[2] = 1; args[3] = 0; + args[4] = primitives * rasterVertices; args[5] = primitives ? p[1] : 0; args[6] = 0; args[7] = p[5]; + args[64] = 0; args[65] = 0; args[66] = p[5]; args[67] = primitives * rasterVertices; + } + if (!tail) { return; } // Restart marker: never invoke the application's vertex shader for it. + uint index = 0; + for (uint b = 0; b < p[2]; ++b) { index |= uint(source[uint64_t(i) * p[2] + b]) << (8 * b); } + compact[captured - 1] = index; + if (perVertexRestartPrimitiveCount(tail, topology) == perVertexRestartPrimitiveCount(tail - 1, topology)) { return; } + uint a = captured - 1, b = a, c = a; + if (rasterVertices == 2) { a = captured - 2; } + if (rasterVertices == 3) { a = captured - 3; b = captured - 2; } + if (topology == 4 && ((tail - 3) & 1)) { + if (p[4]) { uint tmp = a; a = b; b = tmp; } + else { uint tmp = b; b = c; c = tmp; } + } + if (topology == 5) { + a = p[4] ? captured - tail : captured - 2; + b = p[4] ? captured - 2 : captured - 1; + c = p[4] ? captured - 1 : captured - tail; + } + triplets[3 * (primitives - 1)] = a; + triplets[3 * (primitives - 1) + 1] = b; + triplets[3 * (primitives - 1) + 2] = c; +} +// Portable PerVertexKHR indirect draws, planned from Vulkan arguments and Count read on the GPU. +// p: maxDrawCount, stride, indexBytes, index capacity lo/hi, record capacity, views, topology, +// provokingLast, corners, hasCount, draw, table/gather/capture threadgroup widths, phase, +// restart, restart scan steps, snapshot word offset. +// Phases: admit every draw of the command, plan one draw, build replay tables, gather indices. +// Plan words: 0 status (1 capacity, 2 index range), 4 capture draw, 8 capture dispatch, +// 12 stage-in region, 20 replay draw, 24 table dispatch, 28 gather dispatch, 32 table params, +// 36 restart dispatch, 40 restart expansion dispatch, 48-52 one-draw argument snapshot, +// 53-54 saturated 64-bit physical record requirement, 55 snapshot-valid flag, 56 frozen Count, +// 64 replay constants (byte 256), 128 capture params (byte 512), +// then perVertexRestart parameters every 64 words from byte 1024. +// Snapshot s: from word p[18], each draw's Vulkan command, four words, five when indexed. A single draw binds the plan +// with p[18] = 48; several draws bind a buffer of their own, so that it never exceeds the argument buffer. +kernel void perVertexIndirect(const device uint8_t* args [[buffer(0)]], const device uint* count [[buffer(1)]], device uint* w [[buffer(2)]], const device uint8_t* source [[buffer(3)]], device uint* gathered [[buffer(4)]], device uint* pairs [[buffer(5)]], device uint* triplets [[buffer(6)]], device uint* corners [[buffer(7)]], constant uint* p [[buffer(8)]], device uint* s [[buffer(9)]], uint i [[thread_position_in_grid]]) { + uint draws = p[15] == 0 ? (p[10] ? min(p[0], count[0]) : p[0]) : w[56]; + uint topology = p[7], rasterVertices = topology == 0 ? 1 : topology <= 2 ? 2 : 3; + if (p[15] == 0) { + if (i == 0) { w[56] = draws; } + if (i >= draws) { return; } + const device uint* a = reinterpret_cast(args + ulong(i) * p[1]); + uint words = p[2] ? 5u : 4u; + device uint* snapshot = s + ulong(p[18]) + ulong(i) * words; + for (uint word = 0; word < words; ++word) { snapshot[word] = a[word]; } + if (p[0] == 1) { + ulong base = ulong(a[0]) * a[1]; + ulong lower = ulong(uint(base)) * p[6]; + // The high limb detects overflow without a full-width division in Metal. + ulong upper = ulong(uint(base >> 32)) * p[6] + (lower >> 32); + w[53] = upper > 0xfffffffful ? 0xffffffffu : uint(lower); + w[54] = upper > 0xfffffffful ? 0xffffffffu : uint(upper); + w[55] = 1; + } + if (!a[0] || !a[1]) { return; } + // Physical records are instances x views x vertices; dense record IDs stay below the reserved capacity. + if (ulong(a[0]) * a[1] > ulong(p[5]) / p[6]) { atomic_fetch_or_explicit(reinterpret_cast(w), 1u, memory_order_relaxed); } + if (p[2] && ulong(a[2]) + a[0] > ((ulong(p[4]) << 32) | p[3])) { atomic_fetch_or_explicit(reinterpret_cast(w), 2u, memory_order_relaxed); } + return; + } + if (p[15] == 1) { + if (i) { return; } + // A single draw uses the phase-0 snapshot if the source changes before phase 1. + const device uint* a = s + ulong(p[18]) + ulong(p[11]) * (p[2] ? 5u : 4u); + bool enabled = !w[0] && p[11] < draws && a[0] && a[1]; + uint vertices = enabled ? a[0] : 0, instances = enabled ? a[1] * p[6] : 0; + uint first = p[2] ? a[3] : a[2], firstInstance = p[2] ? a[4] : a[3]; + uint primitives = perVertexRestartPrimitiveCount(vertices, topology), occurrences = primitives * rasterVertices; + w[4] = vertices; w[5] = instances; w[6] = first; w[7] = firstInstance; + w[8] = uint((ulong(vertices) + p[14] - 1) / p[14]); w[9] = instances; w[10] = 1; + w[12] = first; w[13] = firstInstance; w[14] = 0; w[15] = vertices; w[16] = instances; w[17] = 1; + w[20] = occurrences; w[21] = occurrences ? instances : 0; w[22] = 0; w[23] = firstInstance; + w[24] = uint((ulong(primitives) * instances + p[12] - 1) / p[12]); w[25] = 1; w[26] = 1; + w[28] = p[2] ? uint((ulong(vertices) + p[13] - 1) / p[13]) : 0; w[29] = 1; w[30] = 1; + w[32] = vertices; w[33] = instances; w[34] = primitives; w[35] = p[2] ? a[2] : 0; + w[64] = 0; w[65] = 0; w[66] = firstInstance; w[67] = occurrences; + w[128] = vertices; + if (p[16]) { + // Restart assembly with a fixed number of scan steps: surplus steps copy, keeping parity. + w[36] = uint((max(ulong(vertices), 1ul) + p[12] - 1) / p[12]); w[37] = 1; w[38] = 1; + w[40] = uint((ulong(vertices) * instances + p[12] - 1) / p[12]); w[41] = 1; w[42] = 1; + for (uint block = 0; block < p[17] + 3; ++block) { + device uint* r = w + 256 + 64 * block; + uint phase = block == 0 ? 0 : block <= p[17] ? 1 : block - p[17] + 1; + r[0] = vertices; r[1] = instances; r[2] = 4; r[3] = topology; r[4] = p[8]; r[5] = firstInstance; r[6] = p[9]; + r[7] = phase; r[8] = phase == 1 ? 1u << (block - 1) : 0; r[9] = phase == 0 ? 0 : phase == 1 ? (block - 1) & 1 : p[17] & 1; + } + } + return; + } + if (p[15] == 3) { + if (i >= w[32]) { return; } + uint index = 0; + ulong offset = (ulong(w[35]) + i) * p[2]; + for (uint b = 0; b < p[2]; ++b) { index |= uint(source[offset + b]) << (8 * b); } + // Widened indices keep their restart marker for perVertexRestart, which then reads four bytes. + if (p[16] && index == (p[2] == 1 ? 0xFFu : p[2] == 2 ? 0xFFFFu : 0xFFFFFFFFu)) { index = 0xFFFFFFFFu; } + gathered[i] = index; + return; + } + uint vertices = w[32], primitives = w[34]; + if (!primitives || i >= primitives * w[33]) { return; } + // Same records, keys and corner order as mvkPopulatePerVertexReplay(). + uint instance = i / primitives, primitive = i % primitives; + uint v[3] = {primitive * 3, primitive * 3 + 1, primitive * 3 + 2}; + if (topology == 0) { + v[0] = v[1] = v[2] = primitive; + } else if (rasterVertices == 2) { + v[0] = topology == 1 ? primitive * 2 : primitive; + v[1] = v[2] = v[0] + 1; + } else if (topology == 4) { + v[0] = primitive; v[1] = primitive + 1; v[2] = primitive + 2; + if (primitive & 1) { + uint x = p[8] ? 0 : 1, tmp = v[x]; + v[x] = v[x + 1]; v[x + 1] = tmp; + } + } else if (topology == 5) { + v[0] = p[8] ? 0 : primitive + 1; + v[1] = p[8] ? primitive + 1 : primitive + 2; + v[2] = p[8] ? primitive + 2 : 0; + } + for (uint corner = 0; corner < 3; ++corner) { + uint record = instance * vertices + v[corner]; + triplets[3 * i + corner] = record; + if (corner >= rasterVertices) { continue; } + uint occurrence = i * rasterVertices + corner; + pairs[2 * occurrence] = record; + pairs[2 * occurrence + 1] = i; + if (p[9]) { corners[occurrence] = corner; } + } +} + +// Portable PerVertexKHR TES topology for triangles with equal spacing, generated from the float32 levels +// (outer[4], inner[2] per patch) the TCS actually wrote. Vulkan discards a patch whose first three outer +// levels include one <= 0 or NaN, then clamps levels to [1, maxLevel] and rounds them up. Uniform levels 1, +// 2 and 3 use the Metal topologies proven by the Vulkan oracles; any other level refuses the whole draw. +// Levels are decoded from their bits, so NaN and sign tests survive fast math. +// p: patches, maxLevel bits, reverse corners (counter-clockwise winding), corners output, phase, patch table word. +// Plan words: 0 status, 4 replay draw, 64 replay constants (byte 256), 128 counts, then offsets; from word p[5], +// the patch index of each generated triangle, which the fragment reads as Vulkan's PrimitiveId. +uint perVertexTessLevel(uint bits, float maxLevel) { + if ((bits & 0x7fffffffu) > 0x7f800000u) { return 0; } // NaN + if ((bits & 0x80000000u) || !(bits & 0x7fffffffu)) { return 1; } // <= 0 clamps to 1 + if (bits >= 0x7f800000u) { return uint(maxLevel); } + return uint(ceil(min(max(as_type(bits), 1.0f), maxLevel))); +} +kernel void perVertexTessTopology(const device uint* levels [[buffer(0)]], device uint* plan [[buffer(1)]], device uint* invocations [[buffer(2)]], device uint* pairs [[buffer(3)]], device uint* triplets [[buffer(4)]], device uint* corners [[buffer(5)]], constant uint* p [[buffer(6)]], uint i [[thread_position_in_grid]]) { + uint patches = p[0]; + device uint* counts = plan + 128; + device uint* offsets = plan + 128 + patches; + if (p[4] == 0) { + if (i >= patches) { return; } + const device uint* f = levels + 6 * ulong(i); + bool discarded = false; + for (uint k = 0; k < 3; ++k) { discarded |= (f[k] & 0x80000000u) || !(f[k] & 0x7fffffffu) || (f[k] & 0x7fffffffu) > 0x7f800000u; } + uint triangles = 0; + if (!discarded) { + float maxLevel = as_type(p[1]); + uint n = perVertexTessLevel(f[0], maxLevel); + bool uniform = n && perVertexTessLevel(f[1], maxLevel) == n && perVertexTessLevel(f[2], maxLevel) == n && perVertexTessLevel(f[4], maxLevel) == n; + triangles = !uniform ? 0 : n == 1 ? 1 : n == 2 ? 6 : n == 3 ? 13 : 0; + if (!triangles) { atomic_fetch_or_explicit(reinterpret_cast(plan), 1u, memory_order_relaxed); } + } + counts[i] = triangles; + return; + } + if (p[4] == 1) { + if (i) { return; } + // Serial prefix over patches; very large patch counts need a parallel scan. + uint total = 0; + for (uint patch = 0; patch < patches; ++patch) { offsets[patch] = total; total += counts[patch]; } + uint records = plan[0] ? 0 : 3 * total; + invocations[0] = records; invocations[1] = 0; invocations[2] = 0; invocations[3] = 0; + plan[4] = records; plan[5] = records ? 1 : 0; plan[6] = 0; plan[7] = 0; + plan[64] = 0; plan[65] = 0; plan[66] = 0; plan[67] = records; + return; + } + if (i >= patches || plan[0] || !counts[i]) { return; } + // Clockwise (u,v) corner orders of the proven Metal topologies; counter-clockwise swaps corners 1 and 2. + // Coordinates are the M4 tessellator's, in 1/65536 steps: 1 - x is exact (Vulkan invariance rule 8), each + // edge vertex (x, 1-x) has its exact mirror (rule 3), and every edge is subdivided alike (rule 4). + const float q = 1.0f / 65536.0f; + const float3 points[12] = { + float3(1, 0, 0), float3(43691 * q, 21845 * q, 0), float3(21845 * q, 43691 * q, 0), float3(0, 1, 0), + float3(0, 43691 * q, 21845 * q), float3(0, 21845 * q, 43691 * q), float3(0, 0, 1), + float3(21845 * q, 0, 43691 * q), float3(43691 * q, 0, 21845 * q), + float3(36409 * q, 14564 * q, 14563 * q), float3(14563 * q, 36409 * q, 14564 * q), float3(14563 * q, 14563 * q, 36410 * q)}; + const uint3 level3[13] = {uint3(9, 8, 0), uint3(9, 0, 1), uint3(10, 2, 3), uint3(10, 3, 4), uint3(11, 5, 6), uint3(11, 6, 7), uint3(9, 10, 11), + uint3(9, 1, 10), uint3(10, 1, 2), uint3(10, 4, 11), uint3(11, 4, 5), uint3(11, 7, 9), uint3(9, 7, 8)}; + const float3 level2[7] = {float3(1, 0, 0), float3(0.5f, 0.5f, 0), float3(0, 1, 0), float3(0, 0.5f, 0.5f), float3(0, 0, 1), float3(0.5f, 0, 0.5f), float3(21845 * q, 21845 * q, 21846 * q)}; + uint triangles = counts[i]; + for (uint triangle = 0; triangle < triangles; ++triangle) { + uint key = offsets[i] + triangle; + plan[p[5] + key] = i; + for (uint corner = 0; corner < 3; ++corner) { + uint slot = p[2] && corner ? 3 - corner : corner; + float3 coordinate = triangles == 1 ? points[slot == 0 ? 0 : slot == 1 ? 3 : 6] : triangles == 6 ? level2[slot ? (triangle + slot - 1) % 6 : 6] : points[level3[triangle][slot]]; + uint record = 3 * key + corner; + device uint* words = invocations + 4 + 8 * ulong(record); + words[0] = as_type(coordinate.x); words[1] = as_type(coordinate.y); words[2] = as_type(coordinate.z); + words[3] = i; words[4] = record; words[5] = 0; words[6] = 0; words[7] = 0; + pairs[2 * record] = record; + pairs[2 * record + 1] = key; + triplets[3 * key + corner] = record; + if (p[3]) { corners[record] = corner; } + } + } +} +// Ordinary tessellation of triangles with equal spacing keeps the TCS levels in float32 (outer[4], inner[2] per +// patch) and classifies them as Vulkan does into the half factors Metal's tessellator reads: a patch whose first +// three outer levels include one <= 0 or NaN is discarded (all factors 0); otherwise each level is clamped to +// [1, maxLevel] and rounded up, which half represents exactly. A NaN inner level stays NaN, as half conversion +// gave it before. params: the TCS indirect parameters, patch count in word 1. +kernel void tessLevelsToHalfFactors(const device uint* levels [[buffer(0)]], device MTLTriangleTessellationFactorsHalf* factors [[buffer(1)]], constant float& maxLevel [[buffer(2)]], constant uint* params [[buffer(3)]], uint i [[thread_position_in_grid]]) { + if (i >= params[1]) { return; } + const device uint* f = levels + 6 * ulong(i); + bool discarded = false; + for (uint k = 0; k < 3; ++k) { discarded |= (f[k] & 0x80000000u) || !(f[k] & 0x7fffffffu) || (f[k] & 0x7fffffffu) > 0x7f800000u; } + for (uint k = 0; k < 3; ++k) { factors[i].edgeTessellationFactor[k] = discarded ? half(0) : half(perVertexTessLevel(f[k], maxLevel)); } + bool innerNaN = (f[4] & 0x7fffffffu) > 0x7f800000u; + factors[i].insideTessellationFactor = discarded ? half(0) : innerNaN ? as_type(ushort(0x7e00)) : half(perVertexTessLevel(f[4], maxLevel)); +} )"; diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandPool.mm b/MoltenVK/MoltenVK/Commands/MVKCommandPool.mm index 3aeed65ba..6ea359da6 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandPool.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandPool.mm @@ -106,7 +106,10 @@ {} MVKCommandPool::~MVKCommandPool() { + // Reset each command buffer while _commandEncodingPool exists: its cleanup may return Metal buffers there, and + // that member is destroyed before _commandBufferPool, which destroys the command buffers. for (auto& mvkCB : _allocatedCommandBuffers) { + mvkCB->reset(VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT); _commandBufferPool.returnObject(mvkCB); } } diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.h b/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.h index fef56feb1..f8629d3c8 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.h +++ b/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.h @@ -383,7 +383,17 @@ class MVKCommandResourceFactory : public MVKBaseDeviceObject { id newAccumulateOcclusionQueryResultsMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); /** Returns a new MTLComputePipelineState for converting a Uint8 index buffer to Uint16. */ - id newConvertUint8IndicesMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); + id newPerVertexRestartMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); + + /** Returns a new MTLComputePipelineState for planning portable PerVertexKHR indirect draws on the GPU. */ + id newPerVertexIndirectMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); + /** Returns a new MTLComputePipelineState generating portable PerVertexKHR tessellation topology. */ + id newPerVertexTessTopologyMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); + + /** Returns a new compute pipeline state that classifies float32 TCS levels into Metal half tessellation factors. */ + id newTessLevelsToHalfFactorsMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner); + + id newConvertUint8IndicesMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner, bool preserveValues = false); #pragma mark Construction diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.mm b/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.mm index 48fc98796..3af031d2a 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandResourceFactory.mm @@ -623,7 +623,24 @@ return newMTLComputePipelineState("accumulateOcclusionQueryResults", owner); } -id MVKCommandResourceFactory::newConvertUint8IndicesMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner) { +id MVKCommandResourceFactory::newPerVertexRestartMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner) { + return newMTLComputePipelineState("perVertexRestart", owner); +} + +id MVKCommandResourceFactory::newPerVertexIndirectMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner) { + return newMTLComputePipelineState("perVertexIndirect", owner); +} + +id MVKCommandResourceFactory::newPerVertexTessTopologyMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner) { + return newMTLComputePipelineState("perVertexTessTopology", owner); +} + +id MVKCommandResourceFactory::newTessLevelsToHalfFactorsMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner) { + return newMTLComputePipelineState("tessLevelsToHalfFactors", owner); +} + +id MVKCommandResourceFactory::newConvertUint8IndicesMTLComputePipelineState(MVKVulkanAPIDeviceObject* owner, bool preserveValues) { + if (preserveValues) { return newMTLComputePipelineState("convertUint8IndicesRaw", owner); } #if MVK_USE_METAL_PRIVATE_API if (getMVKConfig().useMetalPrivateAPI) { // Private API allows us to control restart index and enable. Do not convert restart sentinels. @@ -685,6 +702,10 @@ id MVKCommandResourceFactory::newMTLComputePipelineState(const char* funcName, MVKVulkanAPIDeviceObject* owner) { id mtlFunc = newFunctionNamed(funcName); // temp retain + if (!mtlFunc) { + owner->setConfigurationResult(owner->reportError(VK_ERROR_INITIALIZATION_FAILED, "Could not find support compute shader function %s.", funcName)); + return nil; + } // Providing a function directly may cause issues with Metal shader validation layer object // management for some reason, so create a temporary pipeline descriptor to provide instead. MTLComputePipelineDescriptor* plDesc = [MTLComputePipelineDescriptor new]; // temp retain diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def index 9d3b04653..a51135e95 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def +++ b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def @@ -120,6 +120,8 @@ MVK_CMD_TYPE_POOL(Draw) MVK_CMD_TYPE_POOL(DrawIndexed) MVK_CMD_TYPE_POOL(DrawIndirect) MVK_CMD_TYPE_POOL(DrawIndexedIndirect) +MVK_CMD_TYPE_POOL(DrawMeshTasks) +MVK_CMD_TYPE_POOL(DrawMeshTasksIndirect) MVK_CMD_TYPE_POOLS_FROM_THRESHOLD(CopyImage, 1) MVK_CMD_TYPE_POOLS_FROM_THRESHOLD(BlitImage, 1) MVK_CMD_TYPE_POOLS_FROM_THRESHOLD(ResolveImage, 1) diff --git a/MoltenVK/MoltenVK/Commands/MVKPerVertexCapacity.h b/MoltenVK/MoltenVK/Commands/MVKPerVertexCapacity.h new file mode 100644 index 000000000..7f46b1ecc --- /dev/null +++ b/MoltenVK/MoltenVK/Commands/MVKPerVertexCapacity.h @@ -0,0 +1,42 @@ +/* Copyright (c) 2026 Jean-Philippe Meunier. Licensed under the Apache License, Version 2.0. */ +#pragma once +#include +#include +#include +#include +#include + +// PerVertexKHR leaves read the capture buffer; only ordinary active inputs need raster varyings. +template +static inline std::vector mvkPerVertexReplayUserLocations(const Inputs& inputs) { + std::vector locations; + for (const auto& input : inputs) { if (input.isUsed && !input.perVertex && input.builtin == spv::BuiltInMax) { locations.push_back(input.location); } } + std::sort(locations.begin(), locations.end()); + locations.erase(std::unique(locations.begin(), locations.end()), locations.end()); + return locations; +} + +// Component decorations can make Metal pad a location up to its highest occupied lane. +// Selection is by whole Location, exactly as in compile_captured_output_replay(). +static inline uint64_t mvkPerVertexReplayVaryingComponents(const SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout& layout, bool perspective, bool noPerspective, const std::vector& userLocations) { + std::map widths; + for (uint32_t location : userLocations) { widths.emplace(location, 0); } + for (const auto& field : layout.components) { + auto width = widths.find(field.location); + if (width != widths.end()) { width->second = std::max(width->second, field.component + 1); } + } + uint64_t count = 1 + 3 * uint32_t(perspective) + 3 * uint32_t(noPerspective); + for (const auto& width : widths) { count += width.second; } + // Position has a dedicated rasterizer output. Conservatively charge other captured builtins. + for (const auto& field : layout.builtins) { if (field.builtin != spv::BuiltInPosition) { ++count; } } + return count; +} + +// Retain the all-output budget for callers using the original replay overload. +static inline uint64_t mvkPerVertexReplayVaryingComponents(const SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout& layout, bool perspective, bool noPerspective) { + std::vector locations; + for (const auto& field : layout.components) { locations.push_back(field.location); } + return mvkPerVertexReplayVaryingComponents(layout, perspective, noPerspective, locations); +} + +static inline bool mvkPerVertexBufferIndexAvailable(uint32_t index, uint32_t descriptorCount, uint32_t bufferCount) { return index >= descriptorCount && index < bufferCount; } diff --git a/MoltenVK/MoltenVK/Commands/MVKPerVertexReplay.h b/MoltenVK/MoltenVK/Commands/MVKPerVertexReplay.h new file mode 100644 index 000000000..61cbbefe9 --- /dev/null +++ b/MoltenVK/MoltenVK/Commands/MVKPerVertexReplay.h @@ -0,0 +1,139 @@ +/* Copyright (c) 2026 Jean-Philippe Meunier. Licensed under the Apache License, Version 2.0. */ +#pragma once +#include +#include +#include +#include + +// Vulkan ignores restart for nonindexed draws; indexed restart needs GPU primitive assembly. +static inline bool mvkPerVertexRequiresRestartAssembly(bool indexed, bool primitiveRestart) { return indexed && primitiveRestart; } + +// Dense capture record IDs are uint32. Buffer sizes are checked separately before reservation. +static inline bool mvkCanAssemblePerVertexRestart(uint32_t count, uint32_t instances, bool indirectDrawing) { return indirectDrawing && uint64_t(count) * instances <= UINT32_MAX; } + +// Compact UINT32 indices followed by two ping-pong scans of four uints per input index. +static inline uint64_t mvkPerVertexRestartIndexScratchSize(uint32_t count) { return uint64_t(count) * 9 * sizeof(uint32_t); } + +// Indirect restart encodes the scan steps of the largest admissible count; surplus steps only copy. +static inline uint32_t mvkPerVertexIndirectRestartScanSteps(uint32_t capacity) { + uint32_t steps = 0; + for (uint64_t step = 1; step < capacity; step *= 2) { ++steps; } + return steps; +} + +// Zero also identifies topologies outside the portable capture/replay contract. +static inline uint32_t mvkPerVertexReplayVertexCount(VkPrimitiveTopology topology) { + switch (topology) { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return 1; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: return 2; + case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: return 3; + default: return 0; + } +} + +static inline uint32_t mvkPerVertexPrimitiveCount(uint32_t vertexCount, VkPrimitiveTopology topology) { + switch (topology) { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return vertexCount; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: return vertexCount / 2; + case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: return vertexCount > 1 ? vertexCount - 1 : 0; + case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: return vertexCount / 3; + case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: return vertexCount > 2 ? vertexCount - 2 : 0; + default: return 0; + } +} + +// Each callback is a separate dispatch followed by a buffer barrier. No invocation scans a draw. +template +static inline void mvkDispatchPerVertexRestart(uint32_t count, uint32_t instances, VkPrimitiveTopology topology, Dispatch&& dispatch) { + dispatch(0, 0, 0, std::max(count, 1u)); + if (!count) { return; } + uint32_t source = 0; + for (uint64_t step = 1; step < count; step *= 2) { + dispatch(1, uint32_t(step), source, count); + source ^= 1; + } + dispatch(2, 0, source, count); + uint64_t primitives = uint64_t(mvkPerVertexPrimitiveCount(count, topology)) * instances; + if (primitives) { dispatch(3, 0, source, primitives); } +} + +static inline uint32_t mvkPerVertexIndexSize(VkIndexType type) { + switch (type) { + case VK_INDEX_TYPE_UINT8: return 1; + case VK_INDEX_TYPE_UINT16: return 2; + case VK_INDEX_TYPE_UINT32: return 4; + default: return 0; + } +} + +// Validate both the Vulkan binding and its underlying Metal allocation without overflowing. +static inline bool mvkPerVertexIndexRange(uint32_t count, uint32_t firstIndex, VkIndexType type, uint64_t bindingSize, uint64_t bindingOffset, uint64_t bufferSize) { + uint32_t size = mvkPerVertexIndexSize(type); + uint64_t start = uint64_t(firstIndex) * size, bytes = uint64_t(count) * size; + return size && start <= bindingSize && bytes <= bindingSize - start && bindingOffset <= bufferSize && start <= bufferSize - bindingOffset && bytes <= bufferSize - bindingOffset - start; +} + +// Caller checks counts and buffer bounds with mvkCanEncodePerVertexDraw before allocating. +// Records are dense (instance, input occurrence), including for indexed capture on the GPU. +// PerVertexKHR always uses three records; point/line raster replay uses only one/two vertices. +static inline void mvkPopulatePerVertexReplay(uint32_t vertexCount, uint32_t instanceCount, VkPrimitiveTopology topology, bool provokingLast, uint32_t* pairs, uint32_t* indices, uint32_t* corners) { + uint32_t primitiveCount = mvkPerVertexPrimitiveCount(vertexCount, topology); + uint32_t replayVertices = mvkPerVertexReplayVertexCount(topology); + for (uint32_t instance = 0; instance < instanceCount; instance++) { + for (uint32_t primitive = 0; primitive < primitiveCount; primitive++) { + uint32_t key = instance * primitiveCount + primitive; + uint32_t vertices[] = {primitive * 3, primitive * 3 + 1, primitive * 3 + 2}; + // Vulkan primsrast-barycentric-order-table and -last-vertex. MoltenVK + // advertises triStripVertexOrderIndependentOfProvokingVertex = false. + // These orders preserve winding and the replay primitive's provoking vertex. + if (topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST) { + vertices[0] = vertices[1] = vertices[2] = primitive; + } else if (replayVertices == 2) { + vertices[0] = topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST ? primitive * 2 : primitive; + vertices[1] = vertices[2] = vertices[0] + 1; + } else if (topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + vertices[0] = primitive; vertices[1] = primitive + 1; vertices[2] = primitive + 2; + if (primitive & 1) { std::swap(vertices[provokingLast ? 0 : 1], vertices[provokingLast ? 1 : 2]); } + } else if (topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN) { + vertices[0] = provokingLast ? 0 : primitive + 1; + vertices[1] = provokingLast ? primitive + 1 : primitive + 2; + vertices[2] = provokingLast ? primitive + 2 : 0; + } + for (uint32_t vertex = 0; vertex < 3; vertex++) { + uint32_t record = instance * vertexCount + vertices[vertex]; + indices[3 * key + vertex] = record; + if (vertex >= replayVertices) { continue; } + uint32_t occurrence = key * replayVertices + vertex; + pairs[2 * occurrence] = record; + pairs[2 * occurrence + 1] = key; + if (corners) { corners[occurrence] = vertex; } + } + } + } +} + +// Without GPU restart assembly, the replay tables of a single-instance draw do not depend on its vertex count: the +// tables populated for a larger count begin with those of every smaller count. Instanced list draws without dangling +// vertices are the single-instance draw of vertexCount * instanceCount vertices. One table per topology and provoking +// mode then serves all these draws; the caller requests vertexCount * instanceCount vertices. +static inline bool mvkPerVertexReplayIsSizeIndependent(bool restart, VkPrimitiveTopology topology, uint32_t vertexCount, uint32_t instanceCount) { + if (restart || uint64_t(vertexCount) * instanceCount > UINT32_MAX) { return false; } + if (instanceCount == 1) { return true; } + bool list = topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + return list && vertexCount % mvkPerVertexReplayVertexCount(topology) == 0; +} + +// Shared tables grow in powers of two, so each is rebuilt O(log n) times. +static inline uint32_t mvkPerVertexSharedReplayVertexCount(uint32_t vertexCount) { + uint64_t capacity = 1024; + while (capacity < vertexCount) { capacity *= 2; } + return uint32_t(std::min(capacity, UINT32_MAX)); +} + +// Reserve whole locations: a private float3 must not share an application's Component lanes. +static inline uint32_t mvkAllocatePerVertexVaryingLocation(std::unordered_set& locations, uint32_t maxLocations) { + for (uint32_t location = 0; location < maxLocations; ++location) { + if (locations.insert(location).second) { return location; } + } + return ~0u; +} diff --git a/MoltenVK/MoltenVK/Commands/MVKPerVertexScratch.h b/MoltenVK/MoltenVK/Commands/MVKPerVertexScratch.h new file mode 100644 index 000000000..46c5d10ea --- /dev/null +++ b/MoltenVK/MoltenVK/Commands/MVKPerVertexScratch.h @@ -0,0 +1,223 @@ +/* Copyright (c) 2026 Jean-Philippe Meunier. Licensed under the Apache License, Version 2.0. */ +#pragma once + +#import +#include "MVKPerVertexReplay.h" +#include +#include +#include +#include +#include +#include + +struct MVKPerVertexScratchRequest { + NSUInteger captureSize; + NSUInteger occurrenceSize; + NSUInteger primitiveSize; + NSUInteger cornerSize = 0; + NSUInteger indexSize = 0; + bool restart = false; + bool widenUint8 = false; + // Indirect draws: record ceiling of the GPU plan, and the shared plan/status buffer. The replay scratch is + // reserved only after the plan completes, from the counts it froze (mvkSizePerVertexIndirectReplay). + uint32_t indirectCapacity = 0; + NSUInteger planSize = 0; + // Several indirect draws freeze their Vulkan commands in a separate shared buffer: 16 bytes each, 20 when indexed. + NSUInteger snapshotSize = 0; + // TES invocation ABI, VS output, TCS vertex and patch output, float32 levels, topology plan. + NSUInteger tessSizes[6] = {}; +}; + +// Read only after successful GPU phase-0 completion. The snapshot holds the Vulkan command each draw froze: four words, +// five for indexed draws; a single draw freezes it in the plan, at word 48. +// A nonzero GPU status is rejected, including the old capacity refusal; this does not change admission. +// Metadata must describe that same Metal pass (1..32 views), pipeline and execution-time restart state. +// Restart tables are bounds from frozen counts, not compacted counts: the plan has no marker census. +// On success, captureSize == 0 means skip replay entirely (do not call reserveReplay). Otherwise the +// request includes minimal table bindings even for zero primitives. Only replay fields are populated; +// retain the original plan/offset. Outputs are unchanged on failure. The supplied extent must be readable. +static inline bool mvkSizePerVertexIndirectReplay(const void* plan, NSUInteger planBytes, const void* snapshot, NSUInteger snapshotBytes, uint32_t maxDrawCount, uint32_t views, uint32_t stride, VkPrimitiveTopology topology, bool indexed, bool primitiveRestart, bool corners, uint64_t maxMTLBufferSize, MVKPerVertexScratchRequest& request, NSUInteger& gatheredOffset, uint32_t& scanSteps) { + uint32_t rasterVertices = mvkPerVertexReplayVertexCount(topology); + if (!plan || planBytes < 57 * sizeof(uint32_t) || !views || views > 32 || !rasterVertices) { return false; } + auto word = [&](NSUInteger offset) { uint32_t value; memcpy(&value, static_cast(plan) + offset, sizeof(value)); return value; }; + uint32_t draws = word(56 * sizeof(uint32_t)); + NSUInteger commandBytes = (indexed ? 5 : 4) * sizeof(uint32_t); + if (word(0) || draws > maxDrawCount || !snapshot || draws > snapshotBytes / commandBytes) { return false; } + auto frozen = [&](NSUInteger offset) { uint32_t value; memcpy(&value, static_cast(snapshot) + offset, sizeof(value)); return value; }; + if (maxDrawCount == 1 && draws && word(55 * sizeof(uint32_t)) != 1) { return false; } + uint64_t records = 0, primitives = 0; + uint32_t vertices = 0; + for (uint32_t draw = 0; draw < draws; ++draw) { + NSUInteger offset = NSUInteger(draw) * commandBytes; + uint32_t n = frozen(offset), instances = frozen(offset + sizeof(uint32_t)); + if (!n || !instances) { continue; } + uint64_t expanded = uint64_t(instances) * views; + if (!stride || expanded > UINT32_MAX) { return false; } + uint64_t r = uint64_t(n) * expanded; + if (r > UINT32_MAX) { return false; } + uint64_t p = uint64_t(mvkPerVertexPrimitiveCount(n, topology)) * expanded; + // Same uint32 table-index invariants as mvkCanEncodePerVertexDraw; counts are now bounded. + if (p > UINT32_MAX / 3 || p * rasterVertices > UINT32_MAX / 2) { return false; } + records = std::max(records, r); + primitives = std::max(primitives, p); + vertices = std::max(vertices, n); + } + MVKPerVertexScratchRequest sized{}; + NSUInteger gathered = 0; + uint32_t scans = 0; + if (records) { + bool restart = mvkPerVertexRequiresRestartAssembly(indexed, primitiveRestart); + primitives = std::max(primitives, 1); // Valid bindings for active draws with no primitives. + uint64_t occurrences = primitives * rasterVertices; + uint64_t prefix = restart ? mvkPerVertexRestartIndexScratchSize(vertices) : 0; + // All products fit uint64: records/stride are uint32, tables were bounded above, indices <= 40*UINT32_MAX. + uint64_t lengths[] = {records * stride, occurrences * 8, primitives * 12, corners ? occurrences * 4 : 0, indexed ? prefix + uint64_t(vertices) * 4 : 0, restart ? 68 * sizeof(uint32_t) : sizeof(uint32_t)}; + uint64_t limit = std::min(maxMTLBufferSize, NSUIntegerMax); + for (uint64_t length : lengths) { if (length > limit) { return false; } } + sized = {NSUInteger(lengths[0]), NSUInteger(lengths[1]), NSUInteger(lengths[2]), NSUInteger(lengths[3]), NSUInteger(lengths[4]), restart}; + gathered = NSUInteger(prefix); + scans = restart ? mvkPerVertexIndirectRestartScanSteps(vertices) : 0; + } + request = sized; + gatheredOffset = gathered; + scanSteps = scans; + return true; +} + +static inline id mvkNewPerVertexScratchBuffer(id device, NSUInteger length, bool shared) { + return [device newBufferWithLength:length options:shared ? MTLResourceStorageModeShared : MTLResourceStorageModePrivate]; +} + +// MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1: the scratch buffers of direct draws come from free lists by storage mode +// and power-of-two size class, instead of a new Metal buffer per draw. A buffer returns to its list only when its +// MVKPerVertexScratch is destroyed, at the release of its last reference: the completed handler that +// keepPerVertexScratchResident adds to every Metal command buffer encoding it, or the submission, destroyed once all +// its Metal command buffers completed. Pool buffers enter the residency set once, at creation, and stay there: the +// set does not count references, so removing one after a draw would evict it while a later draw reuses it. +// ponytail: the pool keeps its high-water mark until it is destroyed; trim the free lists if memory matters. +struct MVKPerVertexScratchPool { + static constexpr uint8_t kMinSizeClass = 8; // 256 bytes + static uint8_t sizeClass(NSUInteger length) { + uint8_t sizeClass = kMinSizeClass; + while (sizeClass < 63 && (NSUInteger(1) << sizeClass) < length) { ++sizeClass; } + return sizeClass; + } + + MVKPerVertexScratchPool(NSUInteger maxLength, std::function)> makeResident) : _maxLength(maxLength), _makeResident(std::move(makeResident)) {} + MVKPerVertexScratchPool(const MVKPerVertexScratchPool&) = delete; + MVKPerVertexScratchPool& operator=(const MVKPerVertexScratchPool&) = delete; + ~MVKPerVertexScratchPool() { for (auto& lists : _free) { for (auto& list : lists) { for (id buffer : list) { [buffer release]; } } } } + + // Returns a retained buffer of at least length bytes (at most maxLength), and its class for give(). + id take(id device, NSUInteger length, bool shared, uint8_t& sizeClassOut) { + uint8_t c = sizeClassOut = sizeClass(length); + { + std::lock_guard lock(_lock); + auto& list = _free[shared][c]; + if ( !list.empty() ) { + id buffer = list.back(); + list.pop_back(); + return buffer; + } + } + // Every request of this class is at most min(2^c, maxLength) bytes: a power of two above the limit would fail. + id buffer = mvkNewPerVertexScratchBuffer(device, std::min(NSUInteger(1) << c, _maxLength), shared); + if (buffer && _makeResident) { _makeResident(buffer); } + return buffer; + } + + // Only once no Metal command buffer can use the buffer any more. Runs on completion threads. + void give(id buffer, bool shared, uint8_t c) { + std::lock_guard lock(_lock); + try { _free[shared][c].push_back(buffer); } catch (const std::bad_alloc&) { [buffer release]; } + } + +private: + std::mutex _lock; + std::vector> _free[2][64]; + NSUInteger _maxLength; + std::function)> _makeResident; +}; + +// Individually owned allocations: the shared scratch pool cannot recover from a nil Metal buffer. +struct MVKPerVertexScratch { + id buffers[14] = {}; + id indexPipeline = nil; + id widenUint8Pipeline = nil; + id indirectPipeline = nil; + uint32_t indirectCapacity = 0; + id tessTopologyPipeline = nil; + // Direct draws with MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1: the pool of the replay buffers, and their size classes. + std::shared_ptr pool; + uint8_t poolSizeClasses[14] = {}; + MVKPerVertexScratch() = default; + MVKPerVertexScratch(const MVKPerVertexScratch&) = delete; + MVKPerVertexScratch& operator=(const MVKPerVertexScratch&) = delete; + ~MVKPerVertexScratch() { + for (size_t i = 0; i < std::size(buffers); ++i) { + if (poolSizeClasses[i]) { pool->give(buffers[i], isSharedReplayBuffer(i), poolSizeClasses[i]); } else { [buffers[i] release]; } + } + [indexPipeline release]; [widenUint8Pipeline release]; [indirectPipeline release]; [tessTopologyPipeline release]; + } + + static bool isSharedReplayBuffer(size_t i) { return (i > 0 && i < 5) || i == 7; } + + bool reserveReplay(id device, const MVKPerVertexScratchRequest& request) { + NSUInteger lengths[] = {request.captureSize, sizeof(uint32_t), request.occurrenceSize, request.primitiveSize, request.cornerSize, request.indexSize, request.restart ? 68 * sizeof(uint32_t) : 0, request.tessSizes[0], request.tessSizes[1], request.tessSizes[2], request.tessSizes[3], request.tessSizes[4]}; + for (size_t i = 0; i < std::size(lengths); ++i) { + if (!lengths[i]) { continue; } + if (buffers[i]) { return false; } + bool shared = isSharedReplayBuffer(i); + id buffer = pool ? pool->take(device, lengths[i], shared, poolSizeClasses[i]) : mvkNewPerVertexScratchBuffer(device, lengths[i], shared); + if (!buffer || buffer.length < lengths[i] || (shared && !buffer.contents)) { [buffer release]; poolSizeClasses[i] = 0; return false; } + buffers[i] = buffer; + } + return true; + } + + bool reservePlan(id device, const MVKPerVertexScratchRequest& request) { + // Indirect dispatch/draw arguments and replay constants share a 256-byte-aligned plan. + NSUInteger length = std::max(request.planSize, request.tessSizes[5]); + if (length) { + if (buffers[12]) { return false; } + id buffer = mvkNewPerVertexScratchBuffer(device, length, true); + if (!buffer || buffer.length < length || !buffer.contents) { [buffer release]; return false; } + buffers[12] = buffer; + // The GPU ORs admission failures into the status word; each submission starts admitted. + memset(buffer.contents, 0, buffer.length); + } + if (request.snapshotSize) { + if (buffers[13]) { return false; } + id snapshot = mvkNewPerVertexScratchBuffer(device, request.snapshotSize, true); + if (!snapshot || snapshot.length < request.snapshotSize || !snapshot.contents) { [snapshot release]; return false; } + buffers[13] = snapshot; + } + indirectCapacity = request.indirectCapacity; + return true; + } + + bool reserve(id device, const MVKPerVertexScratchRequest& request) { return reserveReplay(device, request) && reservePlan(device, request); } + + // Admission failures ORed by the GPU into the plan buffer; read only after the work completed. + uint32_t getIndirectStatus() const { return indirectCapacity && buffers[12] ? *(volatile const uint32_t*)buffers[12].contents : 0; } + + // Nonzero when the GPU refused a TES draw whose levels have no proven topology. Read once the encoder waited. + uint32_t getTessEvalStatus() const { return !indirectCapacity && buffers[12] ? *(volatile const uint32_t*)buffers[12].contents : 0; } +}; + +using MVKPerVertexScratchReservations = std::vector>; + +// Publish only a complete reservation. Failure releases all allocations made by this attempt, or returns them to the pool. +// Indirect draws (sized while encoding, read back by length) and TES draws (test-only) never use the pool. +static inline bool mvkReservePerVertexScratch(id device, const std::vector& requests, MVKPerVertexScratchReservations& reservations, const std::shared_ptr& pool = nullptr) { + MVKPerVertexScratchReservations pending; + pending.reserve(requests.size()); + for (const auto& request : requests) { + auto scratch = std::make_shared(); + if (!request.indirectCapacity && std::none_of(std::begin(request.tessSizes), std::end(request.tessSizes), [](NSUInteger size) { return size != 0; })) { scratch->pool = pool; } + if (request.indirectCapacity ? !scratch->reservePlan(device, request) : !scratch->reserve(device, request)) { return false; } + pending.push_back(std::move(scratch)); + } + reservations = std::move(pending); + return true; +} diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h index 534a414f6..8f3357f1d 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h @@ -27,9 +27,12 @@ #include "MVKPixelFormats.h" #include "MVKOSExtensions.h" #include "mvk_datatypes.hpp" +#include +#include #include #include #include +#include #include #import @@ -51,6 +54,7 @@ class MVKSwapchain; class MVKDeviceMemory; class MVKFence; class MVKSemaphore; +class MVKLossReleasable; class MVKTimelineSemaphore; class MVKDeferredOperation; class MVKEvent; @@ -72,6 +76,7 @@ class MVKCommandEncoder; class MVKCommandResourceFactory; class MVKPrivateDataSlot; struct MVKUseResourceHelper; +struct MVKPerVertexScratchPool; enum class MVKResourceUsageStages : uint8_t; @@ -426,6 +431,12 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject { /** Returns whether argument buffers are in use. */ bool isUsingMetalArgumentBuffers() const { return _isUsingMetalArgumentBuffers; } + /** + * Returns whether the experimental portable PerVertexKHR capture and replay is enabled. Only the test entry point + * enables it. Otherwise barycentric inputs keep the native Metal path, and PerVertexKHR inputs are not supported. + */ + bool isPortablePerVertexEnabled() const { return _portablePerVertexEnabled; } + /** Returns whether or not vertex instancing can be used to implement multiview. */ bool canUseInstancingForMultiview() { return _metalFeatures.layeredRendering; } @@ -441,6 +452,13 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject { /** Returns info on the sizes of argument buffers. */ const MVKPhysicalDeviceArgumentBufferSizes& getArgumentBufferSizes() const { return _argumentBufferSizes; } + /** + * Returns a Metal command queue that only releases semaphore waits after a logical device is lost, + * so that its command buffers never queue behind stalled application work. Created on first use, + * then shared by the logical devices of this physical device. Returns nil if it cannot be created. + */ + id getLossRescueMTLCommandQueue(); + #pragma mark Construction @@ -503,6 +521,7 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject { id _mtlDevice; const MVKMTLDeviceCapabilities _gpuCapabilities; const MVKExtensionList _supportedExtensions; + bool _portablePerVertexEnabled = false; MVKPixelFormats _pixelFormats; VkPhysicalDeviceFeatures _features; MVKPhysicalDeviceVulkan12NoExtFeatures _vulkan12NoExtFeatures; @@ -527,6 +546,8 @@ class MVKPhysicalDevice : public MVKDispatchableVulkanAPIObject { uint32_t _privateMemoryTypes; uint32_t _lazilyAllocatedMemoryTypes; MVKPhysicalDeviceArgumentBufferSizes _argumentBufferSizes; + id _lossRescueMTLQueue = nil; + std::mutex _lossRescueLock; bool _hasUnifiedMemory = true; bool _isUsingMetalArgumentBuffers = true; }; @@ -690,9 +711,88 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { /** Block the current thread until all queues in this device are idle. */ VkResult waitIdle(); - /** Mark this device (and optionally the physical device) as lost. Releases all waits for this device. */ + /** + * Marks this device (and optionally the physical device) as lost, without blocking the caller. + * New submissions are refused at once, CPU waits made while encoding submissions are cancelled, + * and Metal waits on this device's semaphores are released. Host waits report the loss only once + * no queue submission is encoding (see beginEncoding()), because a loss reported to a host wait + * allows the application to free the command buffers such a submission may still be reading. + */ VkResult markLost(bool alsoMarkPhysicalDevice = false); + /** + * Returns whether this device has started to be lost, even if host waits do not report it yet. + * + * Sequentially consistent: an encoding admits itself by incrementing the count of active encodings, then reading + * this, while the loss is set, then that count is read. One of the two reads must see the other write. An acquire + * load may be served before the preceding increment is visible (LDAPR on arm64), and both could then miss. + */ + bool isLosing() { return _losing.load(); } + + /** + * Returns the result of a host wait that returned with the specified completion. + * During a loss not yet reported to host waits, waits up to the timeout for it to be reported. + */ + VkResult getHostWaitResult(bool isFinished, uint64_t timeout); + + /** + * Brackets the encoding of a queue submission, from its dispatch to a queue worker if it has one. A device loss + * is reported to host waits, and this device destroyed, only while no submission is encoding. endEncoding() is + * the caller's last use of this device and of its queue. + */ + void beginEncoding(); + void endEncoding(); + + /** + * Adds a completion handler, or a scheduled handler, to a Metal command buffer that commitMTLCommandBuffer() + * commits, or that abandonMTLCommandBuffer() abandons. The handlers of each kind run in the order they were added, + * from one Metal handler: Metal documents no order between the handlers of a command buffer. A device loss is + * reported to host waits, and this device destroyed, only once the handlers of every committed command buffer have + * returned: they use this device and objects that the application may destroy as soon as a loss is reported. + */ + void addMTLCommandBufferHandler(id mtlCmdBuff, MTLCommandBufferHandler handler, bool isScheduled = false); + + /** + * MVK_CONFIG_PERVERTEX_BATCH_RESIDENCY=1: adds an allocation that a Metal command buffer uses to the residency + * set when commitMTLCommandBuffer() commits it, together with the others of that command buffer and one residency + * commit, and removes them all, with one commit, from its completed handlers. An abandoned one never adds them. + */ + void addMTLCommandBufferResidency(id mtlCmdBuff, id allocation); + + /** MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1: the pool of PerVertex scratch buffers, otherwise null. */ + const std::shared_ptr& getPerVertexScratchPool() { return _perVertexScratchPool; } + + /** + * Runs now, as cleanup, the completion handlers of a Metal command buffer that its owner then releases without + * committing it, which discards its commands. Its status stays MTLCommandBufferStatusNotEnqueued, which handlers + * that report completion must check. + */ + void abandonMTLCommandBuffer(id mtlCmdBuff); + + /** + * Commits a Metal command buffer, counting its handlers added by addMTLCommandBufferHandler(). A device loss is + * then reported only once it has completed, since MoltenVK does not retain the application resources it uses. + */ + void commitMTLCommandBuffer(id mtlCmdBuff); + + /** + * While this device is being lost, waits until no Metal work committed before the loss inventory closed can still + * write application memory. Used before releasing host memory that the application imported, which it may reuse. + */ + void waitForLossInventory(); + + /** Registers a semaphore or event whose encoded Metal waits must be released if this device is lost. */ + void addLossReleasable(MVKLossReleasable* releasable); + + /** Unregisters an object registered with addLossReleasable(). */ + void removeLossReleasable(MVKLossReleasable* releasable); + + /** Adds a CPU wait made while encoding a submission, cancelled as soon as this device starts to be lost. */ + void addEncodingSemaphore(MVKSemaphoreImpl* sem4); + + /** Removes a CPU wait added with addEncodingSemaphore(). */ + void removeEncodingSemaphore(MVKSemaphoreImpl* sem4); + /** Returns whether or not the given descriptor set layout is supported. */ void getDescriptorSetLayoutSupport(const VkDescriptorSetLayoutCreateInfo* pCreateInfo, VkDescriptorSetLayoutSupport* pSupport); @@ -979,6 +1079,7 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { #if !MVK_XCODE_16 void makeResident(id allocation) {} + void makeResidentBatch(NSArray* allocations) {} #else void makeResident(id allocation) { @synchronized(_residencySet) { @@ -986,10 +1087,19 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { [_residencySet commit]; } } + + /** Adds the allocations to the residency set with a single commit. */ + void makeResidentBatch(NSArray* allocations) { + @synchronized(_residencySet) { + for (id allocation in allocations) { [_residencySet addAllocation: allocation]; } + [_residencySet commit]; + } + } #endif #if !MVK_XCODE_16 void removeResidency(id allocation) {} + void removeResidencyBatch(NSArray* allocations) {} #else void removeResidency(id allocation) { @synchronized(_residencySet) { @@ -997,6 +1107,14 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { [_residencySet commit]; } } + + /** Removes the allocations from the residency set with a single commit. */ + void removeResidencyBatch(NSArray* allocations) { + @synchronized(_residencySet) { + for (id allocation in allocations) { [_residencySet removeAllocation: allocation]; } + [_residencySet commit]; + } + } #endif void addResidencySet(id queue) { @@ -1061,6 +1179,8 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { void initPerformanceTracking(); void initPhysicalDevice(MVKPhysicalDevice* physicalDevice, const VkDeviceCreateInfo* pCreateInfo); void initQueues(const VkDeviceCreateInfo* pCreateInfo); + void releaseSemaphoreWaitsAfterLoss(); + void publishLoss(); void initConfiguration(); void reservePrivateData(const VkDeviceCreateInfo* pCreateInfo); void enableFeatures(const VkDeviceCreateInfo* pCreateInfo); @@ -1101,7 +1221,20 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { MVKSmallVector _privateDataSlots; MVKSmallVector _privateDataSlotsAvailability; MVKSmallVector _awaitingSemaphores; + MVKSmallVector _encodingSemaphores; MVKSmallVector> _awaitingTimelineSem4s; + std::unordered_set _lossReleasables; + std::mutex _lossReleaseLock; + std::mutex _lossLock; + std::condition_variable _lossCondition; + dispatch_queue_t _lossPublisher = nullptr; + std::atomic _activeEncodings{0}; + std::atomic _pendingMTLCommandBufferHandlers{0}; + std::atomic _handlersBeforeInventory{0}; + std::atomic _isLossInventoryClosed{false}; + bool _isLossPublished = false; + std::atomic _losing{false}; + std::atomic _markPhysicalDeviceLost{false}; MVKSmallVector _visibilityBuffers; MVKLiveResourceSet _liveResources; std::mutex _rezLock; @@ -1114,6 +1247,7 @@ class MVKDevice : public MVKDispatchableVulkanAPIObject { #if MVK_XCODE_16 id _residencySet = nil; #endif + std::shared_ptr _perVertexScratchPool; uint32_t _visibilityBufferCount = 0; int _capturePipeFileDesc = -1; bool _isPerformanceTracking = false; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm index 359f8a2b2..2a5a6b19f 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm @@ -30,14 +30,17 @@ #include "MVKRenderPass.h" #include "MVKSync.h" #include "MVKCommandPool.h" +#include "MVKPerVertexScratch.h" #include "MVKFoundation.h" #include "MVKStrings.h" #include #import "CAMetalLayer+MoltenVK.h" +#include #include #include +#include using namespace std; @@ -574,7 +577,8 @@ } case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR: { auto* barycentricFeatures = (VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR*)next; - barycentricFeatures->fragmentShaderBarycentric = true; + // KHR and NV are aliases; report the same support gate used by extension enumeration. + barycentricFeatures->fragmentShaderBarycentric = _supportedExtensions.vk_KHR_fragment_shader_barycentric.enabled || _supportedExtensions.vk_NV_fragment_shader_barycentric.enabled; break; } case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_7_FEATURES_KHR: { @@ -752,6 +756,16 @@ legacyDitheringFeatures->legacyDithering = getMVKConfig().useMetalPrivateAPI; break; } + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT: { + auto* meshFeatures = (VkPhysicalDeviceMeshShaderFeaturesEXT*)next; + // The first mesh path draws mesh and fragment stages only, and is exposed to the test adapter only. + meshFeatures->taskShader = false; + meshFeatures->meshShader = _supportedExtensions.vk_EXT_mesh_shader.enabled; + meshFeatures->multiviewMeshShader = false; + meshFeatures->primitiveFragmentShadingRateMeshShader = false; + meshFeatures->meshShaderQueries = false; + break; + } case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_FEATURES_EXT: { auto* multiDrawFeatures = (VkPhysicalDeviceMultiDrawFeaturesEXT*)next; multiDrawFeatures->multiDraw = true; @@ -1311,6 +1325,43 @@ nestedCmdBuffProps->maxCommandBufferNestingLevel = std::numeric_limits::max(); break; } + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT: { + auto* meshProps = (VkPhysicalDeviceMeshShaderPropertiesEXT*)next; + // Vulkan minimum limits, except where Metal is measured and documented lower (MeshLimits probe, Metal Feature + // Set Tables): those two fall below Vulkan's minimums, so this path cannot be exposed as the extension as is. + // Task shaders are not supported, so their limits are zero. + meshProps->maxTaskWorkGroupTotalCount = 0; + mvkClear(meshProps->maxTaskWorkGroupCount, 3); + meshProps->maxTaskWorkGroupInvocations = 0; + mvkClear(meshProps->maxTaskWorkGroupSize, 3); + meshProps->maxTaskPayloadSize = 0; + meshProps->maxTaskSharedMemorySize = 0; + meshProps->maxTaskPayloadAndSharedMemorySize = 0; + // "Maximum threadgroups per mesh shader grid": 1024 on Apple7 and Apple8, 1,048,575 on Apple9, 4,194,303 on + // Apple10. Each pipeline also reports its own limit, which vkCmdDrawMeshTasksEXT() enforces. + meshProps->maxMeshWorkGroupTotalCount = _gpuCapabilities.supportsApple10 ? 4194303 : _gpuCapabilities.supportsApple9 ? 1048575 : 1024; + for (uint32_t i = 0; i < 3; i++) { meshProps->maxMeshWorkGroupCount[i] = 65535; meshProps->maxMeshWorkGroupSize[i] = 128; } + meshProps->maxMeshWorkGroupInvocations = 128; + meshProps->maxMeshSharedMemorySize = 28 * KIBI; + meshProps->maxMeshPayloadAndSharedMemorySize = 28 * KIBI; + meshProps->maxMeshOutputMemorySize = 32 * KIBI; + meshProps->maxMeshPayloadAndOutputMemorySize = 47 * KIBI; + // Metal counts at most 124 unique scalars per mesh vertex and primitive, the position included. + meshProps->maxMeshOutputComponents = 120; + meshProps->maxMeshOutputVertices = 256; + meshProps->maxMeshOutputPrimitives = 256; + meshProps->maxMeshOutputLayers = 8; + meshProps->maxMeshMultiviewViewCount = 1; + meshProps->meshOutputPerVertexGranularity = 1; + meshProps->meshOutputPerPrimitiveGranularity = 1; + meshProps->maxPreferredTaskWorkGroupInvocations = 0; + meshProps->maxPreferredMeshWorkGroupInvocations = 32; + meshProps->prefersLocalInvocationVertexOutput = true; + meshProps->prefersLocalInvocationPrimitiveOutput = true; + meshProps->prefersCompactVertexOutput = false; + meshProps->prefersCompactPrimitiveOutput = false; + break; + } case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_PROPERTIES_EXT: { auto* multiDrawProps = (VkPhysicalDeviceMultiDrawPropertiesEXT*)next; // Each draw is encoded as a separate command, so no limit is imposed. @@ -3313,6 +3364,8 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope // might affect the contents of the pipeline cache (mostly MSL content). uint32_t mtlFeatures = 0; mtlFeatures |= _isUsingMetalArgumentBuffers << 0; + mtlFeatures |= 1u << 1; // Shader cache archive includes the portable PerVertex input ABI. + mtlFeatures |= 1u << 2; // Portable barycentric opt-in and private varying locations. *(uint32_t*)&_properties.pipelineCacheUUID[uuidComponentOffset] = NSSwapHostIntToBig(mtlFeatures); uuidComponentOffset += sizeof(mtlFeatures); } @@ -3582,10 +3635,15 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope pWritableExtns->vk_EXT_shader_subgroup_ballot.enabled = false; pWritableExtns->vk_EXT_shader_subgroup_vote.enabled = false; } + // The native path uses Metal barycentric coordinates. The experimental capture and replay of PerVertexKHR inputs + // is a separate path, which only the test entry point enables (isPortablePerVertexEnabled()). if (!_metalFeatures.shaderBarycentricCoordinates) { pWritableExtns->vk_KHR_fragment_shader_barycentric.enabled = false; pWritableExtns->vk_NV_fragment_shader_barycentric.enabled = false; } + // The first mesh path (mesh and fragment stages, direct draws, no mesh-stage resources) is exposed only + // to the test device adapter. + pWritableExtns->vk_EXT_mesh_shader.enabled = false; if (!_metalFeatures.arrayOfTextures || !_metalFeatures.arrayOfSamplers) { pWritableExtns->vk_EXT_descriptor_indexing.enabled = false; } @@ -3763,9 +3821,21 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope [nsUUID release]; // temp release } +id MVKPhysicalDevice::getLossRescueMTLCommandQueue() { + lock_guard lock(_lossRescueLock); + if ( !_lossRescueMTLQueue ) { + @autoreleasepool { + _lossRescueMTLQueue = [_mtlDevice newCommandQueue]; // retained + setMetalObjectLabel(_lossRescueMTLQueue, @"MoltenVK device loss semaphore release"); + } + } + return _lossRescueMTLQueue; +} + MVKPhysicalDevice::~MVKPhysicalDevice() { mvkDestroyContainerContents(_queueFamilies); [_timestampMTLCounterSet release]; + [_lossRescueMTLQueue release]; uint64_t memUsed = getCurrentAllocatedSize(); // Retrieve before releasing MTLDevice [_mtlDevice release]; @@ -3895,24 +3965,260 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope return rslt; } +// Called from Metal completion handlers as well as from queue workers, so it must never block. Reporting the loss to +// host waits is deferred to publishLoss(): a host wait that returns VK_ERROR_DEVICE_LOST ends the pending state of the +// command buffers it covers (Vulkan "Lost Device"), and a submission still encoding may be reading them. VkResult MVKDevice::markLost(bool alsoMarkPhysicalDevice) { - lock_guard lock(_sem4Lock); + if (alsoMarkPhysicalDevice) { _markPhysicalDeviceLost.store(true, std::memory_order_release); } + if (_losing.exchange(true)) { + if (alsoMarkPhysicalDevice && getConfigurationResult() != VK_SUCCESS) { _physicalDevice->setConfigurationResult(VK_ERROR_DEVICE_LOST); } + return VK_ERROR_DEVICE_LOST; + } + + // Queue workers blocked on emulated semaphores must stop encoding before the loss can be published. + { + lock_guard lock(_sem4Lock); + for (auto* sem4 : _encodingSemaphores) { sem4->cancel(); } + _encodingSemaphores.clear(); + } + // Release Metal waits now: a submission still encoding may be blocked until one of them completes. + releaseSemaphoreWaitsAfterLoss(); + + lock_guard lock(_lossLock); + _lossPublisher = dispatch_queue_create("MoltenVK device loss", DISPATCH_QUEUE_SERIAL); // retained + dispatch_async(_lossPublisher, ^{ publishLoss(); }); + return VK_ERROR_DEVICE_LOST; +} - setConfigurationResult(VK_ERROR_DEVICE_LOST); - if (alsoMarkPhysicalDevice) { _physicalDevice->setConfigurationResult(VK_ERROR_DEVICE_LOST); } +// The handlers added to a Metal command buffer by MVKDevice::addMTLCommandBufferHandler(), until it is committed. +@interface MVKMTLCommandBufferHandlers : NSObject { +@public + NSMutableArray* _completed; + NSMutableArray* _scheduled; + NSMutableArray* _resident; // MVKDevice::addMTLCommandBufferResidency(), created on first use +} +@end + +@implementation MVKMTLCommandBufferHandlers - for (auto* sem4 : _awaitingSemaphores) { - sem4->release(); +-(instancetype) init { + if ((self = [super init])) { + _completed = [NSMutableArray new]; // retained + _scheduled = [NSMutableArray new]; // retained } - for (auto& sem4AndValue : _awaitingTimelineSem4s) { - VkSemaphoreSignalInfo signalInfo; - signalInfo.value = sem4AndValue.second; - sem4AndValue.first->signal(&signalInfo); + return self; +} + +-(void) dealloc { + [_completed release]; + [_scheduled release]; + [_resident release]; + [super dealloc]; +} + +@end + +static char kMVKMTLCommandBufferHandlersKey; + +// Detaches and returns, retained, the handlers added to a Metal command buffer, if any. +static MVKMTLCommandBufferHandlers* mvkTakeMTLCommandBufferHandlers(id mtlCmdBuff) { + MVKMTLCommandBufferHandlers* handlers = [objc_getAssociatedObject(mtlCmdBuff, &kMVKMTLCommandBufferHandlersKey) retain]; + if (handlers) { objc_setAssociatedObject(mtlCmdBuff, &kMVKMTLCommandBufferHandlersKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } + return handlers; +} + +// Polls rather than waits for a notification: whatever decremented the count may not use this device again. +template +static void mvkWaitForZero(const std::atomic& count) { + while (count.load()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } +} + +// Reports the loss to host waits once no queue submission is encoding, after a final release of the Metal waits +// those submissions encoded. Later submissions are refused or dropped without encoding, since isLosing() is set. +void MVKDevice::publishLoss() { + mvkWaitForZero(_activeEncodings); + // No encoding remains, so no application work can be committed any more: the work that may still write application + // memory is now exactly the command buffers committed so far, whose completed handlers the inventory counts. + _isLossInventoryClosed = true; + releaseSemaphoreWaitsAfterLoss(); + // Work released above, or committed before the loss, may still use application resources. Work that waits for + // Metal work MoltenVK cannot release, such as the application's own on an exported command queue, holds the report + // back: MoltenVK cannot keep those resources alive once the loss is reported (see PerVertexLossTests.mm). + auto start = std::chrono::steady_clock::now(); + bool isWarned = false; + while (_pendingMTLCommandBufferHandlers.load()) { + if ( !isWarned && std::chrono::steady_clock::now() - start > std::chrono::seconds(1) ) { + reportWarning(VK_ERROR_DEVICE_LOST, "The device loss is not reported yet: Metal work that MoltenVK committed has not completed after one second. It may wait for Metal work that MoltenVK cannot release, such as work committed to an exported command queue."); + isWarned = true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - _awaitingSemaphores.clear(); - _awaitingTimelineSem4s.clear(); + { + lock_guard lock(_sem4Lock); + setConfigurationResult(VK_ERROR_DEVICE_LOST); + if (_markPhysicalDeviceLost.load(std::memory_order_acquire)) { _physicalDevice->setConfigurationResult(VK_ERROR_DEVICE_LOST); } + for (auto* sem4 : _awaitingSemaphores) { + sem4->cancel(); + } + for (auto& sem4AndValue : _awaitingTimelineSem4s) { + VkSemaphoreSignalInfo signalInfo; + signalInfo.value = sem4AndValue.second; + sem4AndValue.first->signal(&signalInfo); + } + _awaitingSemaphores.clear(); + _awaitingTimelineSem4s.clear(); + } + { + lock_guard lock(_lossLock); + _isLossPublished = true; + } + _lossCondition.notify_all(); +} - return getConfigurationResult(); +// Signals every semaphore or event that has encoded Metal waits to a value satisfying all of them, on a Metal queue that +// never holds application work. Metal event values only increase, so signals already executed are unaffected. +void MVKDevice::releaseSemaphoreWaitsAfterLoss() { + lock_guard lock(_lossReleaseLock); + if (_lossReleasables.empty()) { return; } + @autoreleasepool { + id mtlQueue = _physicalDevice->getLossRescueMTLCommandQueue(); + id mtlCmdBuff = [mtlQueue commandBuffer]; // retains the events it signals + bool released = true; + for (auto* releasable : _lossReleasables) { released &= releasable->releaseWaitsAfterLoss(mtlCmdBuff); } + commitMTLCommandBuffer(mtlCmdBuff); + if ( !released ) { reportWarning(VK_ERROR_DEVICE_LOST, "Could not release every Metal semaphore wait after device loss. Metal work waiting on them may not complete."); } + } +} + +VkResult MVKDevice::getHostWaitResult(bool isFinished, uint64_t timeout) { + // Orders the completion state the caller read before the loss checks below: a semaphore value raised by the + // release of Metal waits after a loss is then always followed by a check that sees the loss. + std::atomic_thread_fence(std::memory_order_seq_cst); + VkResult rslt = getConfigurationResult(); + if (rslt != VK_SUCCESS) { return rslt; } + if (isLosing()) { + unique_lock lock(_lossLock); + if (timeout == UINT64_MAX) { + _lossCondition.wait(lock, [this]{ return _isLossPublished; }); + } else if ( !_lossCondition.wait_for(lock, chrono::nanoseconds(min(timeout, kMVKUndefinedLargeUInt64)), [this]{ return _isLossPublished; }) ) { + return VK_TIMEOUT; + } + return VK_ERROR_DEVICE_LOST; + } + return isFinished ? VK_SUCCESS : VK_TIMEOUT; +} + +void MVKDevice::beginEncoding() { + _activeEncodings++; +} + +void MVKDevice::endEncoding() { + _activeEncodings--; +} + +// Returns the handlers attached to a Metal command buffer, attaching new ones if needed. +static MVKMTLCommandBufferHandlers* mvkGetMTLCommandBufferHandlers(id mtlCmdBuff) { + MVKMTLCommandBufferHandlers* handlers = objc_getAssociatedObject(mtlCmdBuff, &kMVKMTLCommandBufferHandlersKey); + if ( !handlers ) { + handlers = [MVKMTLCommandBufferHandlers new]; // temp retain + objc_setAssociatedObject(mtlCmdBuff, &kMVKMTLCommandBufferHandlersKey, handlers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [handlers release]; // temp release + } + return handlers; +} + +void MVKDevice::addMTLCommandBufferHandler(id mtlCmdBuff, MTLCommandBufferHandler handler, bool isScheduled) { + if ( !mtlCmdBuff ) { return; } + MVKMTLCommandBufferHandlers* handlers = mvkGetMTLCommandBufferHandlers(mtlCmdBuff); + id copiedHandler = [handler copy]; // temp retain + [(isScheduled ? handlers->_scheduled : handlers->_completed) addObject: copiedHandler]; + [copiedHandler release]; // temp release +} + +void MVKDevice::addMTLCommandBufferResidency(id mtlCmdBuff, id allocation) { + if ( !mtlCmdBuff ) { return; } + MVKMTLCommandBufferHandlers* handlers = mvkGetMTLCommandBufferHandlers(mtlCmdBuff); + if ( !handlers->_resident ) { handlers->_resident = [NSMutableArray new]; } // retained + [handlers->_resident addObject: allocation]; +} + +void MVKDevice::commitMTLCommandBuffer(id mtlCmdBuff) { + if ( !mtlCmdBuff ) { return; } + MVKMTLCommandBufferHandlers* handlers = mvkTakeMTLCommandBufferHandlers(mtlCmdBuff); + // Batch residency: resident before the command buffer is committed, removed after its other completed handlers. + NSArray* resident = handlers ? handlers->_resident : nil; + if (resident.count) { + makeResidentBatch(resident); + id removal = [^(id) { removeResidencyBatch(resident); } copy]; // temp retain, retains resident + [handlers->_completed addObject: removal]; + [removal release]; // temp release + } + NSArray* scheduled = handlers ? handlers->_scheduled : nil; + NSArray* completed = handlers ? handlers->_completed : nil; + // The end of each Metal handler is its last use of this device, which may be destroyed right after it. + if (scheduled.count) { + _pendingMTLCommandBufferHandlers++; + [mtlCmdBuff addScheduledHandler: ^(id mtlCB) { + for (MTLCommandBufferHandler handler in scheduled) { handler(mtlCB); } + _pendingMTLCommandBufferHandlers--; + }]; + } + // Until a loss closes the inventory, every command buffer is counted until its completed handlers have returned, + // even one without other completed handlers: it may write memory the application releases once the loss is reported. + bool isCounted = !_isLossInventoryClosed.load(); + if (completed.count || isCounted) { + if (isCounted) { _handlersBeforeInventory++; } + _pendingMTLCommandBufferHandlers++; + [mtlCmdBuff addCompletedHandler: ^(id mtlCB) { + for (MTLCommandBufferHandler handler in completed) { handler(mtlCB); } + if (isCounted) { _handlersBeforeInventory--; } + _pendingMTLCommandBufferHandlers--; + }]; + } + [handlers release]; + [mtlCmdBuff commit]; +} + +void MVKDevice::abandonMTLCommandBuffer(id mtlCmdBuff) { + if ( !mtlCmdBuff ) { return; } + MVKMTLCommandBufferHandlers* handlers = mvkTakeMTLCommandBufferHandlers(mtlCmdBuff); + if (handlers) { + for (MTLCommandBufferHandler handler in handlers->_completed) { handler(mtlCmdBuff); } + } + [handlers release]; +} + +void MVKDevice::waitForLossInventory() { + if ( !isLosing() ) { return; } + bool isWarned = false; + while ( !_isLossInventoryClosed.load() || _handlersBeforeInventory.load() ) { + if ( !isWarned ) { + reportWarning(VK_ERROR_DEVICE_LOST, "Freeing imported host memory waits for the Metal work committed before the device loss, which may still write it."); + isWarned = true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void MVKDevice::addLossReleasable(MVKLossReleasable* releasable) { + lock_guard lock(_lossReleaseLock); + _lossReleasables.insert(releasable); +} + +void MVKDevice::removeLossReleasable(MVKLossReleasable* releasable) { + lock_guard lock(_lossReleaseLock); + _lossReleasables.erase(releasable); +} + +void MVKDevice::addEncodingSemaphore(MVKSemaphoreImpl* sem4) { + lock_guard lock(_sem4Lock); + if (isLosing()) { sem4->cancel(); } + else { _encodingSemaphores.push_back(sem4); } +} + +void MVKDevice::removeEncodingSemaphore(MVKSemaphoreImpl* sem4) { + lock_guard lock(_sem4Lock); + mvkRemoveFirstOccurance(_encodingSemaphores, sem4); } void MVKDevice::getDescriptorSetLayoutSupport(const VkDescriptorSetLayoutCreateInfo* pCreateInfo, @@ -4572,7 +4878,11 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope void MVKDevice::freeMemory(MVKDeviceMemory* mvkDevMem, const VkAllocationCallbacks* pAllocator) { - if (mvkDevMem) { mvkDevMem->destroy(); } + if ( !mvkDevMem ) { return; } + // Once this returns, the application may reuse host memory it imported: no Metal work may write it any more. + // Memory imported during the loss, or whose import failed, has no such work. + if (mvkDevMem->isHostMemoryImportedBeforeLoss()) { waitForLossInventory(); } + mvkDevMem->destroy(); } // Look for an available pre-reserved private data slot and return its address if found. @@ -4673,7 +4983,8 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope void MVKDevice::addSemaphore(MVKSemaphoreImpl* sem4) { lock_guard lock(_sem4Lock); - _awaitingSemaphores.push_back(sem4); + if (getConfigurationResult() != VK_SUCCESS) { sem4->cancel(); } + else { _awaitingSemaphores.push_back(sem4); } } void MVKDevice::removeSemaphore(MVKSemaphoreImpl* sem4) { @@ -4833,6 +5144,21 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope @autoreleasepool { VkResult rslt = VK_SUCCESS; MVKMTLBlitEncoder mvkBlitEnc; + // A synchronization from the GPU may write imported host memory, which a device loss must count before reporting: + // it is admitted and committed within an encoding bracket, like a queue submission. Its Metal command buffer is + // acquired before the bracket, since the acquisition blocks while the queue is full, and the loss report waits for + // the bracket. The completion wait comes after the bracket too. Once the device is being lost, nothing is admitted: + // the contents of device memory are undefined after a loss. + for (uint32_t i = 0; i < memRangeCount && !mvkBlitEnc.mtlCmdBuffer && !isLosing(); i++) { + if (((MVKDeviceMemory*)pMemRanges[i].memory)->needsDeviceSynchronization()) { + mvkBlitEnc.mtlCmdBuffer = getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseInvalidateMappedMemoryRanges); + } + } + beginEncoding(); + if (isLosing()) { + abandonMTLCommandBuffer(mvkBlitEnc.mtlCmdBuffer); + mvkBlitEnc.mtlCmdBuffer = nil; + } for (uint32_t i = 0; i < memRangeCount; i++) { const VkMappedMemoryRange* pMem = &pMemRanges[i]; MVKDeviceMemory* mvkMem = (MVKDeviceMemory*)pMem->memory; @@ -4840,10 +5166,9 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope if (rslt == VK_SUCCESS) { rslt = r; } } if (mvkBlitEnc.mtlBlitEncoder) { [mvkBlitEnc.mtlBlitEncoder endEncoding]; } - if (mvkBlitEnc.mtlCmdBuffer) { - [mvkBlitEnc.mtlCmdBuffer commit]; - [mvkBlitEnc.mtlCmdBuffer waitUntilCompleted]; - } + commitMTLCommandBuffer(mvkBlitEnc.mtlCmdBuffer); + endEncoding(); + [mvkBlitEnc.mtlCmdBuffer waitUntilCompleted]; return rslt; } } @@ -4869,14 +5194,11 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope if ( !viewMask ) { return 0; } assert(passIdx < getMultiviewMetalPassCount(viewMask)); uint32_t mask = viewMask; - uint32_t startView = 0, viewCount = 0; + uint32_t startView = 0; if ( !_physicalDevice->canUseInstancingForMultiview() ) { - while (mask != 0) { - mask = mvkGetNextViewMaskGroup(mask, &startView, &viewCount); - while (passIdx-- > 0 && viewCount-- > 0) { - startView++; - } - } + // Each pass consumes one enabled view, including gaps between groups. + for (uint32_t i = 0; i < passIdx; ++i) { mask &= mask - 1; } + startView = __builtin_ctz(mask); } else { for (uint32_t i = 0; i <= passIdx; ++i) { mask = mvkGetNextViewMaskGroup(mask, &startView, nullptr); @@ -5508,6 +5830,10 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope [setDescriptor release]; } #endif + // Pool buffers are made resident once, here at their creation, in the set of this device. + if (mvkGetEnvVarNumber("MVK_CONFIG_PERVERTEX_SCRATCH_POOL", 0) == 1) { + _perVertexScratchPool = std::make_shared(NSUInteger(_physicalDevice->getMTLDevice().maxBufferLength), [this](id buffer) { makeResident(buffer); }); + } auto qFams = _physicalDevice->getQueueFamilies(); uint32_t qrCnt = pCreateInfo->queueCreateInfoCount; @@ -5576,6 +5902,10 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope } MVKDevice::~MVKDevice() { + // Queue submissions still encoding, and handlers of committed Metal command buffers, still use this device. + mvkWaitForZero(_activeEncodings); + mvkWaitForZero(_pendingMTLCommandBufferHandlers); + if (_isPerformanceTracking) { auto perfLogStyle = getMVKConfig().activityPerformanceLoggingStyle; if (perfLogStyle == MVK_CONFIG_ACTIVITY_PERFORMANCE_LOGGING_STYLE_DEVICE_LIFETIME) { @@ -5592,6 +5922,12 @@ static uint32_t mvkGetEntryProperty(io_registry_entry_t entry, CFStringRef prope mvkDestroyContainerContents(queues); } + // Wait for a pending loss report, which uses this device. + if (_lossPublisher) { + dispatch_sync(_lossPublisher, ^{}); + dispatch_release(_lossPublisher); + } + if (_commandResourceFactory) { _commandResourceFactory->destroy(); } for (auto &fences: _barrierFences) for (auto fence: fences) [fence release]; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def index 9342deb96..ecd9a2dc7 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def +++ b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def @@ -105,6 +105,7 @@ MVK_DEVICE_FEATURE_EXTN(ExtendedDynamicState3, EXTENDED_DYNAMIC_STATE MVK_DEVICE_FEATURE_EXTN(FragmentShaderInterlock, FRAGMENT_SHADER_INTERLOCK, EXT, 3) MVK_DEVICE_FEATURE_EXTN(Image2DViewOf3D, IMAGE_2D_VIEW_OF_3D, EXT, 2) MVK_DEVICE_FEATURE_EXTN(LegacyDithering, LEGACY_DITHERING, EXT, 1) +MVK_DEVICE_FEATURE_EXTN(MeshShader, MESH_SHADER, EXT, 5) MVK_DEVICE_FEATURE_EXTN(MultiDraw, MULTI_DRAW, EXT, 1) MVK_DEVICE_FEATURE_EXTN(NestedCommandBuffer, NESTED_COMMAND_BUFFER, EXT, 3) MVK_DEVICE_FEATURE_EXTN(NonSeamlessCubeMap, NON_SEAMLESS_CUBE_MAP, EXT, 1) diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.h b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.h index b349d6ea8..1b830011f 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.h @@ -55,6 +55,21 @@ class MVKDeviceMemory : public MVKVulkanAPIDeviceObject { /** Returns the debug report object type of this object. */ VkDebugReportObjectTypeEXT getVkDebugReportObjectType() override { return VK_DEBUG_REPORT_OBJECT_TYPE_DEVICE_MEMORY_EXT; } + /** + * Returns whether this memory maps host memory that the application imported (VK_EXT_external_memory_host), + * successfully, before the device began to be lost: only Metal work admitted before the loss may still write it. + */ + bool isHostMemoryImportedBeforeLoss() { return _isHostMemImported && !_isCreatedDuringLoss && getConfigurationResult() == VK_SUCCESS; } + + /** Returns whether the host sees the GPU's writes to this memory only after a synchronization encoded on the GPU. */ + bool needsDeviceSynchronization() { +#if MVK_MACOS + return !isUnifiedMemoryGPU() && _mtlBuffer && _mtlStorageMode == MTLStorageModeManaged && isMemoryHostAccessible(); +#else + return false; +#endif + } + /** Returns whether the memory is accessible from the host. */ inline bool isMemoryHostAccessible() { if (_mtlStorageMode == MTLStorageModeMemoryless) @@ -103,11 +118,11 @@ class MVKDeviceMemory : public MVKVulkanAPIDeviceObject { * If this memory is host-visible, pulls the specified memory range from the device. * * If pBlitEnc is not null, it points to a holder for a MTLBlitCommandEncoder and its - * associated MTLCommandBuffer. If this instance has a MTLBuffer using managed memory, - * this function may call synchronizeResource: on the MTLBlitCommandEncoder to - * synchronize the GPU contents to the CPU. If the contents of the pBlitEnc do not - * include a MTLBlitCommandEncoder and MTLCommandBuffer, this function will create - * them and populate the contents into the MVKMTLBlitEncoder struct. + * associated MTLCommandBuffer. If this instance needs a device synchronization, and the + * holder has a MTLCommandBuffer, this function calls synchronizeResource: on its + * MTLBlitCommandEncoder, which it creates if the holder has none, to synchronize the + * GPU contents to the CPU. It never acquires a MTLCommandBuffer: the caller does, before + * it brackets the encoding (see MVKDevice::invalidateMappedMemoryRanges()). */ VkResult pullFromDevice(VkDeviceSize offset, VkDeviceSize size, @@ -185,6 +200,7 @@ class MVKDeviceMemory : public MVKVulkanAPIDeviceObject { MTLCPUCacheMode _mtlCPUCacheMode; bool _isDedicated = false; bool _isHostMemImported = false; + bool _isCreatedDuringLoss = false; VkExternalMemoryHandleTypeFlags _externalMemoryHandleType = 0u; }; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm index 00b182b88..7790b5684 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm @@ -106,8 +106,7 @@ if (memSize == 0 || !isMemoryHostAccessible()) { return VK_SUCCESS; } #if MVK_MACOS - if ( !isUnifiedMemoryGPU() && pBlitEnc && _mtlBuffer && _mtlStorageMode == MTLStorageModeManaged) { - if ( !pBlitEnc->mtlCmdBuffer) { pBlitEnc->mtlCmdBuffer = _device->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseInvalidateMappedMemoryRanges); } + if (needsDeviceSynchronization() && pBlitEnc && pBlitEnc->mtlCmdBuffer) { if ( !pBlitEnc->mtlBlitEncoder) { pBlitEnc->mtlBlitEncoder = [pBlitEnc->mtlCmdBuffer blitCommandEncoder]; } [pBlitEnc->mtlBlitEncoder synchronizeResource: _mtlBuffer]; } @@ -305,6 +304,9 @@ MVKDeviceMemory::MVKDeviceMemory(MVKDevice* device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator) : MVKVulkanAPIDeviceObject(device) { + // No Metal work admitted before a loss can use memory created once the loss has begun. + _isCreatedDuringLoss = device->isLosing(); + // Set Metal memory parameters _vkMemAllocFlags = 0; _vkMemPropFlags = getDeviceMemoryProperties().memoryTypes[pAllocateInfo->memoryTypeIndex].propertyFlags; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h index 45ddd3a47..f44bdd252 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h @@ -499,6 +499,7 @@ class MVKPresentableSwapchainImage : public MVKSwapchainImage { friend MVKSwapchain; id getCAMetalDrawable(); + id requestNextDrawable(); void addPresentedHandler(id mtlDrawable, MVKImagePresentInfo presentInfo, MVKSwapchainSignaler signaler); void releaseMetalDrawable(); MVKSwapchainImageAvailability getAvailability(); diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm index 2b394922d..b55d998ec 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm @@ -447,9 +447,12 @@ static uint32_t mvkGetMTLTextureIOSurfaceID(id tex) { MVKImageSubresource& subresource, const MVKMappedMemoryRange& mappedRange) { - [cmdEncoder->_mtlCmdBuffer addCompletedHandler: ^(id mcb) { - getMTLTextureContent(subresource, mappedRange.offset, mappedRange.size); - }]; + cmdEncoder->getDevice()->addMTLCommandBufferHandler(cmdEncoder->_mtlCmdBuffer, ^(id mcb) { + // An abandoned command buffer never wrote the subresource. + if (mcb.status != MTLCommandBufferStatusNotEnqueued) { + getMTLTextureContent(subresource, mappedRange.offset, mappedRange.size); + } + }); } MVKImagePlane::MVKImagePlane(MVKImage* image, uint8_t planeIndex) { @@ -798,7 +801,13 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) { // to sync the managed memory from the GPU, so the texture content is accessible to be copied by the CPU. if ( !isUnifiedMemoryGPU() && getMTLStorageMode() == MTLStorageModeManaged ) { @autoreleasepool { - id mtlCmdBuff = getDevice()->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseCopyImageToMemory); + // Acquired, admitted and committed as in MVKDevice::invalidateMappedMemoryRanges(), for the same reasons. + id mtlCmdBuff = getDevice()->isLosing() ? nil : getDevice()->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseCopyImageToMemory); + getDevice()->beginEncoding(); + if (getDevice()->isLosing()) { + getDevice()->abandonMTLCommandBuffer(mtlCmdBuff); + mtlCmdBuff = nil; + } id mtlBlitEnc = [mtlCmdBuff blitCommandEncoder]; for (uint32_t imgRgnIdx = 0; imgRgnIdx < pCopyImageToMemoryInfo->regionCount; imgRgnIdx++) { @@ -816,7 +825,8 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) { } [mtlBlitEnc endEncoding]; - [mtlCmdBuff commit]; + getDevice()->commitMTLCommandBuffer(mtlCmdBuff); + getDevice()->endEncoding(); [mtlCmdBuff waitUntilCompleted]; } } @@ -1139,22 +1149,38 @@ static MTLRegion getMTLRegion(const ImgRgn& imgRgn) { } MTLStorageMode MVKImage::getMTLStorageMode() { - if ( !_memoryBindings[0]->_deviceMemory ) return MTLStorageModePrivate; - - MTLStorageMode stgMode = _memoryBindings[0]->_deviceMemory->getMTLStorageMode(); + // Imported textures cannot be promoted. Report their real storage so render-pass + // load/store selection and indexed PerVertex capture still reject true memoryless backing. + if (_planes[0]->_mtlTexture) { return _planes[0]->_mtlTexture.storageMode; } + MVKImageMemoryBinding* memoryBinding = _memoryBindings[0]; + MVKDeviceMemory* dvcMem = memoryBinding->_deviceMemory; + if (!dvcMem) { return MTLStorageModePrivate; } + if ((dvcMem->_externalMemoryHandleType & VK_EXTERNAL_MEMORY_HANDLE_TYPE_MTLTEXTURE_BIT_EXT) && dvcMem->_mtlTexture) { return dvcMem->_mtlTexture.storageMode; } + // Match the dedicated-alias texture reuse in MVKImagePlane::getMTLTexture(). + if (!_ioSurface && !memoryBinding->_mtlTexelBuffer && !(dvcMem->getMTLHeap() && !getIsDepthStencil()) && _isAliasable && dvcMem->isDedicatedAllocation() && !mvkContains(dvcMem->_imageMemoryBindings, memoryBinding)) { + return dvcMem->_imageMemoryBindings[0]->_image->getMTLStorageMode(); + } - if (_ioSurface && stgMode == MTLStorageModePrivate) { stgMode = MTLStorageModeShared; } + MTLStorageMode stgMode = dvcMem->getMTLStorageMode(); // An input attachment is read through a texture binding, which memoryless storage does not // support, so commit the memory instead. Lazily allocated memory only promises that an // implementation may defer the allocation, and never that it must. // TODO: support framebuffer fetch so VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT reads color(m) in the // shader rather than a texture binding, and memoryless storage can be kept here. + // Indexed PerVertex capture also needs store/load across a render -> compute -> render split. + // Extension enablement is immutable before image creation, unlike the eventual pipeline choice. + // Commit lazy attachments on these devices, trading tile-only storage for memory and bandwidth. + const auto& extensions = getEnabledExtensions(); + bool mayCapturePerVertex = getPhysicalDevice()->isPortablePerVertexEnabled() && + (extensions.vk_KHR_fragment_shader_barycentric.enabled || extensions.vk_NV_fragment_shader_barycentric.enabled); if (stgMode == MTLStorageModeMemoryless && - mvkIsAnyFlagEnabled(getCombinedUsage(), VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT)) { + (mvkIsAnyFlagEnabled(getCombinedUsage(), VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT) || mayCapturePerVertex)) { stgMode = MTLStorageModePrivate; } + if (_ioSurface && stgMode == MTLStorageModePrivate) { stgMode = MTLStorageModeShared; } + return stgMode; } @@ -1599,7 +1625,7 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { } signal(signaler.semaphore, signaler.semaphoreSignalToken, mtlCmdBuff); signal(signaler.fence); - [mtlCmdBuff commit]; + _device->commitMTLCommandBuffer(mtlCmdBuff); } _preSignaler = signaler; @@ -1624,8 +1650,10 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { uint32_t attemptCnt = _swapchain->getImageCount(); // Attempt a resonable number of times for (uint32_t attemptIdx = 0; !_mtlDrawable && attemptIdx < attemptCnt; attemptIdx++) { uint64_t startTime = getPerformanceTimestamp(); - _mtlDrawable = [_swapchain->getCAMetalLayer().nextDrawable retain]; // retained + _mtlDrawable = requestNextDrawable(); // retained addPerformanceInterval(getPerformanceStats().queue.retrieveCAMetalDrawable, startTime); + // While the device is being lost, the encoding goes on without a drawable: its submission is lost. + if ( !_mtlDrawable && getDevice()->isLosing() ) { return nil; } hasInvalidFormat = _mtlDrawable && !_mtlDrawable.texture.pixelFormat; if (hasInvalidFormat) { releaseMetalDrawable(); } } @@ -1639,6 +1667,40 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { return _mtlDrawable; } +// Requests the next drawable of the layer on the surface's drawable queue, and waits for it. The wait checks every +// millisecond whether the device is being lost, and then gives up: an encoding, which the loss report waits for, never +// waits in Core Animation, where nextDrawable may take a second or more. A drawable that arrives after the wait gave up +// is released at the end of its request, which returns it to the layer. The request uses neither this image nor the +// device. Returns the drawable retained, or nil. +id MVKPresentableSwapchainImage::requestNextDrawable() { + struct Request { + std::mutex lock; + std::condition_variable done; + id drawable = nil; + bool isDone = false; + bool isAbandoned = false; + }; + auto request = std::make_shared(); + CAMetalLayer* mtlLayer = _swapchain->getCAMetalLayer(); // retained by the block + dispatch_async(_swapchain->_surface->getDrawableQueue(), ^{ + @autoreleasepool { + id drawable = mtlLayer.nextDrawable; + std::lock_guard lock(request->lock); + if ( !request->isAbandoned ) { request->drawable = [drawable retain]; } + request->isDone = true; + request->done.notify_one(); + } + }); + std::unique_lock lock(request->lock); + while ( !request->done.wait_for(lock, std::chrono::milliseconds(1), [&request]{ return request->isDone; }) ) { + if (getDevice()->isLosing()) { + request->isAbandoned = true; + return nil; + } + } + return request->drawable; +} + // If not headless, retrieve the MTLTexture directly from the CAMetalDrawable. id MVKPresentableSwapchainImage::getMTLTexture(uint8_t planeIndex) { return _mtlTextureHeadless ? _mtlTextureHeadless : getCAMetalDrawable().texture; @@ -1656,7 +1718,7 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { // Attach present handler before presenting to avoid race condition. id mtlDrwbl = getCAMetalDrawable(); MVKSwapchainSignaler signaler = getPresentationSignaler(); - [mtlCmdBuff addScheduledHandler: ^(id mcb) { + getDevice()->addMTLCommandBufferHandler(mtlCmdBuff, ^(id mcb) { addPresentedHandler(mtlDrwbl, presentInfo, signaler); @@ -1670,7 +1732,7 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { } else { [mtlDrwbl present]; } - }]; + }, true); // Ensure this image, the drawable, and the present fence are not destroyed while // awaiting MTLCommandBuffer completion. We retain the drawable separately because @@ -1682,13 +1744,13 @@ static void signalAndUntrack(const MVKSwapchainSignaler& signaler) { [mtlDrwbl retain]; auto* fence = presentInfo.fence; if (fence) { fence->retain(); } - [mtlCmdBuff addCompletedHandler: ^(id mcb) { + getDevice()->addMTLCommandBufferHandler(mtlCmdBuff, ^(id mcb) { signal(fence); if (fence) { fence->release(); } [mtlDrwbl release]; release(); if (_swapchain) { _swapchain->notifyPresentComplete(presentInfo); } - }]; + }); signal(signaler.semaphore, signaler.semaphoreSignalToken, mtlCmdBuff); diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm index ef69994ab..556850d57 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm @@ -787,6 +787,9 @@ ADD_DVC_EXT_ENTRY_POINT(vkCmdDebugMarkerInsertEXT, EXT_DEBUG_MARKER); ADD_DVC_EXT_ENTRY_POINT(vkGetMemoryHostPointerPropertiesEXT, EXT_EXTERNAL_MEMORY_HOST); ADD_DVC_EXT_ENTRY_POINT(vkSetHdrMetadataEXT, EXT_HDR_METADATA); + ADD_DVC_EXT_ENTRY_POINT(vkCmdDrawMeshTasksEXT, EXT_MESH_SHADER); + ADD_DVC_EXT_ENTRY_POINT(vkCmdDrawMeshTasksIndirectEXT, EXT_MESH_SHADER); + ADD_DVC_EXT_ENTRY_POINT(vkCmdDrawMeshTasksIndirectCountEXT, EXT_MESH_SHADER); ADD_DVC_EXT_ENTRY_POINT(vkCmdDrawMultiEXT, EXT_MULTI_DRAW); ADD_DVC_EXT_ENTRY_POINT(vkCmdDrawMultiIndexedEXT, EXT_MULTI_DRAW); ADD_DVC_EXT_ENTRY_POINT(vkExportMetalObjectsEXT, EXT_METAL_OBJECTS); diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h index e385e9bd2..0fce2680d 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h @@ -250,6 +250,49 @@ class MVKGraphicsPipeline : public MVKPipeline { /** Returns whether this pipeline has tessellation shaders. */ bool isTessellationPipeline() { return _isTessellationPipeline; } + /** Whether this pipeline draws with a mesh shader, through vkCmdDrawMeshTasksEXT. */ + bool isMeshPipeline() const { return _isMeshPipeline; } + + /** Threads per mesh threadgroup: the workgroup size of the mesh shader. */ + MTLSize getMeshThreadgroupSize() const { return _meshThreadgroupSize; } + + /** The mesh grid limit, in threadgroups, that the Metal pipeline state reports; only smaller grids are executed. */ + NSUInteger getMaxMeshThreadgroupsPerGrid() const { return _maxMeshThreadgroupsPerGrid; } + + /** Portable PerVertexKHR uses render capture for direct draws and compute capture for indexed draws, followed by replay. + * Vertex stage resources describe capture; replay uses getPerVertexReplayBinding() and the optional implicit reversed-depth mask. */ + bool usesPerVertexInputBuffer() const { return _usesPerVertexInputBuffer; } + // Admission is test-only until GPU topology covers arbitrary TCS factors. + bool usesPerVertexTessEval() const { return _usesPerVertexInputBuffer && _isTessellationPipeline; } + + /** Whether the ordinary tessellation path keeps float32 TCS levels and classifies them into Metal half factors. */ + bool usesFloat32TessLevels() const { return _usesFloat32TessLevels && !usesPerVertexTessEval(); } + id getTessLevelsToHalfFactorsPipelineState() const { return _mtlTessLevelsToHalfFactorsState; } + id getPerVertexTessEvalPipelineState() const { return _mtlPerVertexTessEvalState; } + /** Counter-clockwise TES winding reverses the generator's clockwise corner order; a lower-left domain origin reverses it again. */ + bool perVertexTessReversesCorners() const { return (_tessReflectData.windingOrder == spv::ExecutionModeVertexOrderCcw) != _perVertexTessLowerLeft; } + const SPIRV_CROSS_NAMESPACE::MSLCapturedOutputReplayBarycentricBinding& getPerVertexReplayBarycentricBinding() const { return _perVertexReplayBarycentricBinding; } + bool usesPortableBarycentrics() const { return _usesPerspectiveBarycentrics || _usesNoPerspectiveBarycentrics; } + /** + * Whether the non-indexed capture draws triangles. Other topologies, and every topology of a pipeline whose topology + * is dynamic, capture their vertices as points; a dynamic topology stays within the pipeline's topology class. + */ + bool capturesPerVertexTriangleLists() const { return _perVertexCapturesTriangleLists; } + id getPerVertexCapturePipelineState(uint32_t viewCount) const { + auto itr = _perVertexCapturePipelineStates.find(viewCount); + return itr == _perVertexCapturePipelineStates.end() ? nil : itr->second.direct; + } + id getPerVertexIndexedCapturePipelineState(bool index32, uint32_t viewCount) const { + auto itr = _perVertexCapturePipelineStates.find(viewCount); + return itr == _perVertexCapturePipelineStates.end() ? nil : (index32 ? itr->second.index32 : itr->second.index16); + } + const SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout& getPerVertexCapturedLayout() const { return _perVertexCapturedLayout; } + const SPIRV_CROSS_NAMESPACE::MSLCapturedOutputReplayBinding& getPerVertexReplayBinding() const { return _perVertexReplayBinding; } + const SPIRV_CROSS_NAMESPACE::MSLPerVertexInputBinding& getPerVertexInputBinding() const { return _perVertexInputBinding; } + uint32_t getPerVertexCaptureBufferIndex() const { return getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::Output]; } + /** Capture parameters start with the record count per instance (uint32). */ + uint32_t getPerVertexCaptureParamsBufferIndex() const { return getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::IndirectParams]; } + /** Returns the number of output tessellation patch control points. */ uint32_t getOutputControlPointCount() { return _outputControlPointCount; } @@ -339,7 +382,11 @@ class MVKGraphicsPipeline : public MVKPipeline { bool compileTessControlStageState(MTLComputePipelineDescriptor* tcPLDesc, VkPipelineCreationFeedback* pTessCtlFB); void initDynamicState(const VkGraphicsPipelineCreateInfo* pCreateInfo); void initSampleLocations(const VkGraphicsPipelineCreateInfo* pCreateInfo); + bool initPerVertexInputPipeline(const VkGraphicsPipelineCreateInfo* pCreateInfo, const VkPipelineShaderStageCreateInfo* pVertexSS, const VkPipelineShaderStageCreateInfo* pFragmentSS); + bool addPerVertexIndexedCapturePipelines(MTLVertexDescriptor* vertexDesc, const mvk::SPIRVToMSLConversionConfiguration& shaderConfig, const VkPipelineShaderStageCreateInfo* pVertexSS, uint32_t viewCount); + bool addPerVertexReplayShaderToPipeline(MTLRenderPipelineDescriptor* plDesc, const VkGraphicsPipelineCreateInfo* pCreateInfo, const mvk::SPIRVToMSLConversionConfiguration& shaderConfig, const VkPipelineShaderStageCreateInfo* pVertexSS, const VkPipelineShaderStageCreateInfo* pFragmentSS, SPIRVShaderOutputs& vertexOutputs); void initMTLRenderPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, const mvk::SPIRVTessReflectionData& reflectData, VkPipelineCreationFeedback* pPipelineFB, const VkPipelineShaderStageCreateInfo* pVertexSS, VkPipelineCreationFeedback* pVertexFB, const VkPipelineShaderStageCreateInfo* pTessCtlSS, VkPipelineCreationFeedback* pTessCtlFB, const VkPipelineShaderStageCreateInfo* pTessEvalSS, VkPipelineCreationFeedback* pTessEvalFB, const VkPipelineShaderStageCreateInfo* pFragmentSS, VkPipelineCreationFeedback* pFragmentFB); + bool initMeshPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, const VkPipelineShaderStageCreateInfo* pTaskSS, const VkPipelineShaderStageCreateInfo* pMeshSS, VkPipelineCreationFeedback* pMeshFB, const VkPipelineShaderStageCreateInfo* pFragmentSS, VkPipelineCreationFeedback* pFragmentFB); void initShaderConversionConfig(mvk::SPIRVToMSLConversionConfiguration& shaderConfig, const VkGraphicsPipelineCreateInfo* pCreateInfo, const mvk::SPIRVTessReflectionData& reflectData); void initReservedVertexAttributeBufferCount(const VkGraphicsPipelineCreateInfo* pCreateInfo); void addVertexInputToShaderConversionConfig(mvk::SPIRVToMSLConversionConfiguration& shaderConfig, const VkGraphicsPipelineCreateInfo* pCreateInfo); @@ -392,17 +439,41 @@ class MVKGraphicsPipeline : public MVKPipeline { id _mtlTessVertexStageIndex16State = nil; id _mtlTessVertexStageIndex32State = nil; id _mtlTessControlStageState = nil; + id _mtlTessLevelsToHalfFactorsState = nil; + id _mtlPerVertexTessEvalState = nil; id _mtlPipelineState = nil; + struct PerVertexCapturePipelineStates { + id direct = nil; + id index16 = nil; + id index32 = nil; + }; + std::unordered_map _perVertexCapturePipelineStates; + SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout _perVertexCapturedLayout; + SPIRV_CROSS_NAMESPACE::MSLCapturedOutputReplayBinding _perVertexReplayBinding; + SPIRV_CROSS_NAMESPACE::MSLPerVertexInputBinding _perVertexInputBinding; + SPIRV_CROSS_NAMESPACE::MSLCapturedOutputReplayBarycentricBinding _perVertexReplayBarycentricBinding; + bool _usesPerspectiveBarycentrics = false; + bool _usesNoPerspectiveBarycentrics = false; + bool _hasPerVertexInputs = false; + bool _usesPerVertexInputBuffer = false; MVKShaderImplicitRezBinding _reservedVertexAttributeBufferCount; VkPrimitiveTopology _vkPrimitiveTopology; uint32_t _outputControlPointCount; + mvk::SPIRVTessReflectionData _tessReflectData; + bool _perVertexTessLowerLeft = false; + bool _perVertexCapturesTriangleLists = false; MVKShaderModule* _vertexModule = nullptr; MVKShaderModule* _tessCtlModule = nullptr; MVKShaderModule* _tessEvalModule = nullptr; MVKShaderModule* _fragmentModule = nullptr; + MVKShaderModule* _meshModule = nullptr; bool _ownsVertexModule = false; + bool _ownsMeshModule = false; + bool _isMeshPipeline = false; + MTLSize _meshThreadgroupSize = {}; + NSUInteger _maxMeshThreadgroupsPerGrid = 0; bool _ownsTessCtlModule = false; bool _ownsTessEvalModule = false; bool _ownsFragmentModule = false; @@ -411,6 +482,7 @@ class MVKGraphicsPipeline : public MVKPipeline { bool _isRasterizing = false; bool _isRasterizingColor = false; bool _isTessellationPipeline = false; + bool _usesFloat32TessLevels = false; bool _inputAttachmentIsDSAttachment = false; bool _hasRemappedAttachmentLocations = false; }; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm index 47bbd4fca..cb16f0bbb 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm @@ -17,6 +17,12 @@ */ #include "MVKPipeline.h" +#include "../Commands/MVKPerVertexReplay.h" +#include "../Commands/MVKPerVertexCapacity.h" +#ifdef MVK_TEST_TES_PERVERTEX_FIXTURE +// Generated solely by MoltenVK/Tests/run-pervertex-tes-tests.sh. Never a shipping option. +#include MVK_TEST_TES_PERVERTEX_FIXTURE +#endif #include "MVKCommandBuffer.h" #include "MVKInlineObjectConstructor.h" #include "MVKImage.h" @@ -707,6 +713,22 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor const VkGraphicsPipelineCreateInfo* pCreateInfo) : MVKPipeline(device, pipelineCache, (MVKPipelineLayout*)pCreateInfo->layout, getPipelineCreateFlags(pCreateInfo), parent) { + // Mesh pipelines ignore vertex input and input assembly, which may then hold anything. The shared code below + // reads them, so give it no vertex input and the triangle lists that admitted mesh shaders emit. + VkGraphicsPipelineCreateInfo meshCreateInfo; + VkPipelineVertexInputStateCreateInfo meshVertexInput = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, nullptr, 0, 0, nullptr, 0, nullptr}; + VkPipelineInputAssemblyStateCreateInfo meshInputAssembly = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, nullptr, 0, VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_FALSE}; + for (uint32_t i = 0; i < pCreateInfo->stageCount; i++) { + _isMeshPipeline |= mvkIsAnyFlagEnabled(pCreateInfo->pStages[i].stage, VK_SHADER_STAGE_MESH_BIT_EXT | VK_SHADER_STAGE_TASK_BIT_EXT); + } + if (_isMeshPipeline) { + meshCreateInfo = *pCreateInfo; + meshInputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + meshCreateInfo.pVertexInputState = &meshVertexInput; + meshCreateInfo.pInputAssemblyState = &meshInputAssembly; + pCreateInfo = &meshCreateInfo; + } + // Extract dynamic state first, as it can affect many configurations. initDynamicState(pCreateInfo); @@ -755,6 +777,9 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor const VkPipelineShaderStageCreateInfo* pTessCtlSS = nullptr; const VkPipelineShaderStageCreateInfo* pTessEvalSS = nullptr; const VkPipelineShaderStageCreateInfo* pFragmentSS = nullptr; + const VkPipelineShaderStageCreateInfo* pTaskSS = nullptr; + const VkPipelineShaderStageCreateInfo* pMeshSS = nullptr; + VkPipelineCreationFeedback* pMeshFB = nullptr; VkPipelineCreationFeedback* pVertexFB = nullptr; VkPipelineCreationFeedback* pTessCtlFB = nullptr; VkPipelineCreationFeedback* pTessEvalFB = nullptr; @@ -786,6 +811,15 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor pFragmentFB = &pFeedbackInfo->pPipelineStageCreationFeedbacks[i]; } break; + case VK_SHADER_STAGE_TASK_BIT_EXT: + pTaskSS = pSS; + break; + case VK_SHADER_STAGE_MESH_BIT_EXT: + pMeshSS = pSS; + if (pFeedbackInfo && pFeedbackInfo->pPipelineStageCreationFeedbacks) { + pMeshFB = &pFeedbackInfo->pPipelineStageCreationFeedbacks[i]; + } + break; default: break; } @@ -795,6 +829,7 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor _tessCtlModule = getOrCreateShaderModule(device, pTessCtlSS, _ownsTessCtlModule); _tessEvalModule = getOrCreateShaderModule(device, pTessEvalSS, _ownsTessEvalModule); _fragmentModule = getOrCreateShaderModule(device, pFragmentSS, _ownsFragmentModule); + _meshModule = getOrCreateShaderModule(device, pMeshSS, _ownsMeshModule); warnIfUnsupportedRobustnessEnabled(this, pVertexSS); warnIfUnsupportedRobustnessEnabled(this, pTessCtlSS); @@ -819,6 +854,21 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor } // Tessellation - must ignore allowed bad pTessellationState pointer if not tess pipeline + _tessReflectData = reflectData; + // Metal only reads half tessellation factors, which round levels such as nextafter(1, +inf) before Vulkan + // classifies them. The ordinary path keeps the TCS levels in float32 until a kernel classifies them, for the + // configuration the Vulkan oracles prove: triangles, equal spacing, no point mode, upper-left origin, one view. + // Everything else keeps the half path. + if (_isTessellationPipeline && reflectData.float32TessLevels && reflectData.patchKind == spv::ExecutionModeTriangles && + reflectData.partitionMode == spv::ExecutionModeSpacingEqual && !reflectData.pointMode && !getRenderingCreateInfo(pCreateInfo)->viewMask) { + _usesFloat32TessLevels = true; + for (const auto* next = pCreateInfo->pTessellationState ? (const VkBaseInStructure*)pCreateInfo->pTessellationState->pNext : nullptr; next; next = next->pNext) { + if (next->sType == VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_DOMAIN_ORIGIN_STATE_CREATE_INFO && + ((const VkPipelineTessellationDomainOriginStateCreateInfo*)next)->domainOrigin == VK_TESSELLATION_DOMAIN_ORIGIN_LOWER_LEFT) { + _usesFloat32TessLevels = false; + } + } + } _outputControlPointCount = reflectData.numControlPoints; if (_isTessellationPipeline && pCreateInfo->pTessellationState) _staticStateData.patchControlPoints = pCreateInfo->pTessellationState->patchControlPoints; @@ -862,8 +912,24 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor } // Render pipeline state. Do this as early as possible, to fail fast if pipeline requires a fail on cache-miss. - initMTLRenderPipelineState(pCreateInfo, reflectData, pPipelineFB, pVertexSS, pVertexFB, pTessCtlSS, pTessCtlFB, pTessEvalSS, pTessEvalFB, pFragmentSS, pFragmentFB); + if (_isMeshPipeline) { + if (!initMeshPipelineState(pCreateInfo, pTaskSS, pMeshSS, pMeshFB, pFragmentSS, pFragmentFB)) { + _hasValidMTLPipelineStates = false; + return; + } + } else { + if (!initPerVertexInputPipeline(pCreateInfo, pVertexSS, pFragmentSS)) { + _hasValidMTLPipelineStates = false; + return; + } + initMTLRenderPipelineState(pCreateInfo, reflectData, pPipelineFB, pVertexSS, pVertexFB, pTessCtlSS, pTessCtlFB, pTessEvalSS, pTessEvalFB, pFragmentSS, pFragmentFB); + } if ( !_hasValidMTLPipelineStates ) { return; } + if (usesFloat32TessLevels()) { + if (shouldFailOnPipelineCompileRequired()) { setConfigurationResult(VK_PIPELINE_COMPILE_REQUIRED); _hasValidMTLPipelineStates = false; return; } + _mtlTessLevelsToHalfFactorsState = getDevice()->getCommandResourceFactory()->newTessLevelsToHalfFactorsMTLComputePipelineState(this); + if (!_mtlTessLevelsToHalfFactorsState || getConfigurationResult() != VK_SUCCESS) { _hasValidMTLPipelineStates = false; return; } + } // Blending - must ignore allowed bad pColorBlendState pointer if rasterization disabled or no color attachments if (_isRasterizingColor && pCreateInfo->pColorBlendState) { @@ -879,6 +945,7 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor } _staticStateData.primitiveType = mvkMTLPrimitiveTypeFromVkPrimitiveTopology(_vkPrimitiveTopology); + _staticStateData.vkPrimitiveTopology = static_cast(_vkPrimitiveTopology); _staticStateData.enable.set(MVKRenderStateEnableFlag::PrimitiveRestart, primitiveRestart); #if MVK_USE_METAL_PRIVATE_API @@ -1104,7 +1171,7 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor MTLRenderPipelineDescriptor* plDesc = newMTLRenderPipelineDescriptor(pCreateInfo, reflectData, pVertexSS, pVertexFB, pFragmentSS, pFragmentFB); // temp retain if (plDesc) { auto viewMask = getRenderingCreateInfo(pCreateInfo)->viewMask; - if (mvkIsMultiview(viewMask)) { + if (mvkIsMultiview(viewMask) && !_usesPerVertexInputBuffer) { // We need to adjust the step rate for per-instance attributes to account for the // extra instances needed to render all views. But, there's a problem: vertex input // descriptions are static pipeline state. If we need multiple passes, and some have @@ -1169,6 +1236,428 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor } } +// Whether the stage's entry point statically uses any descriptor-set resource or push constants. +static bool mvkUsesResources(const std::vector& spirv, spv::ExecutionModel model, const char* entryName) { + SPIRV_CROSS_NAMESPACE::Compiler reflect(spirv); + reflect.set_entry_point(entryName, model); + auto resources = reflect.get_shader_resources(reflect.get_active_interface_variables()); + return !resources.uniform_buffers.empty() || !resources.storage_buffers.empty() || !resources.storage_images.empty() || + !resources.sampled_images.empty() || !resources.separate_images.empty() || !resources.separate_samplers.empty() || + !resources.subpass_inputs.empty() || !resources.acceleration_structures.empty() || !resources.push_constant_buffers.empty(); +} + +// Checks the mesh shader forms the first mesh path supports: triangle output and no per-primitive outputs. +// Returns the workgroup size, read through LocalSize or LocalSizeId, or an empty reason. +static std::string mvkMeshShaderIncompatibility(const std::vector& spirv, const char* entryName, MTLSize& workgroupSize) { + SPIRV_CROSS_NAMESPACE::Compiler reflect(spirv); + reflect.set_entry_point(entryName, spv::ExecutionModelMeshEXT); + if (!reflect.get_execution_mode_bitset().get(spv::ExecutionModeOutputTrianglesEXT)) { return "emits points or lines."; } + // The mesh stage binds no implicit buffer and tracks no device-address residency yet. + for (auto capability : reflect.get_declared_capabilities()) { + if (capability == spv::CapabilityPhysicalStorageBufferAddresses) { return "uses physical storage buffer addresses."; } + } + reflect.update_active_builtins(); + if (reflect.has_active_builtin(spv::BuiltInDrawIndex, spv::StorageClassInput)) { return "reads gl_DrawID."; } + for (auto varID : reflect.get_active_interface_variables()) { + if (reflect.get_storage_class(varID) != spv::StorageClassOutput) { continue; } + bool perPrimitive = reflect.has_decoration(varID, spv::DecorationPerPrimitiveEXT); + const auto& type = reflect.get_type(reflect.get_type_from_variable(varID).parent_type); + const auto& element = type.array.empty() ? type : reflect.get_type(type.parent_type); + for (uint32_t i = 0; i < element.member_types.size(); i++) { perPrimitive |= reflect.has_member_decoration(element.self, i, spv::DecorationPerPrimitiveEXT); } + if (perPrimitive) { return "writes per-primitive outputs."; } + } + // SPIRV-Cross reads a LocalSizeId operand as a plain constant and throws for a specialization constant operation, + // which Vulkan allows. Refuse it here: vkCreateGraphicsPipelines() must not let the exception through. + try { + workgroupSize = MTLSizeMake(reflect.get_execution_mode_argument(spv::ExecutionModeLocalSize, 0), + reflect.get_execution_mode_argument(spv::ExecutionModeLocalSize, 1), + reflect.get_execution_mode_argument(spv::ExecutionModeLocalSize, 2)); + } catch (const CompilerError&) { + return "sizes its workgroup with a LocalSizeId operand that is not a plain constant, such as a specialization constant operation."; + } + return {}; +} + +// First mesh path: a mesh and a fragment shader drawn with vkCmdDrawMeshTasksEXT, without task shader, mesh-stage +// resources or specialization, per-primitive outputs, pipeline cache, dynamic state or views. Fragment PerVertexKHR inputs +// read the corners that the mesh copies per primitive, in index order, for the forms getMeshPerVertexCornerLocations accepts. +// Anything else is refused at creation. The extension is exposed only to the test device adapter. +bool MVKGraphicsPipeline::initMeshPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, + const VkPipelineShaderStageCreateInfo* pTaskSS, + const VkPipelineShaderStageCreateInfo* pMeshSS, + VkPipelineCreationFeedback* pMeshFB, + const VkPipelineShaderStageCreateInfo* pFragmentSS, + VkPipelineCreationFeedback* pFragmentFB) { + auto unsupported = [&](const std::string& reason) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateGraphicsPipelines(): This mesh pipeline %s", reason.c_str())); + return false; + }; + if (!getEnabledMeshShaderFeatures().meshShader || ![getMTLDevice() supportsFamily:MTLGPUFamilyMetal3]) { return unsupported("needs the meshShader feature and a Metal 3 GPU."); } + if (pTaskSS) { return unsupported("uses a task shader."); } + if (!pMeshSS || !_meshModule || _meshModule->getSPIRV().empty() || !pFragmentSS || !_fragmentModule || pCreateInfo->stageCount != 2) { return unsupported("needs exactly a SPIR-V mesh and a fragment shader."); } + if (getPipelineCache() || !_dynamicStateFlags.empty() || getRenderingCreateInfo(pCreateInfo)->viewMask || !_isRasterizing || isDepthClipNegativeOneToOne(pCreateInfo)) { + return unsupported("uses a pipeline cache, dynamic state, views, rasterizer discard or a [-1, 1] depth range."); + } + if (mvkUsesResources(_meshModule->getSPIRV(), spv::ExecutionModelMeshEXT, pMeshSS->pName)) { return unsupported("uses mesh-stage descriptors or push constants."); } + // The draw dispatches the workgroup size of the unspecialized module. + if (pMeshSS->pSpecializationInfo) { return unsupported("specializes its mesh shader."); } + std::string reason = mvkMeshShaderIncompatibility(_meshModule->getSPIRV(), pMeshSS->pName, _meshThreadgroupSize); + if (!reason.empty()) { return unsupported("mesh shader " + reason); } + bool usesPerVertex = false; + std::string errorLog; + if (!mvk::getFragmentShaderUsesPerVertexInput(_fragmentModule->getSPIRV(), pFragmentSS->pName, usesPerVertex, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect PerVertexKHR inputs: %s", errorLog.c_str())); + return false; + } + uint32_t perVertexLocations = usesPerVertex ? mvk::getMeshPerVertexCornerLocations(_fragmentModule->getSPIRV(), pFragmentSS->pName) : 0; + if (usesPerVertex && !perVertexLocations) { + return unsupported("reads PerVertexKHR fragment inputs other than arrays of 16- or 32-bit scalars or vectors below Location 32."); + } + + SPIRVToMSLConversionConfiguration shaderConfig; + initShaderConversionConfig(shaderConfig, pCreateInfo, _tessReflectData); + // The mesh copies these per-vertex outputs per primitive, and the fragment reads its PerVertexKHR inputs from them. + shaderConfig.options.mslOptions.mesh_per_vertex_corner_locations = perVertexLocations; + shaderConfig.options.entryPointStage = spv::ExecutionModelMeshEXT; + shaderConfig.options.entryPointName = pMeshSS->pName; + MVKMTLFunction mesh = getMTLFunction(shaderConfig, pMeshSS, pMeshFB, _meshModule, "Mesh"); + if (!mesh.getMTLFunction()) { return false; } + const auto& meshResults = mesh.shaderConversionResults; + if (meshResults.needsSwizzleBuffer || meshResults.needsBufferSizeBuffer || meshResults.needsDynamicOffsetBuffer || meshResults.needsViewRangeBuffer || + meshResults.needsDrawId || meshResults.needsDispatchBaseBuffer || meshResults.needsOutputBuffer || meshResults.needsPatchOutputBuffer || + meshResults.usesPhysicalStorageBufferAddressesCapability) { + return unsupported("mesh shader needs an implicit buffer or device-address residency, which the mesh stage does not provide."); + } + SPIRVShaderOutputs meshOutputs; + if (!getShaderOutputs(_meshModule->getSPIRV(), spv::ExecutionModelMeshEXT, pMeshSS->pName, meshOutputs, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to get mesh outputs: %s", errorLog.c_str())); + return false; + } + + // The fragment side is built as for a vertex pipeline, then carried over to the mesh descriptor. + MTLRenderPipelineDescriptor* fragmentDesc = [MTLRenderPipelineDescriptor new]; // temp retain + bool compiled = addFragmentShaderToPipeline(fragmentDesc, pCreateInfo, shaderConfig, meshOutputs, pFragmentSS, pFragmentFB); + if (compiled) { addFragmentOutputToPipeline(fragmentDesc, pCreateInfo); } + MTLMeshRenderPipelineDescriptor* meshDesc = [MTLMeshRenderPipelineDescriptor new]; // temp retain + meshDesc.meshFunction = mesh.getMTLFunction(); + meshDesc.fragmentFunction = fragmentDesc.fragmentFunction; + for (NSUInteger i = 0; i < kMVKMaxColorAttachmentCount; i++) { meshDesc.colorAttachments[i] = fragmentDesc.colorAttachments[i]; } + meshDesc.depthAttachmentPixelFormat = fragmentDesc.depthAttachmentPixelFormat; + meshDesc.stencilAttachmentPixelFormat = fragmentDesc.stencilAttachmentPixelFormat; + meshDesc.rasterSampleCount = fragmentDesc.rasterSampleCount; + meshDesc.alphaToCoverageEnabled = fragmentDesc.alphaToCoverageEnabled; + meshDesc.alphaToOneEnabled = fragmentDesc.alphaToOneEnabled; + meshDesc.rasterizationEnabled = fragmentDesc.rasterizationEnabled; + setMetalObjectLabel(meshDesc, ((MVKPipelineLayout*)pCreateInfo->layout)->getDebugName()); + [fragmentDesc release]; + if (compiled) { + // Compiles synchronously; route through MVKRenderPipelineCompiler for timeouts once mesh leaves the test gate. + NSError* error = nil; + _mtlPipelineState = [getMTLDevice() newRenderPipelineStateWithMeshDescriptor:meshDesc options:MTLPipelineOptionNone reflection:nil error:&error]; // retained + if (!_mtlPipelineState) { setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Could not create the mesh pipeline: %s", error.localizedDescription.UTF8String)); } + } + [meshDesc release]; + // The created Metal pipeline reports what it executes: the mesh workgroup must fit its threadgroup limit, and draws + // are checked against its grid limit, beyond which Metal draws nothing and reports no error. + if (_mtlPipelineState) { + NSUInteger invocations = _meshThreadgroupSize.width * _meshThreadgroupSize.height * _meshThreadgroupSize.depth; + if (invocations > _mtlPipelineState.maxTotalThreadsPerMeshThreadgroup) { + NSUInteger limit = _mtlPipelineState.maxTotalThreadsPerMeshThreadgroup; + [_mtlPipelineState release]; + _mtlPipelineState = nil; + return unsupported("mesh workgroup of " + std::to_string(invocations) + " invocations exceeds the " + std::to_string(limit) + " its Metal pipeline supports."); + } + _maxMeshThreadgroupsPerGrid = _mtlPipelineState.maxTotalThreadgroupsPerMeshGrid; + } + return compiled && _mtlPipelineState; +} + +bool MVKGraphicsPipeline::initPerVertexInputPipeline(const VkGraphicsPipelineCreateInfo* pCreateInfo, const VkPipelineShaderStageCreateInfo* pVertexSS, const VkPipelineShaderStageCreateInfo* pFragmentSS) { + if (!_isRasterizing || !pFragmentSS || !_fragmentModule || _fragmentModule->getSPIRV().empty()) { return true; } + // Without the experimental path, barycentric inputs keep the native Metal path and PerVertexKHR stays unsupported. + if (!getPhysicalDevice()->isPortablePerVertexEnabled()) { return true; } + bool usesPerVertex = false; + std::string errorLog; + if (!mvk::getFragmentShaderUsesPerVertexInput(_fragmentModule->getSPIRV(), pFragmentSS->pName, usesPerVertex, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect PerVertexKHR inputs: %s", errorLog.c_str())); + return false; + } + _hasPerVertexInputs = usesPerVertex; + SPIRVShaderInputs inputs; + if (!getShaderInputs(_fragmentModule->getSPIRV(), spv::ExecutionModelFragment, pFragmentSS->pName, inputs, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect barycentric inputs: %s", errorLog.c_str())); + return false; + } + // Reflection includes explicit interpolation through copied pointers, but excludes inactive block members. + for (const auto& input : inputs) { + if (!input.isUsed) { continue; } + _usesPerspectiveBarycentrics |= input.builtin == spv::BuiltInBaryCoordKHR; + _usesNoPerspectiveBarycentrics |= input.builtin == spv::BuiltInBaryCoordNoPerspKHR; + } + if (!usesPerVertex && !usesPortableBarycentrics()) { return true; } + if (!_isTessellationPipeline && !usesPortableBarycentrics() && getPhysicalDevice()->getMTLDeviceCapabilities().supportsApple10 && getMetalFeatures().mslVersion >= CompilerMSL::Options::make_msl_version(4, 0) && getPrimitiveTopologyClass() == MTLPrimitiveTopologyClassTriangle) { return true; } + auto unsupported = [&](const char* reason) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR %s", reason)); + return false; + }; + bool testTES = false; +#ifdef MVK_TEST_TES_PERVERTEX_ADMISSION + // Test-only admission, never defined by shipping builds. The public extension gate stays closed. + if (_isTessellationPipeline) { + // The GPU generates the topology from the float32 levels the TCS writes; only uniform equal-spacing + // triangle levels whose Metal topology is proven are rendered (MVKCmdDraw.mm). Other modes refuse here. + const auto& tess = _tessReflectData; + if (tess.patchKind != spv::ExecutionModeTriangles || tess.partitionMode != spv::ExecutionModeSpacingEqual || tess.pointMode || + (tess.windingOrder != spv::ExecutionModeVertexOrderCw && tess.windingOrder != spv::ExecutionModeVertexOrderCcw) || tess.numControlPoints != 3) { + return unsupported("TES requires triangles, equal spacing, cw or ccw winding, no point mode and three output control points."); + } + const char* tessCtlEntry = nullptr; + const char* tessEvalEntry = nullptr; + for (uint32_t i = 0; i < pCreateInfo->stageCount; ++i) { + const auto& stage = pCreateInfo->pStages[i]; + if (stage.pSpecializationInfo || stage.pNext || stage.flags) { return unsupported("TES test requires unspecialized entry points without stage extensions or flags."); } + if (stage.stage == VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) { tessCtlEntry = stage.pName; } + if (stage.stage == VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) { tessEvalEntry = stage.pName; } + } + // The float32 TessLevel ABI excludes Metal argument buffers, so TCS and TES must not use descriptors. + // Push constants in these stages are not qualified either. Reflect the application's entry points. + if (mvkUsesResources(_tessCtlModule->getSPIRV(), spv::ExecutionModelTessellationControl, tessCtlEntry) || + mvkUsesResources(_tessEvalModule->getSPIRV(), spv::ExecutionModelTessellationEvaluation, tessEvalEntry)) { + return unsupported("TES requires TCS and TES stages without descriptors or push constants for the float32 tessellation level ABI."); + } + // Only VK_DYNAMIC_STATE_SCISSOR and VK_DYNAMIC_STATE_VIEWPORT are qualified: the render pass the replay restarts + // after the compute stages applies the current scissor and viewport. The WITH_COUNT variants set the same flags, + // so read the Vulkan list. + if (const auto* dynamic = pCreateInfo->pDynamicState) { + for (uint32_t i = 0; i < dynamic->dynamicStateCount; ++i) { + VkDynamicState state = dynamic->pDynamicStates[i]; + if (state != VK_DYNAMIC_STATE_SCISSOR && state != VK_DYNAMIC_STATE_VIEWPORT) { return unsupported("TES test admits no dynamic state other than VK_DYNAMIC_STATE_SCISSOR and VK_DYNAMIC_STATE_VIEWPORT."); } + } + } + // The domain origin is the only qualified tessellation state extension. A lower-left origin reverses the corner order + // of the generated triangles, as the ordinary path reverses the Metal winding of triangle patches. + if (pCreateInfo->pTessellationState) { + for (const auto* next = (const VkBaseInStructure*)pCreateInfo->pTessellationState->pNext; next; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_DOMAIN_ORIGIN_STATE_CREATE_INFO) { return unsupported("TES test admits no tessellation state extension other than the domain origin."); } + _perVertexTessLowerLeft = ((const VkPipelineTessellationDomainOriginStateCreateInfo*)next)->domainOrigin == VK_TESSELLATION_DOMAIN_ORIGIN_LOWER_LEFT; + } + } + const auto* viewports = pCreateInfo->pViewportState; + if (!getMetalFeatures().nonUniformThreadgroups || pCreateInfo->stageCount != 4 || !pCreateInfo->pTessellationState || pCreateInfo->pTessellationState->patchControlPoints != 3 || getRenderingCreateInfo(pCreateInfo)->viewMask || pCreateInfo->pMultisampleState->rasterizationSamples != VK_SAMPLE_COUNT_1_BIT || !pCreateInfo->pVertexInputState || pCreateInfo->pVertexInputState->pNext || + !viewports || viewports->viewportCount != 1 || viewports->scissorCount != 1) { + return unsupported("TES test requires single-view, single-sample triangle patches with three control points, no vertex divisors, and one viewport and scissor."); + } + testTES = true; + } +#endif + if (!pVertexSS || !_vertexModule || _vertexModule->getSPIRV().empty() || (!testTES && (_isTessellationPipeline || pCreateInfo->stageCount != 2))) { return unsupported("requires classic SPIR-V vertex and fragment stages."); } + if (getMetalFeatures().mslVersion < CompilerMSL::Options::make_msl_version(2, 4)) { return unsupported("requires MSL 2.4."); } + const auto* assembly = pCreateInfo->pInputAssemblyState; + if (!assembly || (testTES ? (assembly->topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST || assembly->primitiveRestartEnable) : !mvkPerVertexReplayVertexCount(assembly->topology))) { return unsupported("requires a point, line-list/strip, or triangle-list/strip/fan topology."); } + uint32_t viewMask = getRenderingCreateInfo(pCreateInfo)->viewMask; + if ((viewMask & (viewMask - 1)) || mvkIsAnyFlagEnabled(_flags, VK_PIPELINE_CREATE_2_VIEW_INDEX_FROM_DEVICE_INDEX_BIT)) { return unsupported("supports at most one multiview view and no device-index views."); } + if (pCreateInfo->pRasterizationState->polygonMode != VK_POLYGON_MODE_FILL || _dynamicStateFlags.has(MVKRenderStateFlag::PolygonMode) || _dynamicStateFlags.has(MVKRenderStateFlag::RasterizerDiscardEnable)) { return unsupported("requires static filled rasterization."); } + if (_dynamicStateFlags.has(MVKRenderStateFlag::ProvokingVertexMode)) { return unsupported("does not support dynamic provoking-vertex mode."); } + initReservedVertexAttributeBufferCount(pCreateInfo); + uint32_t captureParams = getImplicitBufferIndex(kMVKShaderStageVertex, 6); + if (!mvkPerVertexBufferIndexAvailable(captureParams, _descriptorBufferCounts.stages[kMVKShaderStageVertex], getMetalFeatures().maxPerStageBufferCount)) { return unsupported("has no free vertex capture parameter buffer slot."); } + if (viewMask && !mvkPerVertexBufferIndexAvailable(getImplicitBufferIndex(kMVKShaderStageVertex, 8), _descriptorBufferCounts.stages[kMVKShaderStageVertex], getMetalFeatures().maxPerStageBufferCount)) { return unsupported("has no free vertex view range buffer slot."); } + if (_hasPerVertexInputs) { + _perVertexInputBinding.vertex_buffer_index = getImplicitBufferIndex(kMVKShaderStageFragment, 4); + _perVertexInputBinding.primitive_index_buffer_index = getImplicitBufferIndex(kMVKShaderStageFragment, 5); + // After tessellation, a fragment PrimitiveId reads the patch index the topology generator stored per triangle. + if (testTES) { _perVertexInputBinding.primitive_id_buffer_index = getImplicitBufferIndex(kMVKShaderStageFragment, 6); } + if (!mvkPerVertexBufferIndexAvailable(testTES ? _perVertexInputBinding.primitive_id_buffer_index : _perVertexInputBinding.primitive_index_buffer_index, _descriptorBufferCounts.stages[kMVKShaderStageFragment], getMetalFeatures().maxPerStageBufferCount)) { return unsupported("has no free fragment capture buffer slots."); } + } + // Replay is generated directly from the capture ABI and is not yet a shader-library cache entry. + if (shouldFailOnPipelineCompileRequired()) { + setConfigurationResult(VK_PIPELINE_COMPILE_REQUIRED); + return false; + } + _usesPerVertexInputBuffer = true; + return true; +} + +bool MVKGraphicsPipeline::addPerVertexIndexedCapturePipelines(MTLVertexDescriptor* vertexDesc, const SPIRVToMSLConversionConfiguration& shaderConfig, const VkPipelineShaderStageCreateInfo* pVertexSS, uint32_t viewCount) { + auto captureConfig = shaderConfig; + const auto& implicit = getImplicitBuffers(kMVKShaderStageVertex).ids; + captureConfig.options.mslOptions.vertex_for_tessellation = true; + captureConfig.options.mslOptions.shader_index_buffer_index = implicit[MVKImplicitBuffer::Index]; + captureConfig.options.shouldFlipVertexY = false; + captureConfig.options.shouldFixupClipSpace = false; + MTLComputePipelineDescriptor* plDesc = [MTLComputePipelineDescriptor new]; + plDesc.stageInputDescriptor = [MTLStageInputOutputDescriptor stageInputOutputDescriptor]; + plDesc.stageInputDescriptor.indexBufferIndex = implicit[MVKImplicitBuffer::Index]; + // Reuse the translated vertex bindings without registering translations or zero divisors twice. + for (uint32_t i = 0; i < 31; i++) { + auto srcAttr = vertexDesc.attributes[i]; + auto dstAttr = plDesc.stageInputDescriptor.attributes[i]; + dstAttr.format = (MTLAttributeFormat)srcAttr.format; + dstAttr.offset = srcAttr.offset; + dstAttr.bufferIndex = srcAttr.bufferIndex; + auto srcLayout = vertexDesc.layouts[i]; + auto dstLayout = plDesc.stageInputDescriptor.layouts[i]; + dstLayout.stride = srcLayout.stride; + dstLayout.stepRate = srcLayout.stepRate; + switch (srcLayout.stepFunction) { + case MTLVertexStepFunctionPerVertex: dstLayout.stepFunction = MTLStepFunctionThreadPositionInGridXIndexed; break; + case MTLVertexStepFunctionPerInstance: dstLayout.stepFunction = MTLStepFunctionThreadPositionInGridY; break; + default: dstLayout.stepFunction = (MTLStepFunction)srcLayout.stepFunction; break; + } + } + MSLPerVertexInputBuffer expectedLayout = {true, _perVertexCapturedLayout, {}}; + bool success = true; + for (uint32_t i = 0; i < 2 && success; i++) { + captureConfig.options.mslOptions.vertex_index_type = i ? CompilerMSL::Options::IndexType::UInt32 : CompilerMSL::Options::IndexType::UInt16; + MVKMTLFunction func = getMTLFunction(captureConfig, pVertexSS, nullptr, _vertexModule, "Indexed PerVertexKHR capture"); + plDesc.computeFunction = func.getMTLFunction(); + if (!plDesc.computeFunction) { success = false; break; } + auto& results = func.shaderConversionResults; + MSLPerVertexInputBuffer actualLayout = {true, results.capturedVertexLayout, {}}; + if (!results.needsOutputBuffer || !expectedLayout.matches(actualLayout)) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR indexed capture does not match the replay vertex layout.")); + success = false; + break; + } + populateResourceUsage(_stageResources[kMVKShaderStageVertex], captureConfig, results, spv::ExecutionModelVertex); + plDesc.stageInputDescriptor.indexType = i ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16; + auto& states = _perVertexCapturePipelineStates[viewCount]; + success = !!getOrCompilePipeline(plDesc, i ? states.index32 : states.index16, "Indexed PerVertexKHR capture"); + } + [plDesc release]; + _stageResources[kMVKShaderStageVertex].implicitBuffers.needed.add(MVKImplicitBuffer::Index); + return success && verifyImplicitBuffers(kMVKShaderStageVertex); +} + +bool MVKGraphicsPipeline::addPerVertexReplayShaderToPipeline(MTLRenderPipelineDescriptor* plDesc, const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVToMSLConversionConfiguration& shaderConfig, const VkPipelineShaderStageCreateInfo* pVertexSS, const VkPipelineShaderStageCreateInfo* pFragmentSS, SPIRVShaderOutputs& vertexOutputs) { + SPIRVShaderInputs fragmentInputs; + std::string errorLog; + if (!getShaderInputs(_fragmentModule->getSPIRV(), spv::ExecutionModelFragment, pFragmentSS->pName, fragmentInputs, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect PerVertexKHR fragment interface: %s", errorLog.c_str())); + return false; + } + if (isRenderingPoints() && !std::any_of(vertexOutputs.begin(), vertexOutputs.end(), [](const auto& output) { return output.builtin == spv::BuiltInPointSize && output.isUsed; })) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable point replay requires an explicit PointSize output with the pinned compiler.")); + return false; + } + std::unordered_set locations; + for (const auto& output : vertexOutputs) { if (output.builtin == spv::BuiltInMax) { locations.insert(output.location); } } + for (const auto& input : fragmentInputs) { if (input.builtin == spv::BuiltInMax) { locations.insert(input.location); } } + for (const auto& component : _perVertexCapturedLayout.components) { locations.insert(component.location); } + auto userLocations = mvkPerVertexReplayUserLocations(fragmentInputs); + uint32_t maxComponents = std::min(getDeviceProperties().limits.maxFragmentInputComponents, getDeviceProperties().limits.maxVertexOutputComponents); + uint64_t replayComponents = mvkPerVertexReplayVaryingComponents(_perVertexCapturedLayout, _usesPerspectiveBarycentrics, _usesNoPerspectiveBarycentrics, userLocations); + if (replayComponents > maxComponents) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR replay requires %llu varying components including private key and basis; Metal budget is %u.", (unsigned long long)replayComponents, maxComponents)); + return false; + } + // Metal user(locnN) names are not Vulkan Locations. Allow three private names beyond + // the public range; a sparse interface can fill every Location and still fit Metal. + uint32_t maxLocations = maxComponents / 4 + 3; + auto allocateLocation = [&]() -> uint32_t { + return mvkAllocatePerVertexVaryingLocation(locations, maxLocations); + }; + _perVertexInputBinding.primitive_index_location = allocateLocation(); + if (_usesPerspectiveBarycentrics) { _perVertexReplayBarycentricBinding.perspective_location = allocateLocation(); } + if (_usesNoPerspectiveBarycentrics) { _perVertexReplayBarycentricBinding.no_perspective_location = allocateLocation(); } + if (_perVertexInputBinding.primitive_index_location == ~0u || (_usesPerspectiveBarycentrics && _perVertexReplayBarycentricBinding.perspective_location == ~0u) || (_usesNoPerspectiveBarycentrics && _perVertexReplayBarycentricBinding.no_perspective_location == ~0u)) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR/barycentrics have no free varying locations for the private key and basis.")); + return false; + } + _perVertexReplayBinding = {0, 1, 2, _perVertexInputBinding.primitive_index_location}; + if (usesPortableBarycentrics()) { _perVertexReplayBarycentricBinding.corner_buffer_index = 3; } + std::string replayMSL; + SPIRVToMSLConversionResultInfo replayResults = {}; +#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS + try { +#endif + auto executionModel = usesPerVertexTessEval() ? spv::ExecutionModelTessellationEvaluation : spv::ExecutionModelVertex; + CompilerMSL replay((usesPerVertexTessEval() ? _tessEvalModule : _vertexModule)->getSPIRV()); + replay.set_entry_point(pVertexSS->pName, executionModel); + auto options = shaderConfig.options.mslOptions; + options.capture_output_to_buffer = false; + options.tese_as_compute = false; + options.tessellation_factors_float32 = false; + options.vertex_for_tessellation = false; + options.multi_patch_workgroup = false; + options.disable_rasterization = false; + // Layered replay imports the captured synthetic Layer; single-view uses its attachment slice. + _perVertexReplayBinding.multiview_layer = options.multiview && options.multiview_layered_rendering; + options.multiview = _perVertexReplayBinding.multiview_layer; + options.multiview_layered_rendering = _perVertexReplayBinding.multiview_layer; + // PointSize is captured from the producer; the pinned replay API cannot synthesize it. + options.enable_point_size_default = false; + options.emulate_reversed_depth_viewport = getPhysicalDevice()->shouldEmulateReversedDepthViewport(); + options.reversed_depth_viewport_buffer_index = getImplicitBuffers(kMVKShaderStageVertex).ids[MVKImplicitBuffer::EmulatedReversedDepthViewport]; + replay.set_msl_options(options); + auto common = replay.get_common_options(); + common.vertex.flip_vert_y = shaderConfig.options.shouldFlipVertexY; + common.vertex.fixup_clipspace = shaderConfig.options.shouldFixupClipSpace; + replay.set_common_options(common); + for (const auto& output : shaderConfig.shaderOutputs) { replay.add_msl_shader_output(output.shaderVar); } + replayMSL = replay.compile_captured_output_replay(_perVertexCapturedLayout, _perVertexReplayBinding, _perVertexReplayBarycentricBinding, userLocations); + replayResults.entryPoint.mtlFunctionName = replay.get_cleansed_entry_point_name(pVertexSS->pName, executionModel); + replayResults.isPositionInvariant = replay.is_position_invariant(); +#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS + } catch (const CompilerError& error) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR replay is unsupported: %s", error.what())); + return false; + } +#endif + MVKShaderLibraryCompiler* compiler = new MVKShaderLibraryCompiler(this); + id library = compiler->newMTLLibrary(@(replayMSL.c_str()), replayResults, {}); + compiler->destroy(); + id function = [library newFunctionWithName:@(replayResults.entryPoint.mtlFunctionName.c_str())]; + [library release]; + if (!function) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR replay function could not be compiled.")); + return false; + } + // Match the active render pass, including sample count and attachment formats. + addFragmentOutputToPipeline(plDesc, pCreateInfo); + plDesc.fragmentFunction = nil; + plDesc.rasterizationEnabled = NO; + plDesc.alphaToCoverageEnabled = NO; + plDesc.alphaToOneEnabled = NO; + // Capture point/line and strip/fan vertices individually. This runs before the pipeline records its topology. + _perVertexCapturesTriangleLists = pCreateInfo->pInputAssemblyState->topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !_dynamicStateFlags.has(MVKRenderStateFlag::PrimitiveTopology); + if (!_perVertexCapturesTriangleLists) { plDesc.inputPrimitiveTopology = MTLPrimitiveTopologyClassPoint; } + if (usesPerVertexTessEval()) { + plDesc.inputPrimitiveTopology = getPrimitiveTopologyClass(); + plDesc.vertexFunction = function; + [function release]; + plDesc.vertexDescriptor = nil; + plDesc.rasterizationEnabled = YES; + return true; + } + bool captured = true; + uint32_t viewMask = getRenderingCreateInfo(pCreateInfo)->viewMask; + uint32_t passCount = viewMask ? getDevice()->getMultiviewMetalPassCount(viewMask) : 1; + uint32_t oldViewCount = 1; + for (uint32_t pass = 0; pass < passCount && captured; ++pass) { + uint32_t viewCount = viewMask ? getDevice()->getViewCountInMetalPass(viewMask, pass) : 1; + if (_perVertexCapturePipelineStates.count(viewCount)) { continue; } + adjustVertexInputForMultiview(plDesc.vertexDescriptor, pCreateInfo->pVertexInputState, viewCount, oldViewCount); + oldViewCount = viewCount; + for (uint32_t binding = 0; binding < 31; ++binding) { + if (plDesc.vertexDescriptor.layouts[binding].stepRate > UINT32_MAX) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR view-expanded vertex divisor exceeds uint32.")); + captured = false; + break; + } + } + if (!captured) { break; } + captured = addPerVertexIndexedCapturePipelines(plDesc.vertexDescriptor, shaderConfig, pVertexSS, viewCount); + if (captured) { captured = !!getOrCompilePipeline(plDesc, _perVertexCapturePipelineStates[viewCount].direct); } + } + plDesc.inputPrimitiveTopology = getPrimitiveTopologyClass(); + plDesc.vertexFunction = function; + [function release]; + plDesc.vertexDescriptor = nil; + plDesc.rasterizationEnabled = YES; + return captured; +} + // Returns a retained MTLRenderPipelineDescriptor constructed from this instance, or nil if an error occurs. // It is the responsibility of the caller to release the returned descriptor. MTLRenderPipelineDescriptor* MVKGraphicsPipeline::newMTLRenderPipelineDescriptor(const VkGraphicsPipelineCreateInfo* pCreateInfo, @@ -1186,18 +1675,24 @@ static bool usesConstantColor(MVKRenderStateFlags dynamic, const VkPipelineColor std::string errorLog; if (!getShaderOutputs(_vertexModule->getSPIRV(), spv::ExecutionModelVertex, pVertexSS->pName, vtxOutputs, errorLog) ) { setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to get vertex outputs: %s", errorLog.c_str())); + [plDesc release]; return nil; } // Add shader stages. Compile vertex shader before others just in case conversion changes anything...like rasterizaion disable. - if (!addVertexShaderToPipeline(plDesc, pCreateInfo, shaderConfig, pVertexSS, pVertexFB, pFragmentSS)) { return nil; } + if (!addVertexShaderToPipeline(plDesc, pCreateInfo, shaderConfig, pVertexSS, pVertexFB, pFragmentSS)) { [plDesc release]; return nil; } // Vertex input // This needs to happen before compiling the fragment shader, or we'll lose information on vertex attributes. - if (!addVertexInputToPipeline(plDesc.vertexDescriptor, pCreateInfo->pVertexInputState, shaderConfig)) { return nil; } + if (!addVertexInputToPipeline(plDesc.vertexDescriptor, pCreateInfo->pVertexInputState, shaderConfig)) { [plDesc release]; return nil; } + + if (_usesPerVertexInputBuffer && !addPerVertexReplayShaderToPipeline(plDesc, pCreateInfo, shaderConfig, pVertexSS, pFragmentSS, vtxOutputs)) { + [plDesc release]; + return nil; + } // Fragment shader - only add if rasterization is enabled - if (!addFragmentShaderToPipeline(plDesc, pCreateInfo, shaderConfig, vtxOutputs, pFragmentSS, pFragmentFB)) { return nil; } + if (!addFragmentShaderToPipeline(plDesc, pCreateInfo, shaderConfig, vtxOutputs, pFragmentSS, pFragmentFB)) { [plDesc release]; return nil; } // Output addFragmentOutputToPipeline(plDesc, pCreateInfo); @@ -1377,7 +1872,7 @@ static MTLVertexFormat mvkAdjustFormatVectorToSize(MTLVertexFormat format, uint3 } // Tessellation state - addTessellationToPipeline(plDesc, reflectData, pCreateInfo->pTessellationState); + if (!usesPerVertexTessEval()) { addTessellationToPipeline(plDesc, reflectData, pCreateInfo->pTessellationState); } // Output addFragmentOutputToPipeline(plDesc, pCreateInfo); @@ -1454,6 +1949,15 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, "Tessellation evaluation", "Fragment" }; + if (_usesPerVertexInputBuffer) { + const auto& buffers = _stageResources[stage].implicitBuffers; + for (MVKImplicitBuffer needed : buffers.needed.removingAll(MVKImplicitBuffer::PushConstant)) { + if (!mvkPerVertexBufferIndexAvailable(buffers.ids[needed], _descriptorBufferCounts.stages[stage], getMetalFeatures().maxPerStageBufferCount)) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR %s shader has no valid slot for its %s buffer.", stageNames[stage], getImplicitBufferName(needed))); + return false; + } + } + } return ::verifyImplicitBuffers(_stageResources[stage].implicitBuffers, stageNames[stage], _descriptorBufferCounts.stages[stage], this); } @@ -1472,13 +1976,26 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, shaderConfig.options.mslOptions.shader_output_buffer_index = implicit[MVKImplicitBuffer::Output]; shaderConfig.options.mslOptions.view_mask_buffer_index = implicit[MVKImplicitBuffer::ViewRange]; shaderConfig.options.mslOptions.draw_id_buffer_index = implicit[MVKImplicitBuffer::DrawId]; - shaderConfig.options.mslOptions.capture_output_to_buffer = false; - shaderConfig.options.mslOptions.disable_rasterization = !_isRasterizing; - setEmulatedReversedDepthViewportConfig(shaderConfig, implicit, getPhysicalDevice()->shouldEmulateReversedDepthViewport()); + shaderConfig.options.mslOptions.capture_output_to_buffer = _usesPerVertexInputBuffer; + shaderConfig.options.mslOptions.disable_rasterization = _usesPerVertexInputBuffer || !_isRasterizing; + shaderConfig.exportCapturedVertexLayout = _usesPerVertexInputBuffer; + if (_usesPerVertexInputBuffer) { + shaderConfig.options.mslOptions.indirect_params_buffer_index = implicit[MVKImplicitBuffer::IndirectParams]; + } + setEmulatedReversedDepthViewportConfig(shaderConfig, implicit, !_usesPerVertexInputBuffer && getPhysicalDevice()->shouldEmulateReversedDepthViewport()); setDepthClipConfig(shaderConfig, implicit, _isRasterizing && isPossibleBothDepthClipClamp(_dynamicStateFlags, _staticStateData, false)); addVertexInputToShaderConversionConfig(shaderConfig, pCreateInfo); + // Capture raw Vulkan outputs; replay applies reversed-depth, clip-space and Y fixups exactly once. + bool flipVertexY = shaderConfig.options.shouldFlipVertexY; + bool fixupClipSpace = shaderConfig.options.shouldFixupClipSpace; + if (_usesPerVertexInputBuffer) { + shaderConfig.options.shouldFlipVertexY = false; + shaderConfig.options.shouldFixupClipSpace = false; + } MVKMTLFunction func = getMTLFunction(shaderConfig, pVertexSS, pVertexFB, _vertexModule, "Vertex"); + shaderConfig.options.shouldFlipVertexY = flipVertexY; + shaderConfig.options.shouldFixupClipSpace = fixupClipSpace; id mtlFunc = func.getMTLFunction(); plDesc.vertexFunction = mtlFunc; if ( !mtlFunc ) { return false; } @@ -1486,10 +2003,18 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, auto& funcRslts = func.shaderConversionResults; plDesc.rasterizationEnabled = !funcRslts.isRasterizationDisabled; populateResourceUsage(_stageResources[kMVKShaderStageVertex], shaderConfig, funcRslts, spv::ExecutionModelVertex); - _stageResources[kMVKShaderStageVertex].implicitBuffers.needed.set(MVKImplicitBuffer::EmulatedReversedDepthViewport, shaderConfig.options.mslOptions.emulate_reversed_depth_viewport); + // The reversed-depth mask is needed by replay even when capture leaves depth unmodified. + _stageResources[kMVKShaderStageVertex].implicitBuffers.needed.set(MVKImplicitBuffer::EmulatedReversedDepthViewport, getPhysicalDevice()->shouldEmulateReversedDepthViewport()); _layout->populateBindOperations(_stageResources[kMVKShaderStageVertex].bindScript, shaderConfig, spv::ExecutionModelVertex); - if (funcRslts.isRasterizationDisabled) { + if (_usesPerVertexInputBuffer) { + _perVertexCapturedLayout = funcRslts.capturedVertexLayout; + _stageResources[kMVKShaderStageVertex].implicitBuffers.needed.add(MVKImplicitBuffer::IndirectParams); + if (!_perVertexCapturedLayout.stride || !funcRslts.needsOutputBuffer) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Portable PerVertexKHR requires an exported vertex capture layout.")); + return false; + } + } else if (funcRslts.isRasterizationDisabled) { pFragmentSS = nullptr; } @@ -1566,8 +2091,20 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, setDepthClipConfig(shaderConfig, implicit, false); addPrevStageOutputToShaderConversionConfig(shaderConfig, vtxOutputs); addNextStageInputToShaderConversionConfig(shaderConfig, teInputs); + // The GPU topology generator reads the levels exactly as written; Metal's half factors would round + // nextafter(1, +inf) down to level 1. Admission guarantees a descriptor-free TCS. Later stages keep + // the pipeline's own options. + bool argumentBuffers = shaderConfig.options.mslOptions.argument_buffers; + if (usesPerVertexTessEval()) { + shaderConfig.options.mslOptions.tessellation_factors_float32 = true; + shaderConfig.options.mslOptions.argument_buffers = false; + } + // The ordinary path classifies the float32 levels before Metal reads them as half factors. + if (usesFloat32TessLevels()) { shaderConfig.options.mslOptions.tessellation_factors_float32 = true; } MVKMTLFunction func = getMTLFunction(shaderConfig, pTessCtlSS, pTessCtlFB, _tessCtlModule, "Tessellation control"); + shaderConfig.options.mslOptions.tessellation_factors_float32 = false; + shaderConfig.options.mslOptions.argument_buffers = argumentBuffers; id mtlFunc = func.getMTLFunction(); if ( !mtlFunc ) { return false; } plDesc.computeFunction = mtlFunc; @@ -1604,7 +2141,53 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, setDepthClipConfig(shaderConfig, implicit, _isRasterizing && isPossibleBothDepthClipClamp(_dynamicStateFlags, _staticStateData, false)); addPrevStageOutputToShaderConversionConfig(shaderConfig, tcOutputs); + if (usesPerVertexTessEval()) { + auto captureConfig = shaderConfig; + auto& options = captureConfig.options.mslOptions; + options.tese_as_compute = true; + options.capture_output_to_buffer = true; + options.disable_rasterization = true; + options.vertex_for_tessellation = false; + options.multi_patch_workgroup = false; + options.argument_buffers = false; // Admission guarantees a descriptor-free TES. + options.tessellation_factors_float32 = true; + options.emulate_reversed_depth_viewport = false; + // For triangles, the lower-left origin changes no TES coordinate; the topology generator reverses the corner order. + options.tess_domain_origin_lower_left = false; + options.indirect_params_buffer_index = implicit[MVKImplicitBuffer::IndirectParams]; + options.shader_output_buffer_index = implicit[MVKImplicitBuffer::Output]; + captureConfig.options.shouldFlipVertexY = false; + captureConfig.options.shouldFixupClipSpace = false; + captureConfig.exportCapturedVertexLayout = true; + MVKMTLFunction capture = getMTLFunction(captureConfig, pTessEvalSS, pTessEvalFB, _tessEvalModule, "PerVertex TES capture"); + if (!capture.getMTLFunction()) { return false; } + auto& results = capture.shaderConversionResults; + _perVertexCapturedLayout = results.capturedVertexLayout; + if (!_perVertexCapturedLayout.stride || !results.needsOutputBuffer) { + setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "PerVertex TES capture requires an exported output layout.")); + return false; + } + populateResourceUsage(_stageResources[kMVKShaderStageTessEval], captureConfig, results, spv::ExecutionModelTessellationEvaluation); + _stageResources[kMVKShaderStageTessEval].implicitBuffers.needed.add(MVKImplicitBuffer::IndirectParams); + _layout->populateBindOperations(_stageResources[kMVKShaderStageTessEval].bindScript, captureConfig, spv::ExecutionModelTessellationEvaluation); + if (!verifyImplicitBuffers(kMVKShaderStageTessEval)) { return false; } + auto* descriptor = [MTLComputePipelineDescriptor new]; + descriptor.computeFunction = capture.getMTLFunction(); + bool compiled = !!getOrCompilePipeline(descriptor, _mtlPerVertexTessEvalState, "PerVertex TES capture"); + [descriptor release]; + if (!compiled) { return false; } + SPIRVShaderOutputs outputs; + std::string error; + if (!getShaderOutputs(_tessEvalModule->getSPIRV(), spv::ExecutionModelTessellationEvaluation, pTessEvalSS->pName, outputs, error)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect TES replay outputs: %s", error.c_str())); + return false; + } + return addPerVertexReplayShaderToPipeline(plDesc, pCreateInfo, shaderConfig, pTessEvalSS, pFragmentSS, outputs); + } + // Vulkan TES levels are the values the TCS wrote, so the TES reads the float32 levels, not the half factors. + shaderConfig.options.mslOptions.tessellation_factors_float32 = usesFloat32TessLevels(); MVKMTLFunction func = getMTLFunction(shaderConfig, pTessEvalSS, pTessEvalFB, _tessEvalModule, "Tessellation evaluation"); + shaderConfig.options.mslOptions.tessellation_factors_float32 = false; id mtlFunc = func.getMTLFunction(); plDesc.vertexFunction = mtlFunc; // Yeah, you read that right. Tess. eval functions are a kind of vertex function in Metal. if ( !mtlFunc ) { return false; } @@ -1632,6 +2215,14 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, auto& mtlFeats = getMetalFeatures(); if (pFragmentSS) { shaderConfig.options.entryPointStage = spv::ExecutionModelFragment; + // vertex_value requires triangle input and Apple10 hardware, not just a recent OS/MSL version. + shaderConfig.options.mslOptions.supports_per_vertex_fragment_input = getPhysicalDevice()->getMTLDeviceCapabilities().supportsApple10 && shaderConfig.options.mslOptions.supports_msl_version(4, 0) && getPrimitiveTopologyClass() == MTLPrimitiveTopologyClassTriangle; + if (_usesPerVertexInputBuffer) { + shaderConfig.options.mslOptions.supports_per_vertex_fragment_input = false; + shaderConfig.options.mslOptions.disable_rasterization = false; + if (_hasPerVertexInputs) { shaderConfig.setPerVertexInputBuffer(_perVertexCapturedLayout, _perVertexInputBinding); } + if (usesPortableBarycentrics()) { shaderConfig.setFragmentBarycentricInput({_perVertexReplayBarycentricBinding.perspective_location, _perVertexReplayBarycentricBinding.no_perspective_location}); } + } addCommonImplicitBuffersToShaderConfig(shaderConfig, implicit); shaderConfig.options.mslOptions.view_mask_buffer_index = implicit[MVKImplicitBuffer::ViewRange]; shaderConfig.options.entryPointName = pFragmentSS->pName; @@ -1666,6 +2257,20 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, shaderConfig.options.mslOptions.arrayed_subpass_input = true; } addPrevStageOutputToShaderConversionConfig(shaderConfig, shaderOutputs); + if (_usesPerVertexInputBuffer) { + SPIRVShaderInputs fragmentInputs; + std::string errorLog; + if (!getShaderInputs(_fragmentModule->getSPIRV(), spv::ExecutionModelFragment, pFragmentSS->pName, fragmentInputs, errorLog)) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Failed to reflect portable PerVertexKHR input remaps: %s", errorLog.c_str())); + return false; + } + for (const auto& fragmentInput : fragmentInputs) { + if (!fragmentInput.perVertex) { continue; } + // Captured leaves use their physical producer layout. Ordinary components keep their remaps. + auto& inputs = shaderConfig.shaderInputs; + inputs.erase(std::remove_if(inputs.begin(), inputs.end(), [&](const mvk::MSLShaderInput& input) { return input.shaderVar.builtin == spv::BuiltInMax && input.shaderVar.location == fragmentInput.location && input.shaderVar.component == fragmentInput.component; }), inputs.end()); + } + } MVKMTLFunction func = getMTLFunction(shaderConfig, pFragmentSS, pFragmentFB, _fragmentModule, "Fragment"); id mtlFunc = func.getMTLFunction(); @@ -1673,6 +2278,10 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, if ( !mtlFunc ) { return false; } auto& funcRslts = func.shaderConversionResults; + if (_usesPerVertexInputBuffer && _hasPerVertexInputs && !funcRslts.needsPerVertexInputBuffer) { + setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "Portable PerVertexKHR fragment shader did not consume its captured input buffer.")); + return false; + } populateResourceUsage(_stageResources[kMVKShaderStageFragment], shaderConfig, funcRslts, spv::ExecutionModelFragment); _layout->populateBindOperations(_stageResources[kMVKShaderStageFragment].bindScript, shaderConfig, spv::ExecutionModelFragment); } @@ -2066,6 +2675,11 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::ViewRange] = extra; _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::Index] = extra; _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::DrawId] = getImplicitBufferIndex(stage, 5); + if (_usesPerVertexInputBuffer) { + _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::IndirectParams] = getImplicitBufferIndex(stage, 6); + // Indexed compute capture needs both the index buffer and the single-view range. + if (getRenderingCreateInfo(pCreateInfo)->viewMask) { _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::ViewRange] = getImplicitBufferIndex(stage, 8); } + } break; case kMVKShaderStageFragment: _stageResources[stage].implicitBuffers.ids[MVKImplicitBuffer::ViewRange] = extra; @@ -2112,7 +2726,8 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, shaderConfig.options.shouldFixupClipSpace = isDepthClipNegativeOneToOne(pCreateInfo); shaderConfig.options.mslOptions.tess_domain_origin_lower_left = pTessDomainOriginState && pTessDomainOriginState->domainOrigin == VK_TESSELLATION_DOMAIN_ORIGIN_LOWER_LEFT; shaderConfig.options.mslOptions.multiview = mvkIsMultiview(pRendInfo->viewMask); - shaderConfig.options.mslOptions.multiview_layered_rendering = getPhysicalDevice()->canUseInstancingForMultiview(); + // Keep the admitted single-view ABI; the closed multibit path uses captured-layer replay. + shaderConfig.options.mslOptions.multiview_layered_rendering = getPhysicalDevice()->canUseInstancingForMultiview() && (!_usesPerVertexInputBuffer || (pRendInfo->viewMask & (pRendInfo->viewMask - 1))); shaderConfig.options.mslOptions.view_index_from_device_index = mvkAreAllFlagsEnabled(_flags, VK_PIPELINE_CREATE_2_VIEW_INDEX_FROM_DEVICE_INDEX_BIT); shaderConfig.options.mslOptions.replace_recursive_inputs = mvkOSVersionIsAtLeast(14.0, 17.0, 1.0); #if MVK_MACOS @@ -2327,11 +2942,19 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, [_mtlTessVertexStageIndex16State release]; [_mtlTessVertexStageIndex32State release]; [_mtlTessControlStageState release]; + [_mtlTessLevelsToHalfFactorsState release]; + [_mtlPerVertexTessEvalState release]; + for (auto& entry : _perVertexCapturePipelineStates) { + [entry.second.direct release]; + [entry.second.index16 release]; + [entry.second.index32 release]; + } [_mtlPipelineState release]; if (_ownsVertexModule) delete _vertexModule; if (_ownsTessCtlModule) delete _tessCtlModule; if (_ownsTessEvalModule) delete _tessEvalModule; if (_ownsFragmentModule) delete _fragmentModule; + if (_ownsMeshModule) delete _meshModule; } } @@ -2538,6 +3161,9 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, #if MVK_USE_CEREAL static uint32_t kDataHeaderSize = (sizeof(uint32_t) * 4) + VK_UUID_SIZE; +// Version the private payload independently of the Vulkan header/driver UUID. Old readers +// stop at this unknown entry type; new readers reject unversioned caches before reading options. +static constexpr uint32_t kMVKPipelineCacheSchema = 0x4D564B03; // Includes TES compute, float32 factors and mesh PerVertexKHR corners. #endif // Entry type markers to be inserted into data stream @@ -2635,6 +3261,7 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, writer(NSSwapHostIntToLittle(devProps.vendorID)); writer(NSSwapHostIntToLittle(devProps.deviceID)); writer(devProps.pipelineCacheUUID); + writer(kMVKPipelineCacheSchema); // Shader libraries // Output a cache entry for each shader library, including the shader module key in each entry. @@ -2670,8 +3297,8 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, size_t byteCount = pCreateInfo->initialDataSize; uint32_t cacheEntryType; - // Must be able to read the header and at least one cache entry type. - if (byteCount < kDataHeaderSize + sizeof(cacheEntryType)) { return; } + // Must be able to read the header, schema, and at least one cache entry type. + if (byteCount < kDataHeaderSize + sizeof(uint32_t) + sizeof(cacheEntryType)) { return; } mvk::membuf mb((char*)pCreateInfo->pInitialData, byteCount); istream inStream(&mb); @@ -2697,6 +3324,10 @@ static void setDepthClipConfig(SPIRVToMSLConversionConfiguration& shaderConfig, reader(pcUUID); // Pipeline cache UUID if ( !mvkAreEqual(pcUUID, dvcProps.pipelineCacheUUID, VK_UUID_SIZE) ) { return; } + uint32_t schema; + reader(schema); + if (schema != kMVKPipelineCacheSchema) { return; } + bool done = false; while ( !done ) { reader(cacheEntryType); @@ -2795,6 +3426,7 @@ void serialize(Archive & archive, CompilerMSL::Options& opt) { opt.device_index, opt.enable_frag_output_mask, opt.additional_fixed_sample_mask, + opt.mesh_per_vertex_corner_locations, opt.enable_point_size_builtin, opt.enable_point_size_default, opt.default_point_size, @@ -2802,6 +3434,7 @@ void serialize(Archive & archive, CompilerMSL::Options& opt) { opt.enable_frag_stencil_ref_builtin, opt.disable_rasterization, opt.capture_output_to_buffer, + opt.supports_per_vertex_fragment_input, opt.swizzle_texture_samples, opt.tess_domain_origin_lower_left, opt.multiview, @@ -2828,6 +3461,8 @@ void serialize(Archive & archive, CompilerMSL::Options& opt) { opt.enable_clip_distance_user_varying, opt.multi_patch_workgroup, opt.raw_buffer_tese_input, + opt.tessellation_factors_float32, + opt.tese_as_compute, opt.vertex_for_tessellation, opt.arrayed_subpass_input, opt.ios_use_simdgroup_functions, @@ -2959,7 +3594,10 @@ void serialize(Archive & archive, SPIRVToMSLConversionConfiguration& cfg) { cfg.shaderOutputs, cfg.resourceBindings, cfg.discreteDescriptorSets, - cfg.dynamicBufferDescriptors); + cfg.dynamicBufferDescriptors, + cfg.perVertexInputBuffer, + cfg.fragmentBarycentricInput, + cfg.exportCapturedVertexLayout); } template @@ -2978,7 +3616,9 @@ void serialize(Archive & archive, SPIRVToMSLConversionResultInfo& scr) { scr.needsViewRangeBuffer, scr.needsDrawId, scr.needsDepthClipStateBuffer, - scr.usesPhysicalStorageBufferAddressesCapability); + scr.usesPhysicalStorageBufferAddressesCapability, + scr.needsPerVertexInputBuffer, + scr.capturedVertexLayout); } template @@ -3124,18 +3764,18 @@ static size_t mvkValidateCerealArchiveSize(size_t padByteCnt = 0) { void mvkValidateCeralArchiveDefinitions() { [[maybe_unused]] size_t missingBytes = 0; - missingBytes += mvkValidateCerealArchiveSize(6); + missingBytes += mvkValidateCerealArchiveSize(7); missingBytes += mvkValidateCerealArchiveSize(); missingBytes += mvkValidateCerealArchiveSize(); missingBytes += mvkValidateCerealArchiveSize(); missingBytes += mvkValidateCerealArchiveSize(3); missingBytes += mvkValidateCerealArchiveSize(20); // Contains string - missingBytes += mvkValidateCerealArchiveSize(24); // Contains string + missingBytes += mvkValidateCerealArchiveSize(25); // Contains string missingBytes += mvkValidateCerealArchiveSize(3); missingBytes += mvkValidateCerealArchiveSize(2); missingBytes += mvkValidateCerealArchiveSize(); - missingBytes += mvkValidateCerealArchiveSize(104); // Contains collection - missingBytes += mvkValidateCerealArchiveSize(39); // Contains collection + missingBytes += mvkValidateCerealArchiveSize(154); // Contains collection + missingBytes += mvkValidateCerealArchiveSize(74); // Contains collection missingBytes += mvkValidateCerealArchiveSize(22); // Contains string missingBytes += mvkValidateCerealArchiveSize(); missingBytes += mvkValidateCerealArchiveSize>(20); // Contains collection diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKQueryPool.mm b/MoltenVK/MoltenVK/GPUObjects/MVKQueryPool.mm index e5d4baeb9..d8a4ef722 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKQueryPool.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKQueryPool.mm @@ -83,9 +83,10 @@ uint32_t endQuery = firstQuery + queryCount; if (mvkAreAllFlagsEnabled(flags, VK_QUERY_RESULT_WAIT_BIT)) { - _availabilityBlocker.wait(lock, [this, firstQuery, endQuery]{ - return areQueriesHostAvailable(firstQuery, endQuery); - }); + // A reported device loss makes the queries host-available, but nothing notifies this wait of it, and a query + // whose command buffer the loss refused is never finished. Wait in slices, so that the predicate sees the loss. + auto isAvailable = [this, firstQuery, endQuery]{ return areQueriesHostAvailable(firstQuery, endQuery); }; + while ( !_availabilityBlocker.wait_for(lock, chrono::milliseconds(1), isAvailable) ) {} } VkResult rqstRslt = VK_SUCCESS; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h index b18549c7d..0300c0068 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h @@ -23,6 +23,8 @@ #include "MVKImage.h" #include "MVKSync.h" #include "MVKSmallVector.h" +#include +#include #include #include @@ -30,6 +32,7 @@ class MVKQueue; class MVKQueueSubmission; +class MVKQueueCommandBufferSubmission; class MVKPhysicalDevice; class MVKGPUCaptureScope; @@ -145,12 +148,29 @@ class MVKQueue : public MVKDispatchableVulkanAPIObject, public MVKDeviceTracking NSString* getMTLCommandBufferLabel(MVKCommandUse cmdUse); void handleMTLCommandBufferError(id mtlCmdBuff); + /** + * Returns whether a Metal command buffer has failed in a way that loses the device: any failure, unless + * MVKConfiguration::resumeLostDevice lets a failure local to that command buffer resume the device. + */ + bool isDeviceLoss(id mtlCmdBuff); + + /** + * Publishes a command buffer submission whose Metal work has completed, then the later ones already complete, in + * the order they were submitted. A fence or semaphore signal covers every earlier submission to its queue: it is + * never reported before the failure of an earlier submission is known. + */ + void publishInOrder(MVKQueueCommandBufferSubmission* submission); + MVKQueueFamily* _queueFamily; std::string _name; dispatch_queue_t _execQueue; + dispatch_queue_t _workerQueue = nullptr; std::mutex _execQueueMutex; std::condition_variable _execQueueConditionVariable; - uint32_t _execQueueJobCount = 0; + std::atomic _execQueueJobCount{0}; + std::mutex _publicationLock; + std::deque _unpublishedSubmissions; + bool _isPublishing = false; id _mtlQueue = nil; NSString* _mtlCmdBuffLabelBeginCommandBuffer = nil; NSString* _mtlCmdBuffLabelQueueSubmit = nil; @@ -173,6 +193,9 @@ class MVKQueue : public MVKDispatchableVulkanAPIObject, public MVKDeviceTracking typedef struct MVKSemaphoreSubmitInfo { private: MVKSemaphore* _semaphore; + uint64_t _reservation = 0; + bool _isReserved = false; + bool isReservable(); public: uint64_t value; VkPipelineStageFlags2 stageMask; @@ -180,6 +203,8 @@ typedef struct MVKSemaphoreSubmitInfo { void encodeWait(id mtlCmdBuff); void encodeSignal(id mtlCmdBuff); + void completeSignal(); + void reserveWait(); MVKSemaphoreSubmitInfo(const VkSemaphoreSubmitInfo& semaphoreSubmitInfo); MVKSemaphoreSubmitInfo(const VkSemaphore semaphore, VkPipelineStageFlags stageMask); MVKSemaphoreSubmitInfo(const MVKSemaphoreSubmitInfo& other); @@ -203,6 +228,18 @@ class MVKQueueSubmission : public MVKBaseDeviceObject, public MVKConfigurableMix */ virtual VkResult execute() = 0; + /** + * Returns whether executing this submission waits for its own Metal work to complete. Such a submission, + * and every later one to its queue, is executed in order on a worker instead of the submitting thread. + */ + virtual bool needsWorker() { return false; } + + /** Reserves, in submission order, the semaphore values that a deferred execution encodes. */ + virtual void reserveSemaphores() {} + + /** Takes its place, in submission order, among the submissions its queue publishes in order. */ + virtual void queueForPublication() {} + MVKQueueSubmission(MVKQueue* queue, uint32_t waitSemaphoreInfoCount, const VkSemaphoreSubmitInfo* pWaitSemaphoreSubmitInfos); @@ -232,6 +269,7 @@ class MVKQueueSubmission : public MVKBaseDeviceObject, public MVKConfigurableMix typedef struct MVKCommandBufferSubmitInfo { MVKCommandBuffer* commandBuffer; uint32_t deviceMask; + MVKPerVertexScratchReservations perVertexScratch; MVKCommandBufferSubmitInfo(const VkCommandBufferSubmitInfo& commandBufferInfo); MVKCommandBufferSubmitInfo(VkCommandBuffer commandBuffer); @@ -246,6 +284,17 @@ class MVKQueueCommandBufferSubmission : public MVKQueueSubmission { public: VkResult execute() override; + bool needsWorker() override { return _needsContinuation; } + void reserveSemaphores() override; + void queueForPublication() override; + virtual VkResult reservePerVertexScratch(std::unordered_set& prefilledExecutions) { return getConfigurationResult(); } + + /** + * Commits the Metal command buffer encoded so far, waits until it and its completion handlers finish, then makes + * a new Metal command buffer active for the rest of this submission, and returns it. Returns nil if the committed + * work failed, the device is being lost, or no Metal command buffer is available. + */ + id continueOnNewMTLCommandBuffer(); MVKQueueCommandBufferSubmission(MVKQueue* queue, const VkSubmitInfo2* pSubmit, @@ -261,18 +310,25 @@ class MVKQueueCommandBufferSubmission : public MVKQueueSubmission { protected: friend MVKCommandBuffer; + friend MVKQueue; id getActiveMTLCommandBuffer(); void setActiveMTLCommandBuffer(id mtlCmdBuff); VkResult commitActiveMTLCommandBuffer(bool signalCompletion = false); + void releaseMTLCommandBuffer(); void finish() override; virtual void submitCommandBuffers() {} + // Set by scratch preflight: encoding waits on the GPU for a PerVertexKHR indirect plan or TES topology. + bool _needsContinuation = false; MVKCommandEncodingContext _encodingContext; MVKSmallVector _signalSemaphores; MVKFence* _fence = nullptr; id _activeMTLCommandBuffer = nil; MVKCommandUse _commandUse = kMVKCommandUseNone; + std::atomic _pendingMTLCommandBuffers{1}; // Committed Metal command buffers not done, plus encoding. + std::atomic _isMTLCommandBufferLost{false}; + bool _isComplete = false; // Guarded by the queue's publication lock. bool _emulatedWaitDone = false; //Used to track if we've already waited for emulated semaphores. }; @@ -285,6 +341,7 @@ template class MVKQueueFullCommandBufferSubmission : public MVKQueueCommandBufferSubmission { public: + VkResult reservePerVertexScratch(std::unordered_set& prefilledExecutions) override; MVKQueueFullCommandBufferSubmission(MVKQueue* queue, const VkSubmitInfo2* pSubmit, VkFence fence, @@ -319,4 +376,3 @@ class MVKQueuePresentSurfaceSubmission : public MVKQueueSubmission { MVKSmallVector _presentInfo; }; - diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm index dec4b452a..820d3fe06 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm @@ -18,6 +18,7 @@ #include "MVKInstance.h" #include "MVKQueue.h" +#include "MVKSubmissionTransaction.h" #include "MVKSurface.h" #include "MVKSwapchain.h" #include "MVKSync.h" @@ -69,34 +70,73 @@ // Execute the queue submission under an autoreleasepool to ensure transient Metal objects are autoreleased. // This is critical for apps that don't use standard OS autoreleasing runloop threading. -static inline VkResult execute(MVKQueueSubmission* qSubmit) { @autoreleasepool { return qSubmit->execute(); } } +// The caller brackets it with beginEncoding() and endEncoding(): the device reports a loss to host waits, +// and is destroyed, only while no submission is encoding. +static inline VkResult execute(MVKQueueSubmission* qSubmit) { + VkResult rslt; + @autoreleasepool { rslt = qSubmit->execute(); } // May destroy qSubmit. + return rslt; +} + +static inline VkResult executeNow(MVKDevice* device, MVKQueueSubmission* qSubmit) { + device->beginEncoding(); + VkResult rslt = execute(qSubmit); + device->endEncoding(); + return rslt; +} // Executes the submmission, either immediately, or by dispatching to an execution queue. // Submissions to the execution queue are wrapped in a dedicated autoreleasepool. // Relying on the dispatch queue to find time to drain the autoreleasepool can // result in significant memory creep under heavy workloads. VkResult MVKQueue::submit(MVKQueueSubmission* qSubmit) { - if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); } + VkResult devRslt = _device->isLosing() ? VK_ERROR_DEVICE_LOST : _device->getConfigurationResult(); + if (devRslt != VK_SUCCESS) { + if (qSubmit) { qSubmit->destroy(); } + return devRslt; + } if ( !qSubmit ) { return VK_SUCCESS; } // Ignore nils + qSubmit->queueForPublication(); // Extract result before submission to avoid race condition with early destruction // Submit regardless of config result, to ensure submission semaphores and fences are signalled. // The submissions will ensure a misconfiguration will be safe to execute. VkResult rslt = qSubmit->getConfigurationResult(); - if (_execQueue) { - std::unique_lock lock(_execQueueMutex); + MVKDevice* device = _device; + dispatch_queue_t execQueue = _execQueue; + // Synchronous submits run on the caller, unless this submission, or one still pending on the worker, must continue. + // Only this call, externally synchronized per queue, increments the job count: a zero read stays valid. + if ( !execQueue && !qSubmit->needsWorker() && !_execQueueJobCount ) { return executeNow(device, qSubmit); } + + std::unique_lock lock(_execQueueMutex); + bool usesWorker = !execQueue && (qSubmit->needsWorker() || _execQueueJobCount); + if (usesWorker) { + // With synchronous submits, a submission that waits for its own Metal work would block the caller, which may + // be the only thread able to release that work. It, and every later submission to this queue until the worker + // is idle, execute in order on a worker, and report execution errors to host waits instead of vkQueueSubmit(). + if ( !_workerQueue ) { _workerQueue = dispatch_queue_create((getName() + "-Worker").c_str(), DISPATCH_QUEUE_SERIAL); } // retained + execQueue = _workerQueue; + // Synchronous submits assign the values of binary semaphore waits encoded by Metal here, in submission order. + qSubmit->reserveSemaphores(); + } + if (execQueue) { _execQueueJobCount++; - dispatch_async(_execQueue, ^{ + // A dispatched submission uses this queue and the device until its block ends. + device->beginEncoding(); + dispatch_async(execQueue, ^{ execute(qSubmit); - - std::unique_lock execLock(_execQueueMutex); - if (!--_execQueueJobCount) - _execQueueConditionVariable.notify_all(); + { + std::unique_lock execLock(_execQueueMutex); + if (!--_execQueueJobCount) + _execQueueConditionVariable.notify_all(); + } + device->endEncoding(); // The last use of this queue and of the device. } ); } else { - rslt = execute(qSubmit); + lock.unlock(); + rslt = executeNow(device, qSubmit); } return rslt; } @@ -106,16 +146,19 @@ template VkResult MVKQueue::submit(uint32_t submitCount, const S* pSubmits, VkFence fence, MVKCommandUse cmdUse) { + if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); } // Fence-only submission if (submitCount == 0 && fence) { return submit(new MVKQueueCommandBufferSubmission(this, (S*)nullptr, fence, cmdUse)); } - VkResult rslt = VK_SUCCESS; - for (uint32_t sIdx = 0; sIdx < submitCount; sIdx++) { - VkFence fenceOrNil = (sIdx == (submitCount - 1)) ? fence : VK_NULL_HANDLE; // last one gets the fence - + // Prepare the entire API call before dispatching even its first batch. Destruction + // on failure only releases retained objects and scratch; execute()/finish() have + // not run, so no waits, signals, fences, or Metal submissions have been touched. + std::unordered_set prefilledExecutions; + return mvkSubmitTransaction(submitCount, [&](uint32_t sIdx) { + VkFence fenceOrNil = sIdx == submitCount - 1 ? fence : VK_NULL_HANDLE; const S* pVkSub = &pSubmits[sIdx]; MVKQueueCommandBufferSubmission* mvkSub; uint32_t cbCnt = getCommandBufferCount(pVkSub); @@ -135,10 +178,12 @@ mvkSub = new MVKQueueFullCommandBufferSubmission<512>(this, pVkSub, fenceOrNil, cmdUse); } - VkResult subRslt = submit(mvkSub); - if (rslt == VK_SUCCESS) { rslt = subRslt; } - } - return rslt; + return mvkSub; + }, [&](MVKQueueCommandBufferSubmission& submission) { + return submission.reservePerVertexScratch(prefilledExecutions); + }, [&](MVKQueueCommandBufferSubmission* submission) { + return submit(submission); + }); } // Concrete implementations of templated MVKQueue::submit(). @@ -150,17 +195,17 @@ } VkResult MVKQueue::waitIdle(MVKCommandUse cmdUse) { - if (_execQueue) { + { std::unique_lock lock(_execQueueMutex); while (_execQueueJobCount) _execQueueConditionVariable.wait(lock); } @autoreleasepool { auto* mtlCmdBuff = getMTLCommandBuffer(cmdUse); - [mtlCmdBuff commit]; + _device->commitMTLCommandBuffer(mtlCmdBuff); [mtlCmdBuff waitUntilCompleted]; } - return _device->getConfigurationResult(); + return _device->getHostWaitResult(true, UINT64_MAX); } id MVKQueue::getMTLCommandBuffer(MVKCommandUse cmdUse, bool retainRefs) { @@ -178,7 +223,7 @@ addPerformanceInterval(getPerformanceStats().queue.retrieveMTLCommandBuffer, startTime); NSString* mtlCmdBuffLabel = getMTLCommandBufferLabel(cmdUse); setMetalObjectLabel(mtlCmdBuff, mtlCmdBuffLabel); - [mtlCmdBuff addCompletedHandler: ^(id mtlCB) { handleMTLCommandBufferError(mtlCB); }]; + _device->addMTLCommandBufferHandler(mtlCmdBuff, ^(id mtlCB) { handleMTLCommandBufferError(mtlCB); }); if ( !mtlCmdBuff ) { reportError(VK_ERROR_OUT_OF_POOL_MEMORY, "%s could not be acquired.", mtlCmdBuffLabel.UTF8String); } return mtlCmdBuff; @@ -217,6 +262,20 @@ return "unknown"; } +bool MVKQueue::isDeviceLoss(id mtlCmdBuff) { + if (mtlCmdBuff.status != MTLCommandBufferStatusError) { return false; } + switch (mtlCmdBuff.error.code) { + case MTLCommandBufferErrorBlacklisted: + case MTLCommandBufferErrorNotPermitted: +#if MVK_MACOS && !MVK_MACCAT + case MTLCommandBufferErrorDeviceRemoved: +#endif + return true; + default: + return !getMVKConfig().resumeLostDevice; + } +} + void MVKQueue::handleMTLCommandBufferError(id mtlCmdBuff) { if (mtlCmdBuff.status != MTLCommandBufferStatusError) { return; } @@ -225,7 +284,7 @@ // If the error is local to this command buffer, optionally mark the device (but not the // physical device) as lost, depending on the value of MVKConfiguration::resumeLostDevice. VkResult vkErr = VK_ERROR_UNKNOWN; - bool markDeviceLoss = !getMVKConfig().resumeLostDevice; + bool markDeviceLoss = isDeviceLoss(mtlCmdBuff); bool markPhysicalDeviceLoss = false; switch (mtlCmdBuff.error.code) { case MTLCommandBufferErrorBlacklisted: @@ -234,7 +293,6 @@ case MTLCommandBufferErrorDeviceRemoved: #endif vkErr = VK_ERROR_DEVICE_LOST; - markDeviceLoss = true; markPhysicalDeviceLoss = true; break; case MTLCommandBufferErrorTimeout: @@ -353,12 +411,16 @@ [_mtlCmdBuffLabelInvalidateMappedMemoryRanges release]; } -// Destroys the execution dispatch queue. +// Destroys the execution dispatch queues. void MVKQueue::destroyExecQueue() { if (_execQueue) { dispatch_release(_execQueue); _execQueue = nullptr; } + if (_workerQueue) { + dispatch_release(_workerQueue); + _workerQueue = nullptr; + } } @@ -366,11 +428,33 @@ #pragma mark MVKQueueSubmission void MVKSemaphoreSubmitInfo::encodeWait(id mtlCmdBuff) { - if (_semaphore) { _semaphore->encodeWait(mtlCmdBuff, value); } + if ( !_semaphore ) { return; } + if (_isReserved) { + _semaphore->encodeReservedWait(mtlCmdBuff, _reservation); + } else { + _semaphore->encodeWait(mtlCmdBuff, value); + } } +// Encodes the signal while encoding the submission, and keeps what completeSignal() needs once it has completed. void MVKSemaphoreSubmitInfo::encodeSignal(id mtlCmdBuff) { - if (_semaphore) { _semaphore->encodeSignal(mtlCmdBuff, value); } + if (_semaphore) { _reservation = _semaphore->encodeSubmissionSignal(mtlCmdBuff, value); } +} + +void MVKSemaphoreSubmitInfo::completeSignal() { + if (_semaphore) { _semaphore->completeSubmissionSignal(_reservation); } +} + +// Binary Metal event semaphores assign the values of their waits while encoding; timeline values come from the application. +bool MVKSemaphoreSubmitInfo::isReservable() { + return _semaphore && _semaphore->getSemaphoreType() == VK_SEMAPHORE_TYPE_BINARY && _semaphore->isUsingCommandEncoding(); +} + +void MVKSemaphoreSubmitInfo::reserveWait() { + if (isReservable()) { + _reservation = _semaphore->reserveWait(); + _isReserved = true; + } } MVKSemaphoreSubmitInfo::MVKSemaphoreSubmitInfo(const VkSemaphoreSubmitInfo& semaphoreSubmitInfo) : @@ -392,6 +476,8 @@ MVKSemaphoreSubmitInfo::MVKSemaphoreSubmitInfo(const MVKSemaphoreSubmitInfo& other) : _semaphore(other._semaphore), + _reservation(other._reservation), + _isReserved(other._isReserved), value(other.value), stageMask(other.stageMask), deviceIndex(other.deviceIndex) { @@ -404,6 +490,8 @@ if (_semaphore) { _semaphore->release(); } _semaphore = other._semaphore; + _reservation = other._reservation; + _isReserved = other._isReserved; value = other.value; stageMask = other.stageMask; deviceIndex = other.deviceIndex; @@ -428,13 +516,13 @@ MVKBaseDeviceObject(queue->getDevice()), _queue(queue) { - _queue->retain(); // Retain here and release in destructor. See note for MVKQueueCommandBufferSubmission::finish(). _creationTime = getPerformanceTimestamp(); _waitSemaphores.reserve(waitSemaphoreInfoCount); for (uint32_t i = 0; i < waitSemaphoreInfoCount; i++) { _waitSemaphores.emplace_back(pWaitSemaphoreSubmitInfos[i]); } + _queue->retain(); // Retain after allocations succeed; release in the destructor. } MVKQueueSubmission::MVKQueueSubmission(MVKQueue* queue, @@ -444,13 +532,13 @@ MVKBaseDeviceObject(queue->getDevice()), _queue(queue) { - _queue->retain(); // Retain here and release in destructor. See note for MVKQueueCommandBufferSubmission::finish(). _creationTime = getPerformanceTimestamp(); _waitSemaphores.reserve(waitSemaphoreCount); for (uint32_t i = 0; i < waitSemaphoreCount; i++) { _waitSemaphores.emplace_back(pWaitSemaphores[i], pWaitDstStageMask ? pWaitDstStageMask[i] : 0); } + _queue->retain(); // Retain after allocations succeed; release in the destructor. } MVKQueueSubmission::~MVKQueueSubmission() { @@ -465,6 +553,13 @@ _queue->_submissionCaptureScope->beginScope(); + // While the device is being lost, a submission accepted earlier encodes nothing, and never reads its command + // buffers: host waits report the loss once no submission is encoding. Its Metal waiters are released by the device. + if (getDevice()->isLosing()) { + _queue->publishInOrder(this); + return VK_ERROR_DEVICE_LOST; + } + // If using encoded semaphore waiting, do so now. for (auto& ws : _waitSemaphores) { ws.encodeWait(getActiveMTLCommandBuffer()); } @@ -474,14 +569,43 @@ // Submit each command buffer. submitCommandBuffers(); - // If using encoded semaphore signaling, do so now. - for (auto& ss : _signalSemaphores) { ss.encodeSignal(getActiveMTLCommandBuffer()); } + // If using encoded semaphore signaling, do so now. A device lost while encoding, for example when a GPU-planned + // draw cannot be replayed, signals nothing: no successor may take this submission as complete. + if ( !getDevice()->isLosing() ) { + for (auto& ss : _signalSemaphores) { ss.encodeSignal(getActiveMTLCommandBuffer()); } + } // Commit the last MTLCommandBuffer. // Nothing after this because callback might destroy this instance before this function ends. return commitActiveMTLCommandBuffer(true); } +void MVKQueueCommandBufferSubmission::reserveSemaphores() { + for (auto& ws : _waitSemaphores) { ws.reserveWait(); } +} + +// Waits in slices: once the device is being lost, the committed work may wait on a signal that never comes. +static constexpr uint64_t kMVKContinuationWaitSliceNanos = 1000000; + +id MVKQueueCommandBufferSubmission::continueOnNewMTLCommandBuffer() { + id mtlCmdBuff = [getActiveMTLCommandBuffer() retain]; + if ( !mtlCmdBuff ) { return nil; } + + // Registered after every handler added while encoding. + dispatch_semaphore_t completion = dispatch_semaphore_create(0); // retained + getDevice()->addMTLCommandBufferHandler(mtlCmdBuff, ^(id) { dispatch_semaphore_signal(completion); }); + commitActiveMTLCommandBuffer(); + + bool isCompleted = false; + while ( !(isCompleted = !dispatch_semaphore_wait(completion, dispatch_time(DISPATCH_TIME_NOW, kMVKContinuationWaitSliceNanos))) && !getDevice()->isLosing() ) {} + // Also waits for the completed handlers, which release resources the new Metal command buffer may reuse. + if (isCompleted) { [mtlCmdBuff waitUntilCompleted]; } + bool succeeded = isCompleted && mtlCmdBuff.status == MTLCommandBufferStatusCompleted && !getDevice()->isLosing(); + [mtlCmdBuff release]; + dispatch_release(completion); + return succeeded ? getActiveMTLCommandBuffer() : nil; +} + // Returns the active MTLCommandBuffer, lazily retrieving it from the queue if needed. id MVKQueueCommandBufferSubmission::getActiveMTLCommandBuffer() { if ( !_activeMTLCommandBuffer ) { @@ -493,6 +617,9 @@ needsRetain = true; } setActiveMTLCommandBuffer(_queue->getMTLCommandBuffer(_commandUse, needsRetain)); + // Commands encoded without a Metal command buffer never execute, even if a later one is available: + // report them as a lost device, never as complete. + if ( !_activeMTLCommandBuffer ) { getDevice()->markLost(); } } return _activeMTLCommandBuffer; } @@ -545,24 +672,56 @@ id mtlCmdBuff = signalCompletion ? getActiveMTLCommandBuffer() : _activeMTLCommandBuffer; _activeMTLCommandBuffer = nil; + // Metal documents no order between the handlers of different command buffers: this submission reports its + // completion only once every one of its Metal command buffers has run its handlers, with a final status. uint64_t startTime = getPerformanceTimestamp(); - [mtlCmdBuff addCompletedHandler: ^(id mtlCB) { - addPerformanceInterval(getPerformanceStats().queue.mtlCommandBufferExecution, startTime); - if (signalCompletion) { this->finish(); } // Must be the last thing the completetion callback does. - }]; + MVKDevice* device = getDevice(); + if (mtlCmdBuff) { + _pendingMTLCommandBuffers++; + device->addMTLCommandBufferHandler(mtlCmdBuff, ^(id mtlCB) { + if (_queue->isDeviceLoss(mtlCB)) { _isMTLCommandBufferLost = true; } + addPerformanceInterval(getPerformanceStats().queue.mtlCommandBufferExecution, startTime); + releaseMTLCommandBuffer(); // May destroy this instance. + }); + } // Retrieve the result before committing MTLCommandBuffer, because finish() will destroy this instance. - VkResult rslt = mtlCmdBuff ? getConfigurationResult() : VK_ERROR_OUT_OF_POOL_MEMORY; - [mtlCmdBuff commit]; + VkResult rslt = mtlCmdBuff ? getConfigurationResult() : VK_ERROR_DEVICE_LOST; + device->commitMTLCommandBuffer(mtlCmdBuff); [mtlCmdBuff release]; // retained - // If we need to signal completion, but an error occurred and the MTLCommandBuffer - // was not created, call the finish() function directly. - if (signalCompletion && !mtlCmdBuff) { finish(); } + // The encoding holds its own count until the last Metal command buffer is committed, or none could be created. + if (signalCompletion) { releaseMTLCommandBuffer(); } // May destroy this instance. return rslt; } +void MVKQueueCommandBufferSubmission::releaseMTLCommandBuffer() { + if (--_pendingMTLCommandBuffers == 0) { _queue->publishInOrder(this); } +} + +void MVKQueueCommandBufferSubmission::queueForPublication() { + lock_guard lock(_queue->_publicationLock); + _queue->_unpublishedSubmissions.push_back(this); +} + +// Only one thread publishes at a time, outside the lock: another that completes a submission meanwhile leaves it to +// that thread. finish() destroys the submission; the queue lives on, since its device waits for this handler. +void MVKQueue::publishInOrder(MVKQueueCommandBufferSubmission* submission) { + unique_lock lock(_publicationLock); + submission->_isComplete = true; + if (_isPublishing) { return; } + _isPublishing = true; + while ( !_unpublishedSubmissions.empty() && _unpublishedSubmissions.front()->_isComplete ) { + MVKQueueCommandBufferSubmission* next = _unpublishedSubmissions.front(); + _unpublishedSubmissions.pop_front(); + lock.unlock(); + next->finish(); + lock.lock(); + } + _isPublishing = false; +} + // Be sure to retain() any API objects referenced in this function, and release() them in the // destructor (or superclass destructor). It is possible for rare race conditions to result // in the app destroying API objects before this function completes execution. For example, @@ -574,11 +733,16 @@ // immediately after a waitIdle() is cleared by fence below, taking the capture scope with it. _queue->_submissionCaptureScope->endScope(); - // If using inline semaphore signaling, do so now. - for (auto& ss : _signalSemaphores) { ss.encodeSignal(nil); } + // A failed Metal command buffer of this submission loses the device, whatever order Metal ran the handlers in. + // During a device loss, nothing is reported complete: host waits report the loss once it is published. + if (_isMTLCommandBufferLost) { getDevice()->markLost(); } + if ( !getDevice()->isLosing() ) { + // If using inline semaphore signaling, do so now. + for (auto& ss : _signalSemaphores) { ss.completeSignal(); } - // If a fence exists, signal it. - if (_fence) { _fence->signal(); } + // If a fence exists, signal it. + if (_fence) { _fence->signal(); } + } this->destroy(); } @@ -596,7 +760,6 @@ _fence((MVKFence*)fence), _commandUse(cmdUse) { - if (_fence) { _fence->retain(); } // pSubmit can be null if just tracking the fence alone if (pSubmit) { @@ -606,6 +769,7 @@ _signalSemaphores.emplace_back(pSubmit->pSignalSemaphoreInfos[i]); } } + if (_fence) { _fence->retain(); } // No throwing allocations after this retain. } // On device loss, the fence and signal semaphores may be signalled early, and they might then @@ -623,7 +787,6 @@ _fence((MVKFence*)fence), _commandUse(cmdUse) { - if (_fence) { _fence->retain(); } // pSubmit can be null if just tracking the fence alone if (pSubmit) { @@ -655,6 +818,7 @@ } } } + if (_fence) { _fence->retain(); } // No throwing allocations after this retain. } MVKQueueCommandBufferSubmission::~MVKQueueCommandBufferSubmission() { @@ -662,11 +826,32 @@ } +template +VkResult MVKQueueFullCommandBufferSubmission::reservePerVertexScratch(std::unordered_set& prefilledExecutions) { + if (!wasConfigurationSuccessful()) { return getConfigurationResult(); } + for (auto& cbInfo : _cmdBuffers) { + VkResult result = cbInfo.commandBuffer->reservePerVertexScratch(cbInfo.perVertexScratch, prefilledExecutions); + if (result != VK_SUCCESS) { return result; } + for (auto& scratch : cbInfo.perVertexScratch) { + // Indirect plans and TES topologies are checked on the GPU while encoding, which waits for them. + if (scratch->indirectCapacity || scratch->tessTopologyPipeline) { _needsContinuation = true; } + } + } + return VK_SUCCESS; +} + template void MVKQueueFullCommandBufferSubmission::submitCommandBuffers() { uint64_t startTime = getPerformanceTimestamp(); - for (auto& cbInfo : _cmdBuffers) { cbInfo.commandBuffer->submit(this, &_encodingContext); } + for (auto& cbInfo : _cmdBuffers) { + // After a loss, later command buffers are neither encoded nor marked as executing. + if (getDevice()->isLosing()) { break; } + cbInfo.commandBuffer->submit(this, &_encodingContext, cbInfo.perVertexScratch); + // Preserve encode-time failures for synchronous submissions. Async submit has + // already returned; allocation pressure must not be promoted to device loss. + setConfigurationResult(cbInfo.commandBuffer->getConfigurationResult()); + } addPerformanceInterval(getPerformanceStats().queue.submitCommandBuffers, startTime); } @@ -713,6 +898,12 @@ // If the semaphores are not encodable, wait on them inline after presenting. // The semaphores know what to do. VkResult MVKQueuePresentSurfaceSubmission::execute() { + // While the device is being lost, a presentation accepted earlier is dropped. + if (getDevice()->isLosing()) { + finish(); + return VK_ERROR_DEVICE_LOST; + } + // MTLCommandBuffer retain references to avoid rare case where objects are destroyed too early. // Although testing could not determine which objects were being lost, queue present MTLCommandBuffers // are used only once per frame, and retain so few objects, that blanket retention is still performant. @@ -743,8 +934,8 @@ // Retrieve the result first, because finish() will destroy this instance. VkResult rslt = getConfigurationResult(); if (mtlCmdBuff) { - [mtlCmdBuff addCompletedHandler: ^(id mtlCB) { this->finish(); }]; - [mtlCmdBuff commit]; + getDevice()->addMTLCommandBufferHandler(mtlCmdBuff, ^(id mtlCB) { this->finish(); }); + getDevice()->commitMTLCommandBuffer(mtlCmdBuff); } else { finish(); } @@ -850,4 +1041,3 @@ setConfigurationResult(scRslt); } } - diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h index 2fa123426..c5c3dfac5 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h @@ -75,6 +75,17 @@ class MVKRenderSubpass : public MVKBaseObject { /** Returns whether or not the stencil attachment is being used. */ bool isStencilAttachmentUsed() const { return _stencilAttachment.attachment != VK_ATTACHMENT_UNUSED; } + /** Returns whether an active source attachment has a native or fallback resolve. */ + bool hasResolveAttachments() const { + for (size_t i = 0; i < _colorAttachments.size() && i < _resolveAttachments.size(); ++i) { + if (_colorAttachments[i].attachment != VK_ATTACHMENT_UNUSED && _resolveAttachments[i].attachment != VK_ATTACHMENT_UNUSED) { return true; } + } + return (isDepthAttachmentUsed() && _depthResolveAttachment.attachment != VK_ATTACHMENT_UNUSED && _depthResolveMode != VK_RESOLVE_MODE_NONE) || (isStencilAttachmentUsed() && _stencilResolveAttachment.attachment != VK_ATTACHMENT_UNUSED && _stencilResolveMode != VK_RESOLVE_MODE_NONE); + } + + /** Returns why an active resolve cannot survive a portable indexed capture split, or nullptr. */ + const char* getPerVertexResolveError(); + /** Return the depth attachment format. */ VkFormat getDepthFormat(); @@ -420,6 +431,9 @@ class MVKRenderingAttachmentIterator : public MVKBaseObject { #pragma mark - #pragma mark Support functions +/** Checks native resolve support for a portable indexed capture split. Memoryless views are checked by the caller. */ +const char* mvkGetPerVertexResolveError(const MVKPhysicalDeviceMetalFeatures& features, MVKPixelFormats* formats, VkFormat resolveFormat, VkImageAspectFlagBits aspect, VkResolveModeFlagBits mode, VkRenderingFlags renderingFlags); + /** Returns whether the view mask uses multiview. */ static constexpr bool mvkIsMultiview(uint32_t viewMask) { return viewMask != 0; } diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm index b6841665f..482c494b3 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm @@ -294,6 +294,36 @@ - (void)setOpenGLModeEnabled:(BOOL)enabled; #endif } +const char* mvkGetPerVertexResolveError(const MVKPhysicalDeviceMetalFeatures& features, MVKPixelFormats* formats, VkFormat resolveFormat, VkImageAspectFlagBits aspect, VkResolveModeFlagBits mode, VkRenderingFlags renderingFlags) { + // Suspension selects Store, which Metal forbids while a native resolve texture is attached. + if (mvkIsAnyFlagEnabled(renderingFlags, VK_RENDERING_SUSPENDING_BIT)) { return "indexed capture cannot suspend rendering with resolve attachments."; } + if (!features.combinedStoreResolveAction) { return "indexed capture with resolve requires combined store/resolve to preserve the final source."; } + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT && !mvkAreAllFlagsEnabled(formats->getCapabilities(resolveFormat), kMVKMTLFmtCapsResolve)) { return "indexed capture does not support software color resolve."; } + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT && (!features.depthResolve || (mode != VK_RESOLVE_MODE_SAMPLE_ZERO_BIT && mode != VK_RESOLVE_MODE_MIN_BIT && mode != VK_RESOLVE_MODE_MAX_BIT))) { return "indexed capture requires supported native depth resolve."; } + if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && (!features.stencilResolve || mode != VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) { return "indexed capture requires native sample-zero stencil resolve."; } + return nullptr; +} + +const char* MVKRenderSubpass::getPerVertexResolveError() { + auto check = [&](uint32_t index, VkImageAspectFlagBits aspect, VkResolveModeFlagBits mode) { + // Render-pass formats match valid framebuffer/imageless views; dynamic rendering records their effective formats. + if (index >= _renderPass->_attachments.size()) { return "indexed capture resolve attachment index is out of range."; } + return mvkGetPerVertexResolveError(_renderPass->getMetalFeatures(), _renderPass->getPixelFormats(), _renderPass->_attachments[index].getFormat(), aspect, mode, _renderPass->getRenderingFlags()); + }; + for (size_t i = 0; i < _colorAttachments.size() && i < _resolveAttachments.size(); ++i) { + if (_colorAttachments[i].attachment != VK_ATTACHMENT_UNUSED && _resolveAttachments[i].attachment != VK_ATTACHMENT_UNUSED) { + if (const char* error = check(_resolveAttachments[i].attachment, VK_IMAGE_ASPECT_COLOR_BIT, VK_RESOLVE_MODE_AVERAGE_BIT)) { return error; } + } + } + if (isDepthAttachmentUsed() && _depthResolveAttachment.attachment != VK_ATTACHMENT_UNUSED && _depthResolveMode != VK_RESOLVE_MODE_NONE) { + if (const char* error = check(_depthResolveAttachment.attachment, VK_IMAGE_ASPECT_DEPTH_BIT, _depthResolveMode)) { return error; } + } + if (isStencilAttachmentUsed() && _stencilResolveAttachment.attachment != VK_ATTACHMENT_UNUSED && _stencilResolveMode != VK_RESOLVE_MODE_NONE) { + if (const char* error = check(_stencilResolveAttachment.attachment, VK_IMAGE_ASPECT_STENCIL_BIT, _stencilResolveMode)) { return error; } + } + return nullptr; +} + void MVKRenderSubpass::encodeStoreActions(MVKCommandEncoder* cmdEncoder, bool isRenderingEntireAttachment, MVKArrayRef attachments, @@ -1237,9 +1267,9 @@ uint32_t mvkGetNextViewMaskGroup(uint32_t viewMask, uint32_t* startView, uint32_ // This is one past the end of the next clump. Clear the bits as we go, so we can use // ffs(3) again on the next clump. // TODO: Find a way to make this faster. - while (viewMask & (1 << end)) { - if (groupMask) { *groupMask |= viewMask & (1 << end); } - viewMask &= ~(1 << (end++)); + while (viewMask && (viewMask & (1u << end))) { + if (groupMask) { *groupMask |= viewMask & (1u << end); } + viewMask &= ~(1u << (end++)); } if (startView) { *startView = pos; } if (viewCount) { *viewCount = end - pos; } diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKShaderModule.mm b/MoltenVK/MoltenVK/GPUObjects/MVKShaderModule.mm index 3aa44a44b..10d22151e 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKShaderModule.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKShaderModule.mm @@ -437,6 +437,8 @@ static uint32_t getWorkgroupDimensionSize(const SPIRVWorkgroupSizeDimension& wgD case spv::ExecutionModelGeometry: type = "-gs"; break; case spv::ExecutionModelTaskNV: type = "-ts"; break; case spv::ExecutionModelMeshNV: type = "-ms"; break; + case spv::ExecutionModelTaskEXT: type = "-ts"; break; + case spv::ExecutionModelMeshEXT: type = "-ms"; break; case spv::ExecutionModelGLCompute: type = "-cs"; break; default: type = ""; break; } diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSubmissionTransaction.h b/MoltenVK/MoltenVK/GPUObjects/MVKSubmissionTransaction.h new file mode 100644 index 000000000..f05523c87 --- /dev/null +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSubmissionTransaction.h @@ -0,0 +1,27 @@ +/* Copyright (c) 2026 Jean-Philippe Meunier. Licensed under the Apache License, Version 2.0. */ +#pragma once + +#include +#include +#include + +// Keep preparation and dispatch separate even when a later batch fails. This helper +// is also exercised with CPU-only submissions that inject allocation failures. +template +VkResult mvkSubmitTransaction(uint32_t count, Create create, Reserve reserve, Dispatch dispatch) { + std::vector> pending; + try { + pending.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + pending.emplace_back(create(i)); + VkResult result = reserve(*pending.back()); + if (result != VK_SUCCESS) { return result; } + } + } catch (const std::bad_alloc&) { return VK_ERROR_OUT_OF_HOST_MEMORY; } + VkResult result = VK_SUCCESS; + for (auto& submission : pending) { + VkResult dispatched = dispatch(submission.release()); + if (result == VK_SUCCESS) { result = dispatched; } + } + return result; +} diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h index 3f526c10b..856bec50b 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h @@ -58,6 +58,12 @@ class MVKSurface : public MVKVulkanAPIObject { /** Returns whether this surface is headless. */ bool isHeadless() { return !_mtlCAMetalLayer && wasConfigurationSuccessful(); } + /** + * Returns the serial queue on which the drawables of the CAMetalLayer are requested, so that the encoding that + * needs one does not wait in Core Animation itself (see MVKPresentableSwapchainImage::getCAMetalDrawable()). + */ + dispatch_queue_t getDrawableQueue(); + #pragma mark Construction MVKSurface(MVKInstance* mvkInstance, @@ -87,5 +93,6 @@ class MVKSurface : public MVKVulkanAPIObject { CAMetalLayer* _mtlCAMetalLayer = nil; MVKBlockObserver* _layerObserver = nil; MVKSwapchain* _activeSwapchain = nullptr; + dispatch_queue_t _drawableQueue = nullptr; }; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm index 87e6192ce..717642c7a 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm @@ -120,7 +120,14 @@ _layerObserver = nil; } +dispatch_queue_t MVKSurface::getDrawableQueue() { + std::lock_guard lock(_layerLock); + if ( !_drawableQueue ) { _drawableQueue = dispatch_queue_create("MoltenVK drawables", DISPATCH_QUEUE_SERIAL); } // retained + return _drawableQueue; +} + MVKSurface::~MVKSurface() { releaseLayer(); + if (_drawableQueue) { dispatch_release(_drawableQueue); } // a pending request retains it until it ends } diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSync.h b/MoltenVK/MoltenVK/GPUObjects/MVKSync.h index e357299e6..2963e12ee 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSync.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSync.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include class MVKFenceSitter; @@ -60,6 +62,9 @@ class MVKSemaphoreImpl : public MVKBaseObject { */ bool release(); + /** Permanently unblocks this semaphore after device loss, including later reservations. */ + void cancel(); + /** Returns whether this instance is in a reserved state. */ bool isReserved(); @@ -105,6 +110,27 @@ class MVKSemaphoreImpl : public MVKBaseObject { std::condition_variable _blocker; uint32_t _reservationCount; bool _shouldWaitAll; + bool _cancelled = false; +}; + + +#pragma mark - +#pragma mark MVKLossReleasable + +/** A semaphore or event whose encoded Metal waits a device loss must release, so that the work waiting on them ends. */ +class MVKLossReleasable { + +public: + + /** + * After device loss, releases every Metal wait encoded on this object, by signalling on the specified + * Metal command buffer, or on the host if it is nil and the underlying event allows it. + * Returns false if a wait could not be released. + */ + virtual bool releaseWaitsAfterLoss(id mtlCmdBuff) = 0; + +protected: + ~MVKLossReleasable() = default; }; @@ -112,7 +138,7 @@ class MVKSemaphoreImpl : public MVKBaseObject { #pragma mark MVKSemaphore /** Abstract class that represents a Vulkan semaphore. */ -class MVKSemaphore : public MVKVulkanAPIDeviceObject { +class MVKSemaphore : public MVKVulkanAPIDeviceObject, public MVKLossReleasable { public: @@ -185,6 +211,32 @@ class MVKSemaphore : public MVKVulkanAPIDeviceObject { /** Returns whether this semaphore uses command encoding. */ virtual bool isUsingCommandEncoding() = 0; + /** + * Encodes the signal of a queue submission, and returns the token that the submission passes to + * completeSubmissionSignal() once its Metal work has completed. Unless overridden, this is encodeSignal(), + * then encodeSignal(nil, value) on completion. + */ + virtual uint64_t encodeSubmissionSignal(id mtlCmdBuff, uint64_t value) { + encodeSignal(mtlCmdBuff, value); + return value; + } + + /** Completes a signal encoded by encodeSubmissionSignal(), with the token it returned. */ + virtual void completeSubmissionSignal(uint64_t token) { encodeSignal(nil, token); } + + /** + * Reserves the value that a later encodeReservedWait() waits for, for a semaphore that otherwise assigns it + * while encoding. A submission whose encoding is deferred reserves its waits when it is submitted, so that + * they pair with other submissions exactly as if it were encoded then. + * Returns zero for semaphores that do not assign values while encoding. + */ + virtual uint64_t reserveWait() { return 0; } + + /** Encodes a wait for a value returned by reserveWait(). */ + virtual void encodeReservedWait(id mtlCmdBuff, uint64_t reservation) {} + + bool releaseWaitsAfterLoss(id mtlCmdBuff) override { return true; } + /** * Returns the MTLSharedEvent underlying this Vulkan semaphore, * or nil if this semaphore is not underpinned by a MTLSharedEvent. @@ -238,9 +290,14 @@ class MVKSemaphoreMTLEvent : public MVKSemaphore { public: void encodeWait(id mtlCmdBuff, uint64_t value) override; void encodeSignal(id mtlCmdBuff, uint64_t value) override; + uint64_t encodeSubmissionSignal(id mtlCmdBuff, uint64_t value) override; + void completeSubmissionSignal(uint64_t token) override; uint64_t deferSignal() override; void encodeDeferredSignal(id mtlCmdBuff, uint64_t deferToken) override; bool isUsingCommandEncoding() override { return true; } + uint64_t reserveWait() override; + void encodeReservedWait(id mtlCmdBuff, uint64_t reservation) override; + bool releaseWaitsAfterLoss(id mtlCmdBuff) override; MVKSemaphoreMTLEvent(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, @@ -250,8 +307,16 @@ class MVKSemaphoreMTLEvent : public MVKSemaphore { ~MVKSemaphoreMTLEvent() override; protected: - id _mtlEvent; + id _mtlEvent; std::atomic _mtlEventValue; + std::atomic _maxEncodedWaitValue{0}; + std::atomic _isLossReleased{false}; + std::mutex _signalLock; + uint64_t _lastDeferredSignalValue = 0; + uint64_t _metalSignalValue = 0; // The one pending signal encoded in Metal, or zero. + uint32_t _pendingSubmissionSignals = 0; + bool _isMetalSignalPreempted = false; + bool _isImported = false; }; @@ -317,6 +382,12 @@ class MVKTimelineSemaphore : public MVKSemaphore { #pragma mark - #pragma mark MVKTimelineSemaphoreMTLEvent +/** Links the Metal event listener of a host wait to that wait, until the wait ends and detaches it. */ +struct MVKTimelineWaitToken { + std::mutex lock; + MVKFenceSitter* sitter; +}; + /** An MVKTimelineSemaphore that uses MTLSharedEvent to provide synchronization. */ class MVKTimelineSemaphoreMTLEvent : public MVKTimelineSemaphore { @@ -330,6 +401,7 @@ class MVKTimelineSemaphoreMTLEvent : public MVKTimelineSemaphore { void signal(const VkSemaphoreSignalInfo* pSignalInfo) override; bool registerWait(MVKFenceSitter* sitter, const VkSemaphoreWaitInfo* pWaitInfo, uint32_t index) override; void unregisterWait(MVKFenceSitter* sitter) override; + bool releaseWaitsAfterLoss(id mtlCmdBuff) override; MVKTimelineSemaphoreMTLEvent(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, @@ -342,7 +414,9 @@ class MVKTimelineSemaphoreMTLEvent : public MVKTimelineSemaphore { protected: id _mtlEvent = nil; std::mutex _lock; - std::unordered_set _sitters; + std::unordered_map> _sitters; + std::atomic _maxEncodedWaitValue{0}; + std::atomic _isLossReleased{false}; }; @@ -492,13 +566,14 @@ class MVKEvent : public MVKVulkanAPIDeviceObject { #pragma mark MVKEventNative /** An MVKEvent that uses native MTLSharedEvent to provide VkEvent functionality. */ -class MVKEventNative : public MVKEvent { +class MVKEventNative : public MVKEvent, public MVKLossReleasable { public: bool isSet() override; void signal(bool status) override; void encodeSignal(id mtlCmdBuff, bool status) override; void encodeWait(id mtlCmdBuff) override; + bool releaseWaitsAfterLoss(id mtlCmdBuff) override; id getMTLSharedEvent() override { return _mtlEvent; }; MVKEventNative(MVKDevice* device, @@ -510,6 +585,8 @@ class MVKEventNative : public MVKEvent { protected: id _mtlEvent; + std::atomic _maxEncodedWaitValue{0}; + std::atomic _isLossReleased{false}; }; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm index b15e6b8a2..fa20018e7 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm @@ -18,6 +18,7 @@ #include "MVKSync.h" #include "MVKFoundation.h" +#include using namespace std; @@ -27,7 +28,7 @@ bool MVKSemaphoreImpl::release() { lock_guard lock(_lock); - if (isClear()) { return true; } + if (_cancelled || isClear()) { return true; } // Either decrement the reservation counter, or clear it altogether if (_shouldWaitAll) { @@ -40,9 +41,16 @@ return isClear(); } +void MVKSemaphoreImpl::cancel() { + lock_guard lock(_lock); + _cancelled = true; + _reservationCount = 0; + _blocker.notify_all(); +} + void MVKSemaphoreImpl::reserve() { lock_guard lock(_lock); - _reservationCount++; + if (!_cancelled) { _reservationCount++; } } bool MVKSemaphoreImpl::isReserved() { @@ -71,7 +79,7 @@ isDone = _blocker.wait_for(lock, nanos, [this]{ return isClear(); }); } - if (reserveAgain) { _reservationCount++; } + if (reserveAgain && !_cancelled) { _reservationCount++; } return isDone; } @@ -116,14 +124,76 @@ #pragma mark MVKSemaphoreMTLEvent void MVKSemaphoreMTLEvent::encodeWait(id mtlCmdBuff, uint64_t) { - if (mtlCmdBuff) { [mtlCmdBuff encodeWaitForEvent: _mtlEvent value: _mtlEventValue++]; } + if (mtlCmdBuff) { encodeReservedWait(mtlCmdBuff, _mtlEventValue++); } +} + +uint64_t MVKSemaphoreMTLEvent::reserveWait() { + return _mtlEventValue++; +} + +void MVKSemaphoreMTLEvent::encodeReservedWait(id mtlCmdBuff, uint64_t reservation) { + if ( !mtlCmdBuff ) { return; } + uint64_t maxValue = _maxEncodedWaitValue.load(); + while (maxValue < reservation && !_maxEncodedWaitValue.compare_exchange_weak(maxValue, reservation)) {} + if ( !_isLossReleased.exchange(true) ) { _device->addLossReleasable(this); } + [mtlCmdBuff encodeWaitForEvent: _mtlEvent value: reservation]; +} + +// Releases the highest wait encoded so far, which covers every lower one since Metal event values never decrease. +// Not the next value: that is the generation of the next Vulkan signal, which an external Metal client of an +// imported or exported event may be waiting for. A wait reserved but not yet encoded is released by the next call. +bool MVKSemaphoreMTLEvent::releaseWaitsAfterLoss(id mtlCmdBuff) { + uint64_t value = _maxEncodedWaitValue.load(); + if ( !value ) { return true; } + if (mtlCmdBuff) { + [mtlCmdBuff encodeSignalEvent: _mtlEvent value: value]; + } else if (_mtlEvent.signaledValue < value) { + _mtlEvent.signaledValue = value; + } + return true; } +// Queue submissions signal through encodeSubmissionSignal(), and swapchains through deferSignal(). void MVKSemaphoreMTLEvent::encodeSignal(id mtlCmdBuff, uint64_t) { if (mtlCmdBuff) { [mtlCmdBuff encodeSignalEvent: _mtlEvent value: _mtlEventValue]; } } +// A binary semaphore pairs each wait with the signal that executes before it, which is not always the one submitted +// first: a signal submitted earlier on another queue may wait for work that a later signal releases (B/A/C/D in +// MoltenVK/Tests/PerVertexBinarySemaphoreOrderTests.mm). Waits take their value in submission order. A signal takes +// the value after the one its semaphore holds when it executes. It is encoded in Metal only while no other signal of +// this semaphore is pending, so that none can execute before it; that one may still be preempted by a later signal, +// see completeSubmissionSignal(). Any other signal sets the value from the host once its submission completes, after +// its work and before its fence. An imported event may be signalled outside Vulkan: its signals always wait for +// completion. A swapchain signal (deferSignal()) cannot overlap them, because acquisition requires a semaphore with +// no pending operation; its value is accounted for all the same. +uint64_t MVKSemaphoreMTLEvent::encodeSubmissionSignal(id mtlCmdBuff, uint64_t) { + lock_guard lock(_signalLock); + if (_pendingSubmissionSignals++ || _isImported || !mtlCmdBuff) { return 0; } + _metalSignalValue = std::max(_mtlEvent.signaledValue, _lastDeferredSignalValue) + 1; + _isMetalSignalPreempted = false; + [mtlCmdBuff encodeSignalEvent: _mtlEvent value: _metalSignalValue]; + return _metalSignalValue; +} + +// The signal encoded in Metal is complete unless a signal set from the host took its value first: its own Metal +// signal did nothing, and it then takes the next value like any other signal. That other signal had executed before +// it, as a binary semaphore must be unsignaled when a signal executes. +void MVKSemaphoreMTLEvent::completeSubmissionSignal(uint64_t token) { + lock_guard lock(_signalLock); + _pendingSubmissionSignals--; + if (token) { + _metalSignalValue = 0; + if ( !_isMetalSignalPreempted ) { return; } + } + uint64_t value = _mtlEvent.signaledValue + 1; + if (value == _metalSignalValue) { _isMetalSignalPreempted = true; } + _mtlEvent.signaledValue = value; +} + uint64_t MVKSemaphoreMTLEvent::deferSignal() { + lock_guard lock(_signalLock); + _lastDeferredSignalValue = _mtlEventValue; return _mtlEventValue; } @@ -135,21 +205,14 @@ const VkSemaphoreCreateInfo* pCreateInfo, const VkExportMetalObjectCreateInfoEXT* pExportInfo, const VkImportMetalSharedEventInfoEXT* pImportInfo) : MVKSemaphore(device, pCreateInfo) { - // In order of preference, import a MTLSharedEvent, - // create a MTLSharedEvent, or create a MTLEvent. - if (pImportInfo && pImportInfo->mtlSharedEvent) { - _mtlEvent = [pImportInfo->mtlSharedEvent retain]; // retained - _mtlEventValue = pImportInfo->mtlSharedEvent.signaledValue + 1; - } else if (pExportInfo && pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_SHARED_EVENT_BIT_EXT) { - _mtlEvent = [getMTLDevice() newSharedEvent]; //retained - _mtlEventValue = ((id)_mtlEvent).signaledValue + 1; - } else { - _mtlEvent = [getMTLDevice() newEvent]; //retained - _mtlEventValue = 1; - } + // Import a MTLSharedEvent, or create one: signals set from the host need a shared event. + _isImported = pImportInfo && pImportInfo->mtlSharedEvent; + _mtlEvent = _isImported ? [pImportInfo->mtlSharedEvent retain] : [getMTLDevice() newSharedEvent]; // retained + _mtlEventValue = _mtlEvent.signaledValue + 1; } MVKSemaphoreMTLEvent::~MVKSemaphoreMTLEvent() { + if (_isLossReleased) { _device->removeLossReleasable(this); } [_mtlEvent release]; } @@ -158,10 +221,11 @@ #pragma mark MVKSemaphoreEmulated void MVKSemaphoreEmulated::encodeWait(id mtlCmdBuff, uint64_t) { + // Only queue submissions wait here, while encoding: a device loss cancels this wait at once. if ( !mtlCmdBuff ) { - _device->addSemaphore(&_blocker); + _device->addEncodingSemaphore(&_blocker); _blocker.wait(UINT64_MAX, true); - _device->removeSemaphore(&_blocker); + _device->removeEncodingSemaphore(&_blocker); } } @@ -195,9 +259,25 @@ // Nil mtlCmdBuff will do nothing. void MVKTimelineSemaphoreMTLEvent::encodeWait(id mtlCmdBuff, uint64_t value) { + if ( !mtlCmdBuff ) { return; } + uint64_t maxValue = _maxEncodedWaitValue.load(); + while (maxValue < value && !_maxEncodedWaitValue.compare_exchange_weak(maxValue, value)) {} + if ( !_isLossReleased.exchange(true) ) { _device->addLossReleasable(this); } [mtlCmdBuff encodeWaitForEvent: _mtlEvent value: value]; } +// Metal event values never decrease, so a value already reached is unaffected. +bool MVKTimelineSemaphoreMTLEvent::releaseWaitsAfterLoss(id mtlCmdBuff) { + uint64_t value = _maxEncodedWaitValue.load(); + if ( !value ) { return true; } + if (mtlCmdBuff) { + [mtlCmdBuff encodeSignalEvent: _mtlEvent value: value]; + } else if (_mtlEvent.signaledValue < value) { + _mtlEvent.signaledValue = value; + } + return true; +} + // Nil mtlCmdBuff will do nothing. void MVKTimelineSemaphoreMTLEvent::encodeSignal(id mtlCmdBuff, uint64_t value) { [mtlCmdBuff encodeSignalEvent: _mtlEvent value: value]; @@ -211,16 +291,19 @@ if (_mtlEvent.signaledValue >= pWaitInfo->pValues[index]) { return true; } lock_guard lock(_lock); sitter->await(); - auto addRslt = _sitters.insert(sitter); + auto addRslt = _sitters.emplace(sitter, nullptr); if (addRslt.second) { - retain(); + // Metal may run the listener after this wait has ended, or never. It holds only a token, which the wait detaches + // when it ends, never the waiter or this semaphore: the waiter's address may be that of a later wait by then. + auto token = std::make_shared(); + token->sitter = sitter; + addRslt.first->second = token; _device->addSemaphore(&sitter->_blocker); [_mtlEvent notifyListener: sitter->getMTLSharedEventListener() atValue: pWaitInfo->pValues[index] block: ^(id, uint64_t) { - lock_guard blockLock(_lock); - if (_sitters.count(sitter)) { sitter->signaled(); } - release(); + lock_guard tokenLock(token->lock); + if (token->sitter) { token->sitter->signaled(); } }]; } return false; @@ -229,7 +312,12 @@ void MVKTimelineSemaphoreMTLEvent::unregisterWait(MVKFenceSitter* sitter) { lock_guard lock(_lock); _device->removeSemaphore(&sitter->_blocker); - _sitters.erase(sitter); + auto found = _sitters.find(sitter); + if (found == _sitters.end()) { return; } + auto token = found->second; + _sitters.erase(found); + lock_guard tokenLock(token->lock); + token->sitter = nullptr; } MVKTimelineSemaphoreMTLEvent::MVKTimelineSemaphoreMTLEvent(MVKDevice* device, @@ -249,6 +337,7 @@ } MVKTimelineSemaphoreMTLEvent::~MVKTimelineSemaphoreMTLEvent() { + if (_isLossReleased) { _device->removeLossReleasable(this); } [_mtlEvent release]; } @@ -335,8 +424,25 @@ void MVKEventNative::encodeWait(id mtlCmdBuff) { if ( !isSet() ) { - [mtlCmdBuff encodeWaitForEvent: _mtlEvent value: _mtlEvent.signaledValue + 1]; + uint64_t value = _mtlEvent.signaledValue + 1; + uint64_t maxValue = _maxEncodedWaitValue.load(); + while (maxValue < value && !_maxEncodedWaitValue.compare_exchange_weak(maxValue, value)) {} + if ( !_isLossReleased.exchange(true) ) { _device->addLossReleasable(this); } + [mtlCmdBuff encodeWaitForEvent: _mtlEvent value: value]; + } +} + +// Like MVKSemaphoreMTLEvent: sets the event to the highest wait encoded so far, never beyond it, since an external +// Metal client of an imported or exported event may wait for a later value. +bool MVKEventNative::releaseWaitsAfterLoss(id mtlCmdBuff) { + uint64_t value = _maxEncodedWaitValue.load(); + if ( !value ) { return true; } + if (mtlCmdBuff) { + [mtlCmdBuff encodeSignalEvent: _mtlEvent value: value]; + } else if (_mtlEvent.signaledValue < value) { + _mtlEvent.signaledValue = value; } + return true; } MVKEventNative::MVKEventNative(MVKDevice* device, @@ -352,6 +458,7 @@ } MVKEventNative::~MVKEventNative() { + if (_isLossReleased) { _device->removeLossReleasable(this); } [_mtlEvent release]; } @@ -359,6 +466,13 @@ #pragma mark - #pragma mark Support functions +// Returns what remains of a host wait timeout in nanoseconds, UINT64_MAX meaning infinite. +static uint64_t mvkRemainingTimeout(chrono::steady_clock::time_point startTime, uint64_t timeout) { + if (timeout == UINT64_MAX) { return UINT64_MAX; } + uint64_t elapsed = chrono::duration_cast(chrono::steady_clock::now() - startTime).count(); + return elapsed < timeout ? timeout - elapsed : 0; +} + VkResult mvkResetFences(uint32_t fenceCount, const VkFence* pFences) { for (uint32_t i = 0; i < fenceCount; i++) { ((MVKFence*)pFences[i])->reset(); @@ -377,7 +491,7 @@ VkResult mvkWaitForFences(MVKDevice* device, return device->getConfigurationResult(); } - VkResult rslt = VK_SUCCESS; + auto startTime = chrono::steady_clock::now(); MVKFenceSitter fenceSitter(waitAll); for (uint32_t i = 0; i < fenceCount; i++) { @@ -385,11 +499,7 @@ VkResult mvkWaitForFences(MVKDevice* device, } bool finished = fenceSitter.wait(timeout); - if (device->getConfigurationResult() != VK_SUCCESS) { - rslt = device->getConfigurationResult(); - } else if ( !finished ) { - rslt = VK_TIMEOUT; - } + VkResult rslt = device->getHostWaitResult(finished, mvkRemainingTimeout(startTime, timeout)); for (uint32_t i = 0; i < fenceCount; i++) { ((MVKFence*)pFences[i])->removeSitter(&fenceSitter); @@ -407,7 +517,7 @@ VkResult mvkWaitSemaphores(MVKDevice* device, return device->getConfigurationResult(); } - VkResult rslt = VK_SUCCESS; + auto startTime = chrono::steady_clock::now(); bool waitAny = mvkIsAnyFlagEnabled(pWaitInfo->flags, VK_SEMAPHORE_WAIT_ANY_BIT); bool alreadySignaled = false; MVKFenceSitter fenceSitter(!waitAny); @@ -421,11 +531,7 @@ VkResult mvkWaitSemaphores(MVKDevice* device, } bool finished = alreadySignaled || fenceSitter.wait(timeout); - if (device->getConfigurationResult() != VK_SUCCESS) { - rslt = device->getConfigurationResult(); - } else if ( !finished ) { - rslt = VK_TIMEOUT; - } + VkResult rslt = device->getHostWaitResult(finished, mvkRemainingTimeout(startTime, timeout)); for (uint32_t i = 0; i < pWaitInfo->semaphoreCount; i++) { ((MVKTimelineSemaphore*)pWaitInfo->pSemaphores[i])->unregisterWait(&fenceSitter); diff --git a/MoltenVK/MoltenVK/Layers/MVKExtensions.def b/MoltenVK/MoltenVK/Layers/MVKExtensions.def index bb0935b10..9a4a2f32b 100644 --- a/MoltenVK/MoltenVK/Layers/MVKExtensions.def +++ b/MoltenVK/MoltenVK/Layers/MVKExtensions.def @@ -159,6 +159,7 @@ MVK_EXTENSION(EXT_legacy_dithering, EXT_LEGACY_DITHERING, MVK_EXTENSION(EXT_line_rasterization, EXT_LINE_RASTERIZATION, DEVICE, 10.11, 8.0, 1.0) MVK_EXTENSION(EXT_load_store_op_none, EXT_LOAD_STORE_OP_NONE, DEVICE, 10.11, 8.0, 1.0) MVK_EXTENSION(EXT_memory_budget, EXT_MEMORY_BUDGET, DEVICE, 10.13, 11.0, 1.0) +MVK_EXTENSION(EXT_mesh_shader, EXT_MESH_SHADER, DEVICE, 13.0, 16.0, 1.0) MVK_EXTENSION(EXT_metal_objects, EXT_METAL_OBJECTS, DEVICE, 10.11, 8.0, 1.0) MVK_EXTENSION(EXT_metal_surface, EXT_METAL_SURFACE, INSTANCE, 10.11, 8.0, 1.0) MVK_EXTENSION(EXT_multi_draw, EXT_MULTI_DRAW, DEVICE, 10.11, 8.0, 1.0) diff --git a/MoltenVK/MoltenVK/Utility/MVKBaseObject.h b/MoltenVK/MoltenVK/Utility/MVKBaseObject.h index d9e04de17..5dbc675da 100644 --- a/MoltenVK/MoltenVK/Utility/MVKBaseObject.h +++ b/MoltenVK/MoltenVK/Utility/MVKBaseObject.h @@ -196,21 +196,22 @@ class MVKConfigurableMixin { public: /** Returns a indication of the success of the configuration of this instance. */ - VkResult getConfigurationResult() { return _configurationResult; } + VkResult getConfigurationResult() { return _configurationResult.load(); } /** If the existing configuration result is VK_SUCCESS, it is set to the specified value. */ void setConfigurationResult(VkResult vkResult) { - if (_configurationResult == VK_SUCCESS) { _configurationResult = vkResult; } + VkResult expected = VK_SUCCESS; + _configurationResult.compare_exchange_strong(expected, vkResult); } /** Returns whether the configuration was successful. */ - bool wasConfigurationSuccessful() { return _configurationResult == VK_SUCCESS; } + bool wasConfigurationSuccessful() { return _configurationResult.load() == VK_SUCCESS; } /** Resets the indication of the success of the configuration of this instance back to VK_SUCCESS. */ - void clearConfigurationResult() { _configurationResult = VK_SUCCESS; } + void clearConfigurationResult() { _configurationResult.store(VK_SUCCESS); } protected: - VkResult _configurationResult = VK_SUCCESS; + std::atomic _configurationResult{VK_SUCCESS}; }; diff --git a/MoltenVK/MoltenVK/Utility/MVKStateTracking.h b/MoltenVK/MoltenVK/Utility/MVKStateTracking.h index 93c14daf3..90c73403f 100644 --- a/MoltenVK/MoltenVK/Utility/MVKStateTracking.h +++ b/MoltenVK/MoltenVK/Utility/MVKStateTracking.h @@ -344,6 +344,7 @@ struct MVKRenderStateData { uint8_t cullMode = MTLCullModeNone; uint8_t frontFace = MTLWindingClockwise; uint8_t primitiveType = MTLPrimitiveTypePoint; + uint8_t vkPrimitiveTopology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; // primitiveType cannot tell a triangle fan from a list MVKPolygonMode polygonMode = MVKPolygonMode::Fill; MVKLineRasterizationMode lineRasterizationMode = MVKLineRasterizationMode::Default; MVKRenderStateEnableFlags enable; diff --git a/MoltenVK/MoltenVK/Vulkan/vulkan.mm b/MoltenVK/MoltenVK/Vulkan/vulkan.mm index 9ee0c4ebc..26c78dffb 100644 --- a/MoltenVK/MoltenVK/Vulkan/vulkan.mm +++ b/MoltenVK/MoltenVK/Vulkan/vulkan.mm @@ -791,11 +791,12 @@ MVK_PUBLIC_VULKAN_SYMBOL VkResult vkGetEventStatus( VkEvent event) { MVKTraceVulkanCallStart(); - VkResult rslt = MVKDevice::getMVKDevice(device)->getConfigurationResult(); - if (rslt == VK_SUCCESS) { - MVKEvent* mvkEvent = (MVKEvent*)event; - rslt = mvkEvent->isSet() ? VK_EVENT_SET : VK_EVENT_RESET; - } + // Like vkGetSemaphoreCounterValue: the release of Metal waits during a device loss may set the event. + MVKDevice* mvkDev = MVKDevice::getMVKDevice(device); + bool isSet = ((MVKEvent*)event)->isSet(); + std::atomic_thread_fence(std::memory_order_seq_cst); + VkResult rslt = mvkDev->isLosing() ? mvkDev->getHostWaitResult(true, UINT64_MAX) : mvkDev->getConfigurationResult(); + if (rslt == VK_SUCCESS) { rslt = isSet ? VK_EVENT_SET : VK_EVENT_RESET; } MVKTraceVulkanCallEnd(); return rslt; } @@ -2480,11 +2481,15 @@ MVK_PUBLIC_VULKAN_SYMBOL VkResult vkGetSemaphoreCounterValue( uint64_t* pValue) { MVKTraceVulkanCallStart(); - VkResult rslt = MVKDevice::getMVKDevice(device)->getConfigurationResult(); - if (rslt == VK_SUCCESS) { - auto* mvkSem4 = (MVKTimelineSemaphore*)semaphore; - *pValue = mvkSem4->getCounterValue(); - } + // During a device loss, the release of Metal waits may raise the counter past work that never ran, while + // submissions may still be reading their command buffers: report the loss once it is published. The counter is + // read before the loss is checked, and the fence orders the two reads: a value raised by that release was written + // after the loss began, so the check that follows sees the loss. + MVKDevice* mvkDev = MVKDevice::getMVKDevice(device); + uint64_t value = ((MVKTimelineSemaphore*)semaphore)->getCounterValue(); + std::atomic_thread_fence(std::memory_order_seq_cst); + VkResult rslt = mvkDev->isLosing() ? mvkDev->getHostWaitResult(true, UINT64_MAX) : mvkDev->getConfigurationResult(); + if (rslt == VK_SUCCESS) { *pValue = value; } MVKTraceVulkanCallEnd(); return rslt; } @@ -4300,6 +4305,57 @@ MVK_PUBLIC_VULKAN_SYMBOL VkResult vkCreateHeadlessSurfaceEXT( #pragma mark - #pragma mark VK_EXT_multi_draw extension +#pragma mark - +#pragma mark VK_EXT_mesh_shader extension + +MVK_PUBLIC_VULKAN_SYMBOL void vkCmdDrawMeshTasksEXT( + VkCommandBuffer commandBuffer, + uint32_t groupCountX, + uint32_t groupCountY, + uint32_t groupCountZ) { + + MVKTraceVulkanCallStart(); + MVKAddCmd(DrawMeshTasks, commandBuffer, groupCountX, groupCountY, groupCountZ); + MVKTraceVulkanCallEnd(); +} + +// The first mesh path draws direct mesh workgroups only: indirect mesh draws fail the command buffer. +static void mvkRefuseIndirectMeshDraw(VkCommandBuffer commandBuffer, const char* command) { + MVKCommandBuffer* cmdBuff = MVKCommandBuffer::getMVKCommandBuffer(commandBuffer); + cmdBuff->setConfigurationResult(cmdBuff->reportError(VK_ERROR_FEATURE_NOT_PRESENT, "%s(): Indirect mesh draws are not supported.", command)); +} + +MVK_PUBLIC_VULKAN_SYMBOL void vkCmdDrawMeshTasksIndirectEXT( + VkCommandBuffer commandBuffer, + VkBuffer buffer, + VkDeviceSize offset, + uint32_t drawCount, + uint32_t stride) { + + MVKTraceVulkanCallStart(); + // MVK_CONFIG_MESH_INDIRECT_GPU=1: a single indirect grid, sized on the GPU (MVKCmdDrawMeshTasksIndirect). + if (mvkMeshIndirectGpu() && drawCount <= 1) { + if (drawCount) { MVKAddCmd(DrawMeshTasksIndirect, commandBuffer, buffer, offset); } + } else { + mvkRefuseIndirectMeshDraw(commandBuffer, "vkCmdDrawMeshTasksIndirectEXT"); + } + MVKTraceVulkanCallEnd(); +} + +MVK_PUBLIC_VULKAN_SYMBOL void vkCmdDrawMeshTasksIndirectCountEXT( + VkCommandBuffer commandBuffer, + VkBuffer buffer, + VkDeviceSize offset, + VkBuffer countBuffer, + VkDeviceSize countBufferOffset, + uint32_t maxDrawCount, + uint32_t stride) { + + MVKTraceVulkanCallStart(); + mvkRefuseIndirectMeshDraw(commandBuffer, "vkCmdDrawMeshTasksIndirectCountEXT"); + MVKTraceVulkanCallEnd(); +} + MVK_PUBLIC_VULKAN_SYMBOL void vkCmdDrawMultiEXT( VkCommandBuffer commandBuffer, uint32_t drawCount, diff --git a/MoltenVK/Tests/MacFamilyHarness/probe.mm b/MoltenVK/Tests/MacFamilyHarness/probe.mm new file mode 100644 index 000000000..00bd32a5b --- /dev/null +++ b/MoltenVK/Tests/MacFamilyHarness/probe.mm @@ -0,0 +1,170 @@ +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Capability probe of the Mac family harness: what Metal and the bundled MoltenVK actually expose on this Mac, before +// and after the test-only device adapters. Creates a Metal device, a Vulkan instance and the physical device list, and +// submits no GPU work. Prints key=value lines; the runner derives every eligibility decision from them, never from the +// name of the Mac or of its GPU, which are printed for the record only. +#import +#import +#define VK_ENABLE_BETA_EXTENSIONS 1 +#include +#include +#include +#include +#include +#include + +static void emit(const std::string& key, long long value) { std::printf("%s=%lld\n", key.c_str(), value); } +static void emit(const std::string& key, const char* value) { std::printf("%s=%s\n", key.c_str(), value); } + +static void probeMetal() { + id device = MTLCreateSystemDefaultDevice(); + emit("mtl.present", device != nil); + if (!device) return; + emit("mtl.name", device.name.UTF8String); + emit("mtl.registry_id", (long long)device.registryID); + // Family values as plain numbers: supportsFamily: answers NO for families this OS does not know. + for (int family = 1; family <= 11; ++family) emit("mtl.family.apple" + std::to_string(family), [device supportsFamily:(MTLGPUFamily)(1000 + family)]); + emit("mtl.family.mac2", [device supportsFamily:(MTLGPUFamily)2002]); + long long metal3 = 0, metal4 = 0; + if (@available(macOS 13.0, *)) metal3 = [device supportsFamily:MTLGPUFamilyMetal3]; + if (@available(macOS 26.0, *)) metal4 = [device supportsFamily:MTLGPUFamilyMetal4]; + emit("mtl.family.metal3", metal3); + emit("mtl.family.metal4", metal4); + emit("mtl.barycentric_coordinates", device.supportsShaderBarycentricCoordinates); + emit("mtl.argument_buffers_tier", (long long)device.argumentBuffersSupport + 1); + emit("mtl.max_buffer_length", (long long)device.maxBufferLength); + emit("mtl.unified_memory", device.hasUnifiedMemory); + emit("mtl.recommended_max_working_set_size", (long long)device.recommendedMaxWorkingSetSize); + emit("mtl.low_power", device.isLowPower); + emit("mtl.headless", device.isHeadless); + emit("mtl.removable", device.isRemovable); +} + +static bool has(const std::vector& extensions, const char* name) { + for (const auto& extension : extensions) if (!std::strcmp(extension.extensionName, name)) return true; + return false; +} + +static std::vector extensionsOf(VkPhysicalDevice physical) { + uint32_t count = 0; + vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, nullptr); + std::vector extensions(count); + vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, extensions.data()); + return extensions; +} + +// Extensions and features the harness cases depend on, under a prefix: vk.public.* before the adapters, vk.* after. +static void probeDevice(VkPhysicalDevice physical, const std::string& prefix) { + auto extensions = extensionsOf(physical); + emit(prefix + "extension_count", (long long)extensions.size()); + for (const char* name : {VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME, VK_EXT_MESH_SHADER_EXTENSION_NAME, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME, + VK_EXT_EXTENDED_DYNAMIC_STATE_2_EXTENSION_NAME, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME, VK_KHR_MAINTENANCE_2_EXTENSION_NAME, + VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME, VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME}) + emit(prefix + "ext." + name, has(extensions, name)); + VkPhysicalDeviceMeshShaderFeaturesEXT mesh{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT}; + VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR}; + barycentric.pNext = &mesh; + VkPhysicalDeviceExtendedDynamicState2FeaturesEXT dynamicState2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT}; + dynamicState2.pNext = &barycentric; + VkPhysicalDeviceVulkan12Features vulkan12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES}; + vulkan12.pNext = &dynamicState2; + VkPhysicalDeviceVulkan11Features vulkan11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES}; + vulkan11.pNext = &vulkan12; + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + features.pNext = &vulkan11; + vkGetPhysicalDeviceFeatures2(physical, &features); + const VkPhysicalDeviceFeatures& core = features.features; + emit(prefix + "feature.fragmentShaderBarycentric", barycentric.fragmentShaderBarycentric); + emit(prefix + "feature.meshShader", mesh.meshShader); + emit(prefix + "feature.taskShader", mesh.taskShader); + if (prefix != "vk.") return; + emit("vk.feature.tessellationShader", core.tessellationShader); + emit("vk.feature.multiDrawIndirect", core.multiDrawIndirect); + emit("vk.feature.drawIndirectFirstInstance", core.drawIndirectFirstInstance); + emit("vk.feature.vertexPipelineStoresAndAtomics", core.vertexPipelineStoresAndAtomics); + emit("vk.feature.fragmentStoresAndAtomics", core.fragmentStoresAndAtomics); + emit("vk.feature.shaderInt16", core.shaderInt16); + emit("vk.feature.geometryShader", core.geometryShader); + emit("vk.feature.multiview", vulkan11.multiview); + emit("vk.feature.shaderDrawParameters", vulkan11.shaderDrawParameters); + emit("vk.feature.storageInputOutput16", vulkan11.storageInputOutput16); + emit("vk.feature.bufferDeviceAddress", vulkan12.bufferDeviceAddress); + emit("vk.feature.shaderFloat16", vulkan12.shaderFloat16); + emit("vk.feature.drawIndirectCount", vulkan12.drawIndirectCount); + emit("vk.feature.timelineSemaphore", vulkan12.timelineSemaphore); + emit("vk.feature.extendedDynamicState2PatchControlPoints", dynamicState2.extendedDynamicState2PatchControlPoints); + VkPhysicalDeviceMeshShaderPropertiesEXT meshProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; + VkPhysicalDeviceVulkan11Properties properties11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES}; + properties11.pNext = mesh.meshShader ? &meshProperties : nullptr; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; + properties.pNext = &properties11; + vkGetPhysicalDeviceProperties2(physical, &properties); + const VkPhysicalDeviceLimits& limits = properties.properties.limits; + emit("vk.limit.maxTessellationGenerationLevel", limits.maxTessellationGenerationLevel); + emit("vk.limit.maxTessellationPatchSize", limits.maxTessellationPatchSize); + emit("vk.limit.maxFragmentInputComponents", limits.maxFragmentInputComponents); + emit("vk.limit.maxDrawIndirectCount", limits.maxDrawIndirectCount); + emit("vk.limit.maxMultiviewViewCount", properties11.maxMultiviewViewCount); + emit("vk.limit.maxMemoryAllocationSize", (long long)properties11.maxMemoryAllocationSize); + if (mesh.meshShader) { + emit("vk.limit.maxMeshWorkGroupInvocations", meshProperties.maxMeshWorkGroupInvocations); + emit("vk.limit.maxMeshOutputVertices", meshProperties.maxMeshOutputVertices); + emit("vk.limit.maxMeshOutputPrimitives", meshProperties.maxMeshOutputPrimitives); + } +} + +int main() { + @autoreleasepool { + emit("os.version", NSProcessInfo.processInfo.operatingSystemVersionString.UTF8String); + probeMetal(); + uint32_t instanceVersion = 0; + vkEnumerateInstanceVersion(&instanceVersion); + emit("vk.instance_version", (long long)instanceVersion); + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.pApplicationName = "Mac family harness probe"; + app.apiVersion = VK_API_VERSION_1_3; + VkInstanceCreateInfo instanceInfo{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + instanceInfo.pApplicationInfo = &app; + VkInstance instance = VK_NULL_HANDLE; + VkResult created = vkCreateInstance(&instanceInfo, nullptr, &instance); + emit("vk.instance", created == VK_SUCCESS); + if (created != VK_SUCCESS) { emit("vk.instance_result", created); return 0; } + uint32_t count = 0; + vkEnumeratePhysicalDevices(instance, &count, nullptr); + std::vector devices(count); + vkEnumeratePhysicalDevices(instance, &count, devices.data()); + emit("vk.physical_device_count", count); + if (!count) return 0; + // The fixtures use the first physical device, as does this probe. + VkPhysicalDevice physical = devices[0]; + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physical, &properties); + emit("vk.device_name", properties.deviceName); + emit("vk.api_version", properties.apiVersion); + emit("vk.driver_version", properties.driverVersion); + emit("vk.vendor_id", properties.vendorID); + emit("vk.device_id", properties.deviceID); + // MoltenVK stores its git revision, big-endian, in the first four bytes of the pipeline cache UUID. + char revision[9]; + std::snprintf(revision, sizeof(revision), "%02x%02x%02x%02x", properties.pipelineCacheUUID[0], properties.pipelineCacheUUID[1], + properties.pipelineCacheUUID[2], properties.pipelineCacheUUID[3]); + emit("vk.mvk_revision", revision); + probeDevice(physical, "vk.public."); + auto tes = reinterpret_cast(dlsym(RTLD_DEFAULT, "mvkEnableTESFixtureDevice")); +#ifdef PROBE_BARYCENTRIC_ADAPTER_ONLY + // The experimental candidate activates only VK_KHR_fragment_shader_barycentric. + void (*mesh)(VkPhysicalDevice) = nullptr; +#else + auto mesh = reinterpret_cast(dlsym(RTLD_DEFAULT, "mvkEnableMeshTestDevice")); +#endif + emit("vk.adapter.tes_linked", tes != nullptr); + emit("vk.adapter.mesh_linked", mesh != nullptr); + if (tes) tes(physical); + if (mesh) mesh(physical); + probeDevice(physical, "vk."); + vkDestroyInstance(instance, nullptr); + emit("probe.complete", 1); + } + return 0; +} diff --git a/MoltenVK/Tests/PerVertexAttachmentSplitTests.mm b/MoltenVK/Tests/PerVertexAttachmentSplitTests.mm new file mode 100644 index 000000000..7de3761c8 --- /dev/null +++ b/MoltenVK/Tests/PerVertexAttachmentSplitTests.mm @@ -0,0 +1,405 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Attachment content across the capture/replay of a direct non-indexed portable PerVertexKHR draw. A 64x64 render pass +// writes content before the PerVertex draw, which must still hold for that draw and after it: +// - memoryless-depth: an imported memoryless D32 attachment, cleared to 0 on the left half; the triangle, at depth 0.5 +// with LESS, must stay hidden there; +// - memoryless-stencil: an imported memoryless D32S8 attachment, stencil set to 1 on the left half; the triangle, +// with stencil EQUAL 0, must stay hidden there; +// - msaa: an ordinary 4x color attachment resolved to the readback image, red on the left half before the draw; the +// triangle is scissored to the right half, and the left must stay red; +// - msaa-memoryless: the same with an imported memoryless 4x color attachment; +// - indirect-count-0, indirect-count-1: no other attachment; the same triangle through vkCmdDrawIndirectCount with a +// 1 GiB argument buffer, stride 16 and the largest legal maxDrawCount (2^26), on a device whose Metal buffer limit +// the test build lowers to 1 GiB. Count 1 draws the triangle, Count 0 nothing. +// Outcome: REFUSED when vkEndCommandBuffer refuses the draw, RENDERED when the image matches, WRONG otherwise. +// Exit 0 for RENDERED, 4 for REFUSED, 1 for WRONG or a device loss, 2 for a setup failure, 3 for a missing capability. +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t kSide = 64; + +void check(VkResult result, const char* step) { + if (result != VK_SUCCESS) { std::fprintf(stderr, "%s: %d\n", step, int(result)); std::exit(2); } +} + +std::vector readSpirv(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { std::fprintf(stderr, "Cannot read %s\n", path.c_str()); std::exit(2); } + std::vector words(size_t(file.tellg()) / 4); + file.seekg(0); + file.read(reinterpret_cast(words.data()), words.size() * 4); + return words; +} + +uint32_t memoryType(VkPhysicalDevice physical, uint32_t bits, VkMemoryPropertyFlags flags) { + VkPhysicalDeviceMemoryProperties properties; + vkGetPhysicalDeviceMemoryProperties(physical, &properties); + for (uint32_t i = 0; i < properties.memoryTypeCount; ++i) { + if ((bits & (1u << i)) && (properties.memoryTypes[i].propertyFlags & flags) == flags) { return i; } + } + std::fprintf(stderr, "No memory type\n"); + std::exit(3); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 3) { std::fprintf(stderr, "usage: %s memoryless-depth|memoryless-stencil|msaa|msaa-memoryless\n", argv[0]); return 64; } + std::string shaders = argv[1], variant = argv[2]; + bool depth = variant == "memoryless-depth", stencil = variant == "memoryless-stencil"; + bool msaa = variant == "msaa" || variant == "msaa-memoryless", memorylessColor = variant == "msaa-memoryless"; + bool indirectCount = variant == "indirect-count-0" || variant == "indirect-count-1"; + uint32_t drawnCount = variant == "indirect-count-1" ? 1 : 0; + constexpr VkDeviceSize kArgumentBytes = 1ull << 30; + if (!depth && !stencil && !msaa && !indirectCount) { std::fprintf(stderr, "unknown variant %s\n", variant.c_str()); return 64; } + VkSampleCountFlagBits samples = msaa ? VK_SAMPLE_COUNT_4_BIT : VK_SAMPLE_COUNT_1_BIT; + + @autoreleasepool { + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.apiVersion = VK_API_VERSION_1_2; + VkInstanceCreateInfo instanceInfo{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + instanceInfo.pApplicationInfo = &app; + VkInstance instance; + check(vkCreateInstance(&instanceInfo, nullptr, &instance), "vkCreateInstance"); + uint32_t count = 1; + VkPhysicalDevice physical; + VkResult enumerated = vkEnumeratePhysicalDevices(instance, &count, &physical); + if ((enumerated != VK_SUCCESS && enumerated != VK_INCOMPLETE) || !count) { std::fprintf(stderr, "No Vulkan device\n"); return 3; } + // The test build opens VK_KHR_fragment_shader_barycentric through its adapter, as the PerVertex fixtures do. + auto enable = reinterpret_cast(dlsym(RTLD_DEFAULT, "mvkEnableTESFixtureDevice")); + if (!enable) { std::fprintf(stderr, "Needs a MoltenVK test build with its test device adapter\n"); return 3; } + enable(physical); + if (indirectCount) { + auto limit = reinterpret_cast(dlsym(RTLD_DEFAULT, "mvkTestLimitMetalBufferSize")); + if (!limit) { std::fprintf(stderr, "Needs a MoltenVK test build with mvkTestLimitMetalBufferSize\n"); return 3; } + limit(physical, kArgumentBytes); + } + VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR}; + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + features.pNext = &barycentric; + vkGetPhysicalDeviceFeatures2(physical, &features); + if (!barycentric.fragmentShaderBarycentric) { std::fprintf(stderr, "No fragment shader barycentric feature\n"); return 3; } + if (indirectCount && !features.features.multiDrawIndirect) { std::fprintf(stderr, "No multiDrawIndirect\n"); return 3; } + VkPhysicalDeviceFeatures2 enabled{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + enabled.pNext = &barycentric; + enabled.features.multiDrawIndirect = indirectCount; + float priority = 1; + VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &priority; + const char* extensions[] = {VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME, VK_EXT_METAL_OBJECTS_EXTENSION_NAME, "VK_KHR_portability_subset", VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME}; + VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + deviceInfo.pNext = &enabled; + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + deviceInfo.enabledExtensionCount = 4; + deviceInfo.ppEnabledExtensionNames = extensions; + VkDevice device; + check(vkCreateDevice(physical, &deviceInfo, nullptr, &device), "vkCreateDevice"); + VkQueue queue; + vkGetDeviceQueue(device, 0, 0, &queue); + VkExportMetalDeviceInfoEXT metalDevice{VK_STRUCTURE_TYPE_EXPORT_METAL_DEVICE_INFO_EXT}; + VkExportMetalObjectsInfoEXT exportInfo{VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECTS_INFO_EXT}; + exportInfo.pNext = &metalDevice; + vkExportMetalObjectsEXT(device, &exportInfo); + if (!metalDevice.mtlDevice) { std::fprintf(stderr, "No exported MTLDevice\n"); return 3; } + + // Images: the attachment under test, and the single-sample color image that is read back. + auto makeImage = [&](VkFormat format, VkImageUsageFlags usage, VkSampleCountFlagBits sampleCount, id imported, VkImage& image, VkDeviceMemory& memory) { + VkImportMetalTextureInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_METAL_TEXTURE_INFO_EXT}; + import.plane = VK_IMAGE_ASPECT_PLANE_0_BIT; + import.mtlTexture = imported; + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.pNext = imported ? &import : nullptr; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {kSide, kSide, 1}; + info.mipLevels = info.arrayLayers = 1; + info.samples = sampleCount; + info.usage = usage; + check(vkCreateImage(device, &info, nullptr, &image), "vkCreateImage"); + VkMemoryRequirements requirements; + vkGetImageMemoryRequirements(device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = memoryType(physical, requirements.memoryTypeBits, 0); + check(vkAllocateMemory(device, &allocation, nullptr, &memory), "vkAllocateMemory image"); + check(vkBindImageMemory(device, image, memory, 0), "vkBindImageMemory"); + }; + auto memorylessTexture = [&](MTLPixelFormat format, NSUInteger sampleCount) { + MTLTextureDescriptor* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format width:kSide height:kSide mipmapped:NO]; + descriptor.textureType = sampleCount > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + descriptor.sampleCount = sampleCount; + descriptor.storageMode = MTLStorageModeMemoryless; + descriptor.usage = MTLTextureUsageRenderTarget; + id texture = [metalDevice.mtlDevice newTextureWithDescriptor:descriptor]; + if (!texture) { std::fprintf(stderr, "Memoryless textures unavailable\n"); std::exit(3); } + return texture; + }; + VkImage colorImage, testImage = VK_NULL_HANDLE; + VkDeviceMemory colorMemory, testMemory = VK_NULL_HANDLE; + makeImage(VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_SAMPLE_COUNT_1_BIT, nil, colorImage, colorMemory); + VkFormat testFormat = depth ? VK_FORMAT_D32_SFLOAT : stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_R8G8B8A8_UNORM; + id imported = nil; + if (depth) { imported = memorylessTexture(MTLPixelFormatDepth32Float, 1); } + if (stencil) { imported = memorylessTexture(MTLPixelFormatDepth32Float_Stencil8, 1); } + if (memorylessColor) { imported = memorylessTexture(MTLPixelFormatRGBA8Unorm, 4); } + VkImageUsageFlags testUsage = (msaa ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) | (imported ? VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT : 0); + if (!indirectCount) { makeImage(testFormat, testUsage, samples, imported, testImage, testMemory); } + VkImageAspectFlags testAspect = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : stencil ? VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + auto view = [&](VkImage image, VkFormat format, VkImageAspectFlags aspect) { + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0, 1, 0, 1}; + VkImageView result; + check(vkCreateImageView(device, &info, nullptr, &result), "vkCreateImageView"); + return result; + }; + VkImageView colorView = view(colorImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT); + VkImageView testView = indirectCount ? VK_NULL_HANDLE : view(testImage, testFormat, testAspect); + + // Render pass: color 0 (single sample, or MSAA resolved into it), and the depth/stencil attachment. + std::vector attachments; + VkAttachmentDescription color{}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; + color.samples = VK_SAMPLE_COUNT_1_BIT; + color.loadOp = msaa ? VK_ATTACHMENT_LOAD_OP_DONT_CARE : VK_ATTACHMENT_LOAD_OP_CLEAR; + color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + attachments.push_back(color); + VkAttachmentDescription test{}; + test.format = testFormat; + test.samples = samples; + test.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + test.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + test.stencilLoadOp = stencil ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + test.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + test.finalLayout = msaa ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + if (!indirectCount) { attachments.push_back(test); } + VkAttachmentReference colorRef{msaa ? 1u : 0u, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference resolveRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pResolveAttachments = msaa ? &resolveRef : nullptr; + subpass.pDepthStencilAttachment = msaa || indirectCount ? nullptr : &depthRef; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = uint32_t(attachments.size()); + passInfo.pAttachments = attachments.data(); + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + VkRenderPass renderPass; + check(vkCreateRenderPass(device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass"); + VkImageView views[] = {colorView, testView}; + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = indirectCount ? 1 : 2; + framebufferInfo.pAttachments = views; + framebufferInfo.width = framebufferInfo.height = kSide; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer; + check(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer"); + + // A full-screen green triangle at depth 0.5, whose fragment reads its PerVertexKHR color. + auto module = [&](const char* name) { + std::vector code = readSpirv(shaders + "/" + name); + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = code.size() * 4; + info.pCode = code.data(); + VkShaderModule result; + check(vkCreateShaderModule(device, &info, nullptr, &result), "vkCreateShaderModule"); + return result; + }; + VkShaderModule vertex = module("split.vert.spv"), fragment = module("split.frag.spv"); + VkPipelineShaderStageCreateInfo stages[2] = {{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkViewport viewport{0, 0, float(kSide), float(kSide), 0, 1}; + VkRect2D scissor{{msaa ? int32_t(kSide / 2) : 0, 0}, {msaa ? kSide / 2 : kSide, kSide}}; + VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportState.viewportCount = viewportState.scissorCount = 1; + viewportState.pViewports = &viewport; + viewportState.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.lineWidth = 1; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = samples; + VkPipelineDepthStencilStateCreateInfo depthState{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthState.depthTestEnable = depth; + depthState.depthWriteEnable = depth; + depthState.depthCompareOp = VK_COMPARE_OP_LESS; + depthState.stencilTestEnable = stencil; + depthState.front.compareOp = VK_COMPARE_OP_EQUAL; + depthState.front.compareMask = 0xFF; + depthState.front.reference = 0; + depthState.front.failOp = depthState.front.passOp = depthState.front.depthFailOp = VK_STENCIL_OP_KEEP; + depthState.back = depthState.front; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = 0xF; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + VkPipelineLayout layout; + check(vkCreatePipelineLayout(device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout"); + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewportState; + pipelineInfo.pRasterizationState = &raster; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pDepthStencilState = msaa || indirectCount ? nullptr : &depthState; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.layout = layout; + pipelineInfo.renderPass = renderPass; + VkPipeline pipeline; + check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + + // Commands: clear, write the left half before the draw, draw, copy the color image to a host buffer. + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = kSide * kSide * 4; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + VkBuffer readback; + check(vkCreateBuffer(device, &bufferInfo, nullptr, &readback), "vkCreateBuffer"); + VkMemoryRequirements bufferRequirements; + vkGetBufferMemoryRequirements(device, readback, &bufferRequirements); + VkMemoryAllocateInfo bufferAllocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + bufferAllocation.allocationSize = bufferRequirements.size; + bufferAllocation.memoryTypeIndex = memoryType(physical, bufferRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + VkDeviceMemory readbackMemory; + check(vkAllocateMemory(device, &bufferAllocation, nullptr, &readbackMemory), "vkAllocateMemory readback"); + check(vkBindBufferMemory(device, readback, readbackMemory, 0), "vkBindBufferMemory"); + void* pixels = nullptr; + check(vkMapMemory(device, readbackMemory, 0, VK_WHOLE_SIZE, 0, &pixels), "vkMapMemory"); + std::memset(pixels, 0, kSide * kSide * 4); + // The argument buffer holds the triangle as its first command; the count buffer holds Count. + VkBuffer arguments = VK_NULL_HANDLE, countBuffer = VK_NULL_HANDLE; + VkDeviceMemory argumentMemory = VK_NULL_HANDLE, countMemory = VK_NULL_HANDLE; + auto hostBuffer = [&](VkDeviceSize size, VkBuffer& buffer, VkDeviceMemory& memory) { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = size; + info.usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; + check(vkCreateBuffer(device, &info, nullptr, &buffer), "vkCreateBuffer indirect"); + VkMemoryRequirements requirements; + vkGetBufferMemoryRequirements(device, buffer, &requirements); + VkMemoryAllocateInfo allocationInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocationInfo.allocationSize = requirements.size; + allocationInfo.memoryTypeIndex = memoryType(physical, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + check(vkAllocateMemory(device, &allocationInfo, nullptr, &memory), "vkAllocateMemory indirect"); + check(vkBindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory indirect"); + void* data = nullptr; + check(vkMapMemory(device, memory, 0, 64, 0, &data), "vkMapMemory indirect"); + return static_cast(data); + }; + if (indirectCount) { + uint32_t* command = hostBuffer(kArgumentBytes, arguments, argumentMemory); + command[0] = 3; command[1] = 1; command[2] = 0; command[3] = 0; + hostBuffer(4, countBuffer, countMemory)[0] = drawnCount; + } + VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + VkCommandPool pool; + check(vkCreateCommandPool(device, &poolInfo, nullptr, &pool), "vkCreateCommandPool"); + VkCommandBufferAllocateInfo allocation{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + allocation.commandPool = pool; + allocation.commandBufferCount = 1; + VkCommandBuffer commands; + check(vkAllocateCommandBuffers(device, &allocation, &commands), "vkAllocateCommandBuffers"); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + check(vkBeginCommandBuffer(commands, &begin), "vkBeginCommandBuffer"); + VkClearValue clears[2]{}; + clears[0].color.float32[2] = clears[0].color.float32[3] = 1; // blue + clears[1] = clears[0]; + if (!msaa && !indirectCount) { clears[1].depthStencil = {1.0f, 0}; } + VkRenderPassBeginInfo passBegin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + passBegin.renderPass = renderPass; + passBegin.framebuffer = framebuffer; + passBegin.renderArea = {{0, 0}, {kSide, kSide}}; + passBegin.clearValueCount = indirectCount ? 1 : 2; + passBegin.pClearValues = clears; + vkCmdBeginRenderPass(commands, &passBegin, VK_SUBPASS_CONTENTS_INLINE); + VkClearAttachment left{}; + VkClearRect leftRect{{{0, 0}, {kSide / 2, kSide}}, 0, 1}; + if (msaa) { + left.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + left.clearValue.color.float32[0] = left.clearValue.color.float32[3] = 1; // red + } else { + left.aspectMask = depth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_STENCIL_BIT; + left.clearValue.depthStencil = {0.0f, 1}; + } + if (!indirectCount) { vkCmdClearAttachments(commands, 1, &left, 1, &leftRect); } + vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + if (indirectCount) { + vkCmdDrawIndirectCountKHR(commands, arguments, 0, countBuffer, 0, uint32_t(kArgumentBytes / 16), 16); + } else { + vkCmdDraw(commands, 3, 1, 0, 0); + } + vkCmdEndRenderPass(commands); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {kSide, kSide, 1}; + vkCmdCopyImageToBuffer(commands, colorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ©); + VkResult ended = vkEndCommandBuffer(commands); + std::printf("variant=%s vkEndCommandBuffer=%d\n", variant.c_str(), int(ended)); + int rc; + if (ended != VK_SUCCESS) { + std::printf("SPLIT_ATTACHMENT REFUSED: the draw was refused before submission (%d)\n", int(ended)); + rc = 4; + } else { + check(ended, "vkEndCommandBuffer"); + VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence; + check(vkCreateFence(device, &fenceInfo, nullptr, &fence), "vkCreateFence"); + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &commands; + VkResult submitted = vkQueueSubmit(queue, 1, &submit, fence); + VkResult waited = submitted == VK_SUCCESS ? vkWaitForFences(device, 1, &fence, VK_TRUE, 5000000000ull) : submitted; + // Left half keeps what was written before the draw (blue for a hidden triangle, red for MSAA); right is green. + uint32_t wrong = 0; + const uint8_t* p = static_cast(pixels); + for (uint32_t y = 0; y < kSide; ++y) { + for (uint32_t x = 0; x < kSide; ++x, p += 4) { + bool leftHalf = x < kSide / 2; + uint8_t want[4] = {uint8_t(leftHalf && msaa ? 255 : 0), uint8_t(leftHalf ? 0 : 255), uint8_t(leftHalf && !msaa ? 255 : 0), 255}; + if (indirectCount) { want[0] = 0; want[1] = drawnCount ? 255 : 0; want[2] = drawnCount ? 0 : 255; } + wrong += std::memcmp(p, want, 4) != 0; + } + } + const uint8_t* corner = static_cast(pixels); + std::printf("submit=%d wait=%d wrong_pixels=%u left_pixel=%02x%02x%02x%02x\n", int(submitted), int(waited), wrong, corner[0], corner[1], corner[2], corner[3]); + bool rendered = waited == VK_SUCCESS && wrong == 0; + std::printf("SPLIT_ATTACHMENT %s\n", rendered ? "RENDERED: content before the draw was preserved" : "WRONG: content was lost or the draw failed"); + rc = rendered ? 0 : 1; + vkDestroyFence(device, fence, nullptr); + } + std::fflush(stdout); + vkDeviceWaitIdle(device); + vkDestroyDevice(device, nullptr); + vkDestroyInstance(instance, nullptr); + return rc; + } +} diff --git a/MoltenVK/Tests/PerVertexCTS/AdapterShim.c b/MoltenVK/Tests/PerVertexCTS/AdapterShim.c new file mode 100644 index 000000000..47b1f10ec --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/AdapterShim.c @@ -0,0 +1,74 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// The Vulkan library given to the CTS (--deqp-vk-library-path): it loads the MoltenVK test build named by +// PERVERTEX_CTS_MOLTENVK and forwards every entry point to it. The only difference is on the physical devices that +// vkEnumeratePhysicalDevices and vkEnumeratePhysicalDeviceGroups return: before the CTS can query their extensions or +// features, it calls the test build's mvkEnableTESFixtureDevice and mvkEnableMeshTestDevice on each of them, as the +// PerVertex fixtures do. The public extension gate of MoltenVK is never changed. +#include +#include +#include +#include +#include + +typedef void (*EnableFunction)(VkPhysicalDevice); + +static PFN_vkGetInstanceProcAddr nextGetInstanceProcAddr; +static PFN_vkEnumeratePhysicalDevices nextEnumeratePhysicalDevices; +static PFN_vkEnumeratePhysicalDeviceGroups nextEnumeratePhysicalDeviceGroups; +static EnableFunction enableBarycentric, enableMesh; + +static int load(void) { + if (nextGetInstanceProcAddr) { return 1; } + const char* path = getenv("PERVERTEX_CTS_MOLTENVK"); + void* library = path ? dlopen(path, RTLD_NOW | RTLD_LOCAL) : NULL; + if (!library) { + fprintf(stderr, "PERVERTEX_CTS_SHIM ERROR: cannot load PERVERTEX_CTS_MOLTENVK=%s: %s\n", path ? path : "(unset)", dlerror()); + return 0; + } + enableBarycentric = (EnableFunction)dlsym(library, "mvkEnableTESFixtureDevice"); + enableMesh = (EnableFunction)dlsym(library, "mvkEnableMeshTestDevice"); + if (!enableBarycentric || !enableMesh) { + fprintf(stderr, "PERVERTEX_CTS_SHIM ERROR: %s is not a MoltenVK test build with the test device adapters\n", path); + return 0; + } + nextGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(library, "vkGetInstanceProcAddr"); + return nextGetInstanceProcAddr != NULL; +} + +static void openAdapters(uint32_t count, const VkPhysicalDevice* devices) { + for (uint32_t i = 0; i < count; ++i) { + enableBarycentric(devices[i]); + enableMesh(devices[i]); + } +} + +static VKAPI_ATTR VkResult VKAPI_CALL enumeratePhysicalDevices(VkInstance instance, uint32_t* count, VkPhysicalDevice* devices) { + VkResult result = nextEnumeratePhysicalDevices(instance, count, devices); + if (devices && (result == VK_SUCCESS || result == VK_INCOMPLETE)) { openAdapters(*count, devices); } + return result; +} + +static VKAPI_ATTR VkResult VKAPI_CALL enumeratePhysicalDeviceGroups(VkInstance instance, uint32_t* count, VkPhysicalDeviceGroupProperties* groups) { + VkResult result = nextEnumeratePhysicalDeviceGroups(instance, count, groups); + if (groups && (result == VK_SUCCESS || result == VK_INCOMPLETE)) { + for (uint32_t i = 0; i < *count; ++i) { openAdapters(groups[i].physicalDeviceCount, groups[i].physicalDevices); } + } + return result; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance, const char* name) { + if (!load()) { return NULL; } + PFN_vkVoidFunction function = nextGetInstanceProcAddr(instance, name); + if (!function || !name) { return function; } + // MoltenVK returns the same entry points for every instance, so one saved pointer serves them all. + if (!strcmp(name, "vkEnumeratePhysicalDevices")) { + nextEnumeratePhysicalDevices = (PFN_vkEnumeratePhysicalDevices)function; + return (PFN_vkVoidFunction)enumeratePhysicalDevices; + } + if (!strcmp(name, "vkEnumeratePhysicalDeviceGroups") || !strcmp(name, "vkEnumeratePhysicalDeviceGroupsKHR")) { + nextEnumeratePhysicalDeviceGroups = (PFN_vkEnumeratePhysicalDeviceGroups)function; + return (PFN_vkVoidFunction)enumeratePhysicalDeviceGroups; + } + return function; +} diff --git a/MoltenVK/Tests/PerVertexCTS/experimental-vsfs-barycentric.txt b/MoltenVK/Tests/PerVertexCTS/experimental-vsfs-barycentric.txt new file mode 100644 index 000000000..ca396fcbb --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/experimental-vsfs-barycentric.txt @@ -0,0 +1,1201 @@ +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.line_strip.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.misc.pervertex_correctness +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.point_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_fan.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_list.clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.dynamic.triangle_strip.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.point_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.line_strip.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_fan.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_list.clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.type.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.struct.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.float.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.float.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.float.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec2.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec3.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec4.per_vertex_interp.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.vec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.int.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.int.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.ivec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uint.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uint.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec2.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec2.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec3.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec3.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec4.per_vertex.vertex_shader +dEQP-VK.fragment_shading_barycentric.data.provoking_first.static.triangle_strip.no_clip.array2.uvec4.per_vertex_flat.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_list.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_list.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_strip.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_strip.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_fan.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.0.triangle_fan.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_list.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_list.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_strip.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_strip.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_fan.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.85.triangle_fan.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_list.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_list.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_strip.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_strip.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_fan.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_static.95.triangle_fan.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_list.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_list.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_strip.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_strip.noperspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_fan.perspective.vertex_shader +dEQP-VK.fragment_shading_barycentric.weights.provoking_first.pipeline_topology_dynamic.single_sample.triangle_fan.noperspective.vertex_shader diff --git a/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology-expected.csv b/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology-expected.csv new file mode 100644 index 000000000..4daffdf13 --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology-expected.csv @@ -0,0 +1,562 @@ +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_bind_unused_ms,Fail +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_bind_unused_ms,Fail +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_bind_unused_ms,Fail +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_geom_bind_unused_ms,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_geom_bind_unused_ms,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_geom_bind_unused_ms,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_patch_geom,NotSupported +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.compute.primitive_topology.before,Pass +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.compute.primitive_topology.after,Pass +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.transfer.primitive_topology.before,Pass +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.transfer.primitive_topology.after,Pass +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.compute.primitive_topology.before,NotSupported +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.compute.primitive_topology.after,NotSupported +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.transfer.primitive_topology.before,NotSupported +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.transfer.primitive_topology.after,NotSupported +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.1,Pass +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.3,Pass +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.17,Pass +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.45,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.1,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.3,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.17,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.45,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_single_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command,Pass +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command_multi_draw,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw.triangle_fan.1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw.triangle_fan.45,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed.triangle_fan.1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed.triangle_fan.45,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_single_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_multi_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_multi_command_multi_draw,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_single_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_multi_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_multi_command_multi_draw,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_single_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command_multi_draw,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_single_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command_multi_draw,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20,NotSupported +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,Pass +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1,NotSupported +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5,NotSupported +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint16.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint32.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint8.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.no_restart_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.restart_all_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.restart_disabled_triangle_fan,Fail +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.no_restart_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.restart_all_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.no_restart_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.restart_all_triangle_fan,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.restart_disabled_triangle_fan,Fail +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_dynamic_topo,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_triangle_list_dynamic_topo,NotSupported +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_triangle_list_dynamic_topo_large_non_indexed_draw,NotSupported +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_dynamic_topo,Pass +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_triangle_list_dynamic_topo,NotSupported +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_triangle_list_dynamic_topo_large_non_indexed_draw,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_patch_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_triangle,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_line,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_patch,Pass +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_triangle_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_line_geom,NotSupported +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_patch_geom,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_with_adjacency,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_with_adjacency_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_wide,NotSupported +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_with_adjacency_wide,NotSupported diff --git a/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology.txt b/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology.txt new file mode 100644 index 000000000..6a6eab605 --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/ordinary-fan-topology.txt @@ -0,0 +1,562 @@ +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.partial_secondary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw.triangle_fan.1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw.triangle_fan.45 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed.triangle_fan.1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed.triangle_fan.45 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command_multi_draw +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.1_single_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command_multi_draw +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indexed_indirect.triangle_fan.45_single_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_multi_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_multi_command_multi_draw +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.1_single_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_multi_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_multi_command_multi_draw +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.basic_draw.draw_indirect.triangle_fan.45_single_command +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.dynamic_rendering.primary_cmd_buff.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.1 +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.17 +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.3 +dEQP-VK.draw.renderpass.basic_draw.draw.triangle_fan.45 +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.1 +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.17 +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.3 +dEQP-VK.draw.renderpass.basic_draw.draw_indexed.triangle_fan.45 +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.17_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.1_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.3_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indexed_indirect.triangle_fan.45_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.17_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.1_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.3_single_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_multi_command +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_multi_command_multi_draw +dEQP-VK.draw.renderpass.basic_draw.draw_indirect.triangle_fan.45_single_command +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.renderpass.instanced.draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.draw_indirect_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_device_address +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.draw_vk_primitive_topology_triangle_fan_multiview_device_address +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_indirect_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indexed_vk_primitive_topology_triangle_fan_multiview_maintenance_5 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_indirect_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_0 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_1 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_2 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_20 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_khr_attrib_divisor_4 +dEQP-VK.draw.renderpass.instanced.dynamic_state_draw_vk_primitive_topology_triangle_fan_multiview +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.compute.primitive_topology.after +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.compute.primitive_topology.before +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.transfer.primitive_topology.after +dEQP-VK.dynamic_state.monolithic.compute_transfer.single.transfer.primitive_topology.before +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.compute.primitive_topology.after +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.compute.primitive_topology.before +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.transfer.primitive_topology.after +dEQP-VK.dynamic_state.shader_object_unlinked_spirv.compute_transfer.single.transfer.primitive_topology.before +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.after_pipelines.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_draw.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.before_good_static.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.between_pipelines.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_line_geom_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_patch_geom_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.cmd_buffer_start.topology_triangle_geom_bind_unused_ms +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.three_draws_dynamic.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_dynamic.topology_triangle_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_line +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_line_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_patch +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_patch_geom +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_triangle +dEQP-VK.pipeline.monolithic.extended_dynamic_state.two_draws_static.topology_triangle_geom +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.no_restart_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.restart_all_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.restart_disabled_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint16.triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.no_restart_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.restart_all_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint32.triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.no_restart_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.restart_all_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.restart_disabled_triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.index_type_uint8.triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_dynamic_topo +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_dynamic_topo +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_triangle_list_dynamic_topo +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_extra_draw_triangle_list_dynamic_topo_large_non_indexed_draw +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_triangle_list_dynamic_topo +dEQP-VK.pipeline.monolithic.input_assembly.primitive_restart.restart_mix.restart_mix_triangle_list_dynamic_topo_large_non_indexed_draw +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint16.triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint32.triangle_fan +dEQP-VK.pipeline.monolithic.input_assembly.primitive_topology.index_type_uint8.triangle_fan +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_line_strip_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.bresenham_lines_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.line_strip_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.lines_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_line_strip_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.rectangular_lines_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_line_strip_with_adjacency_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_wide +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_with_adjacency +dEQP-VK.rasterization.primitives.dynamic_stipple_and_topology.smooth_lines_with_adjacency_wide diff --git a/MoltenVK/Tests/PerVertexCTS/parse-qpa.py b/MoltenVK/Tests/PerVertexCTS/parse-qpa.py new file mode 100755 index 000000000..217c9ef26 --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/parse-qpa.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +"""Reads a deqp-vk TestResults.qpa log and prints one "case,status,message" line per case that has a result. + +The message is the text of the result, with commas and line breaks replaced. A case the process terminated (crash, +timeout) is reported with the reason CTS logged, prefixed "Terminated:". +Usage: parse-qpa.py +""" +import re +import sys + +begin = re.compile(r"^#beginTestCaseResult (\S+)") +status = re.compile(r'([^<]*)') +terminate = re.compile(r"^#terminateTestCaseResult (.*)") + +case = None +with open(sys.argv[1], errors="replace") as log: + for line in log: + match = begin.match(line) + if match: + case = match.group(1) + continue + if case is None: + continue + match = status.search(line) + if match: + message = re.sub(r"[,\s]+", " ", match.group(2)).strip() + print(f"{case},{match.group(1)},{message}") + case = None + continue + match = terminate.match(line) + if match: + print(f"{case},Terminated:{match.group(1).strip()}") + case = None diff --git a/MoltenVK/Tests/PerVertexCTS/run-pervertex-cts.sh b/MoltenVK/Tests/PerVertexCTS/run-pervertex-cts.sh new file mode 100755 index 000000000..287f782d7 --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/run-pervertex-cts.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# Runs a CTS case list against a MoltenVK test build whose test device adapters AdapterShim.c opens, in chunks run one +# after the other, each under gtimeout. When deqp-vk dies (crash, Metal assertion, timeout), the case it died on is +# recorded and the list resumes after it, so one crash never hides the remaining cases. The run stops if free disk +# space falls under 8 GiB, or if deqp-vk twice in a row dies before producing any result. +# The MoltenVK settings are those of Scripts/runcts, without its Metal API validation: "validated" adds +# MTL_DEBUG_LAYER=1 and MTL_SHADER_VALIDATION=1. +# Output: results.csv (case,status,message), summary.txt, per-status case lists, crashes.txt, runs/ (case list, qpa and log of +# each deqp-vk process), INPUTS.sha256 and ENVIRONMENT.txt. +# PERVERTEX_CTS_DIRECT=1 gives deqp-vk the library itself, without the shim: for a public build, whose extension gates +# stay closed, such as the synchronization cases that do not depend on the test adapters. +# Usage: run-pervertex-cts.sh [chunk size] +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../../.." && pwd) +cts=$(cd "${1:?deqp-vk directory}" && pwd) +library=$(cd "$(dirname "${2:?libMoltenVK}")" && pwd)/$(basename "$2") +caselist=$(cd "$(dirname "${3:?case list}")" && pwd)/$(basename "$3") +output=$(mkdir -p "${4:?output}" && cd "$4" && pwd) +mode=${5:?validated or plain} +chunk=${6:-500} +[[ $mode == validated || $mode == plain ]] || { echo 'mode must be validated or plain'; exit 64; } +mkdir -p "$output/runs" +shim=$output/libpervertex-cts-shim.dylib +xcrun clang -dynamiclib -O2 -Wall -Wextra -Werror -I"$root/External/Vulkan-Headers/include" "$test_dir/AdapterShim.c" -o "$shim" +vulkan_library=$shim +[[ -z ${PERVERTEX_CTS_DIRECT:-} ]] || vulkan_library=$library +{ sw_vers; uname -m; date -u; echo "mode=$mode chunk=$chunk vulkan_library=$([[ -z ${PERVERTEX_CTS_DIRECT:-} ]] && echo shim || echo direct)"; echo "moltenvk_checkout=$(git -C "$root" rev-parse HEAD)"; cat "$(dirname "$library")/../../../PROVENANCE.txt" 2>/dev/null || true; } > "$output/ENVIRONMENT.txt" +shasum -a 256 "$cts/deqp-vk" "$library" "$caselist" "$shim" > "$output/INPUTS.sha256" +validation=() +[[ $mode == plain ]] || validation=(MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1) +: > "$output/results.csv" +: > "$output/crashes.txt" +pending=$output/pending.txt +cp "$caselist" "$pending" +run=0 empty=0 +while [[ -s $pending ]]; do + free=$(/bin/df -g "$output" | awk 'NR==2{print $4}') + (( free >= 8 )) || { echo "STOP: $free GiB free, under the 8 GiB floor"; exit 3; } + run=$((run + 1)) + name=$output/runs/$(printf 'run-%05d' "$run") + head -n "$chunk" "$pending" > "$name.txt" + rc=0 + (cd "$cts" && env -u DYLD_LIBRARY_PATH -u DYLD_INSERT_LIBRARIES PERVERTEX_CTS_MOLTENVK="$library" \ + MVK_CONFIG_LOG_LEVEL=1 MVK_CONFIG_RESUME_LOST_DEVICE=1 MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=1 MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE=2 \ + ${validation[@]+"${validation[@]}"} gtimeout -k 10 1800 ./deqp-vk --deqp-vk-library-path="$vulkan_library" --deqp-archive-dir="$cts" \ + --deqp-log-filename="$name.qpa" --deqp-log-images=disable --deqp-log-shader-sources=disable --deqp-shadercache=disable \ + --deqp-log-decompiled-spirv=disable --deqp-caselist-file="$name.txt") > "$name.log" 2>&1 || rc=$? + python3 "$test_dir/parse-qpa.py" "$name.qpa" > "$name.csv" 2>/dev/null || true + # The first case of this run without a result is the one deqp-vk died on; the others go back to the list. + result=$(python3 - "$name" "$pending" "$chunk" "$rc" <<'PY' +import sys +name, pending, chunk, rc = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) +cases = open(name + ".txt").read().split() +done = {line.split(",")[0] for line in open(name + ".csv") if line.strip()} +missing = [case for case in cases if case not in done] +rest = open(pending).read().split()[chunk:] +extra = [] +if missing: + reason = "Timeout" if rc in (124, 137) else f"Crash:exit={rc}" if rc else "Missing" + extra.append(f"{missing[0]},{reason}") + if rc == 0: + extra += [f"{case},Missing" for case in missing[1:]] + missing = [] + else: + missing = missing[1:] +open(pending, "w").write("".join(case + "\n" for case in missing + rest)) +with open(name + ".csv", "a") as csv: + csv.write("".join(line + "\n" for line in extra)) +print(f"{len(done)} {extra[0] if extra else ''}") +PY +) + cat "$name.csv" >> "$output/results.csv" + crash=${result#* } + if [[ -n $crash ]]; then + echo "$crash $(basename "$name") $(grep -m1 -E 'failed assertion|ERROR|Assertion' "$name.log" | cut -c1-300)" >> "$output/crashes.txt" + fi + if [[ ${result%% *} == 0 && $rc != 0 ]]; then empty=$((empty + 1)); else empty=0; fi + (( empty < 2 )) || { echo "STOP: deqp-vk died twice without a result; see $name.log"; exit 4; } + echo "$(date -u +%H:%M:%S) $(basename "$name") results=${result%% *} exit=$rc ${crash:+died on $crash} pending=$(wc -l < "$pending" | tr -d ' ')" +done +python3 - "$output" <<'PY' +import collections, sys +output = sys.argv[1] +statuses = collections.OrderedDict() +for line in open(f"{output}/results.csv"): + case, status, *message = line.rstrip("\n").split(",", 2) + statuses.setdefault(status.split(":")[0], []).append(case + ("\t" + message[0] if message else "")) +with open(f"{output}/summary.txt", "w") as summary: + for status, cases in sorted(statuses.items(), key=lambda item: -len(item[1])): + summary.write(f"{status} {len(cases)}\n") + open(f"{output}/{status}.txt", "w").write("".join(case + "\n" for case in cases)) +print(open(f"{output}/summary.txt").read(), end="") +PY +(cd "$output" && shasum -a 256 results.csv summary.txt crashes.txt > SHA256SUMS) diff --git a/MoltenVK/Tests/PerVertexCTS/select-experimental-scope.py b/MoltenVK/Tests/PerVertexCTS/select-experimental-scope.py new file mode 100644 index 000000000..5927f2a40 --- /dev/null +++ b/MoltenVK/Tests/PerVertexCTS/select-experimental-scope.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# Selects, from a fragment_shading_barycentric case list, the cases of the minimal experimental scope: vertex and +# fragment stages, provoking vertex first, no adjacency, no tessellation, no mesh, single sample. Barycentric weights +# are kept for triangles only: points and lines are not promised defined weights. Per-vertex data cases keep every +# simple topology, static and dynamic. +# Usage: select-experimental-scope.py +import sys + + +def in_scope(case): + if case.endswith('.data.misc.pervertex_correctness'): + return True + if '.shader_combos.' in case or '.provoking_last.' in case or 'adjacency' in case: + return False + if not case.endswith('.vertex_shader'): + return False + if '.weights.' in case: + topology = case.split('.')[6] + return '.msaa_' not in case and topology.startswith('triangle') + return '.data.' in case + + +cases = [line.strip() for line in open(sys.argv[1]) if line.strip()] +selected = [case for case in cases if in_scope(case)] +with open(sys.argv[2], 'w') as output: + output.write(''.join(case + '\n' for case in selected)) +print(f'{len(selected)} of {len(cases)} cases selected') diff --git a/MoltenVK/Tests/PerVertexCaptureVariantTests.mm b/MoltenVK/Tests/PerVertexCaptureVariantTests.mm new file mode 100644 index 000000000..f67862c2c --- /dev/null +++ b/MoltenVK/Tests/PerVertexCaptureVariantTests.mm @@ -0,0 +1,88 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#import +#import +#include +#include +#include +#include +#include +struct MVKGraphicsPipeline { + struct States { id direct = nil; id index16 = nil, index32 = nil; }; + std::unordered_map _perVertexCapturePipelineStates; + id _mtlPerVertexCapturePipelineState = nil; + struct Translation { uint32_t binding, translationBinding; }; + std::vector _translatedVertexBindings{{0, 5}}; + struct Device { + std::vector groups{2, 1, 2, 3}; + uint32_t getMultiviewMetalPassCount(uint32_t) { return uint32_t(groups.size()); } + uint32_t getViewCountInMetalPass(uint32_t, uint32_t pass) { return groups[pass]; } + } device; + VkPipelineRenderingCreateInfo rendering{}; + VkResult result = VK_SUCCESS; + const auto* getRenderingCreateInfo(const VkGraphicsPipelineCreateInfo*) { return &rendering; } + auto* getDevice() { return &device; } + uint32_t getMetalBufferIndexForVertexAttributeBinding(uint32_t binding) { return binding; } + VkResult reportError(VkResult result, const char*) { return result; } + void setConfigurationResult(VkResult error) { result = error; } + void adjustVertexInputForMultiview(MTLVertexDescriptor*, const VkPipelineVertexInputStateCreateInfo*, uint32_t, uint32_t = 1); + bool addPerVertexIndexedCapturePipelines(MTLVertexDescriptor*, int, const void*, uint32_t); + bool build(MTLRenderPipelineDescriptor*, const VkGraphicsPipelineCreateInfo*); + bool getOrCompilePipeline(MTLRenderPipelineDescriptor* desc, id& state) { state = (id)[desc copy]; return true; } + id getPerVertexCapturePipelineState(uint32_t) const; + id getPerVertexIndexedCapturePipelineState(bool, uint32_t) const; + ~MVKGraphicsPipeline() { + for (auto& entry : _perVertexCapturePipelineStates) { [entry.second.direct release]; [entry.second.index16 release]; [entry.second.index32 release]; } + [_mtlPerVertexCapturePipelineState release]; + } +}; +#include "CaptureVariantMethods.inc" +int main() { + @autoreleasepool { + VkVertexInputBindingDescription bindings[] = {{0, 16, VK_VERTEX_INPUT_RATE_INSTANCE}, {1, 16, VK_VERTEX_INPUT_RATE_INSTANCE}, {2, 16, VK_VERTEX_INPUT_RATE_VERTEX}}; + VkPipelineVertexInputStateCreateInfo input{}; + input.vertexBindingDescriptionCount = 3; input.pVertexBindingDescriptions = bindings; + VkGraphicsPipelineCreateInfo info{}; info.pVertexInputState = &input; + auto* descriptor = [MTLRenderPipelineDescriptor new]; + descriptor.vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; + for (uint32_t i : {0u, 5u}) { + descriptor.vertexDescriptor.layouts[i].stride = 16; + descriptor.vertexDescriptor.layouts[i].stepFunction = MTLVertexStepFunctionPerInstance; + descriptor.vertexDescriptor.layouts[i].stepRate = 3; + } + descriptor.vertexDescriptor.layouts[1].stepFunction = MTLVertexStepFunctionConstant; + descriptor.vertexDescriptor.layouts[1].stepRate = 0; + descriptor.vertexDescriptor.layouts[2].stepFunction = MTLVertexStepFunctionPerVertex; + descriptor.vertexDescriptor.layouts[2].stepRate = 1; + { + MVKGraphicsPipeline pipeline; + pipeline.rendering.viewMask = 0x71b; + assert(pipeline.build(descriptor, &info)); + assert(pipeline._perVertexCapturePipelineStates.size() == 3); + for (uint32_t views : {1u, 2u, 3u}) { + auto* direct = (MTLRenderPipelineDescriptor*)pipeline.getPerVertexCapturePipelineState(views); + assert(direct && direct.vertexDescriptor.layouts[0].stepRate == 3 * views); + assert(direct.vertexDescriptor.layouts[5].stepRate == 3 * views); + assert(direct.vertexDescriptor.layouts[1].stepRate == 0 && direct.vertexDescriptor.layouts[2].stepRate == 1); + for (bool index32 : {false, true}) { + auto* indexed = (MTLStageInputOutputDescriptor*)pipeline.getPerVertexIndexedCapturePipelineState(index32, views); + assert(indexed && indexed.layouts[0].stepRate == 3 * views && indexed.layouts[5].stepRate == 3 * views); + assert(indexed.layouts[0].stepFunction == MTLStepFunctionThreadPositionInGridY); + assert(indexed.layouts[2].stepFunction == MTLStepFunctionThreadPositionInGridXIndexed); + assert(indexed.layouts[1].stepRate == 0 && indexed.layouts[2].stepRate == 1); + } + } + assert(!pipeline.getPerVertexCapturePipelineState(4) && !pipeline.getPerVertexIndexedCapturePipelineState(false, 4)); + } + { + MVKGraphicsPipeline pipeline; + pipeline.rendering.viewMask = 3; + descriptor.vertexDescriptor.layouts[0].stepRate = UINT32_MAX; + descriptor.vertexDescriptor.layouts[5].stepRate = UINT32_MAX; + assert(!pipeline.build(descriptor, &info)); + assert(pipeline.result == VK_ERROR_FEATURE_NOT_PRESENT && pipeline._perVertexCapturePipelineStates.empty()); + } + [descriptor release]; + } + puts("production capture variants, translated/divisor-zero layouts, indexed step mapping, runtime getters and divisor overflow: PASS (no GPU)"); +} diff --git a/MoltenVK/Tests/PerVertexDrawSizeTests.mm b/MoltenVK/Tests/PerVertexDrawSizeTests.mm new file mode 100644 index 000000000..7cb2841c5 --- /dev/null +++ b/MoltenVK/Tests/PerVertexDrawSizeTests.mm @@ -0,0 +1,37 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "MVKCmdDraw.h" +#include +#include + +int main() { + constexpr auto list = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + constexpr auto strip = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + constexpr auto fan = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + assert(mvkCanEncodePerVertexDraw(0, UINT32_MAX, 0, list, 0)); + assert(mvkCanEncodePerVertexDraw(UINT32_MAX, 0, 0, fan, 0)); + assert(!mvkCanEncodePerVertexDraw(3, 1, 0, list, 1024)); + // Capture fits exactly; one more instance exceeds the rounded allocation ceiling. + assert(mvkCanEncodePerVertexDraw(4, 2, 16, list, 128)); + assert(!mvkCanEncodePerVertexDraw(4, 3, 16, list, 128)); + assert(!mvkCanEncodePerVertexDraw(4, 3, 16, list, 255)); + assert(mvkCanEncodePerVertexDraw(4, 3, 16, list, 256)); + // Replay pairs, rather than captured records, are the limiting allocation. + assert(mvkCanEncodePerVertexDraw(5, 1, 1, list, 64)); + assert(mvkCanEncodePerVertexDraw(4, 1, 1, strip, 64)); + assert(!mvkCanEncodePerVertexDraw(5, 1, 1, strip, 64)); + assert(mvkCanEncodePerVertexDraw(4, 1, 1, fan, 64)); + assert(!mvkCanEncodePerVertexDraw(5, 1, 1, fan, 64)); + assert(mvkCanEncodePerVertexDraw(3, 2, 1, fan, 64)); + assert(!mvkCanEncodePerVertexDraw(3, 3, 1, fan, 64)); + assert(mvkCanEncodePerVertexDraw(2, 1, 16, fan, 32)); + assert(!mvkCanEncodePerVertexDraw(2, 1, 16, fan, 31)); + // Indexing must fit uint32 even on devices with very large buffer limits. + assert(mvkCanEncodePerVertexDraw(1, UINT32_MAX, 1, list, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(2, UINT32_MAX, 1, list, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(UINT32_MAX, UINT32_MAX, UINT32_MAX, fan, UINT64_MAX)); + assert(mvkCanEncodePerVertexDraw(UINT32_MAX / 2, 1, 1, list, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(UINT32_MAX / 2 + 2, 1, 1, list, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(UINT32_MAX / 2, 1, 1, strip, UINT64_MAX)); + std::puts("PASS: 22 production PerVertex draw-size boundary checks (CPU only)"); +} diff --git a/MoltenVK/Tests/PerVertexDynamicTopologyTests.mm b/MoltenVK/Tests/PerVertexDynamicTopologyTests.mm new file mode 100644 index 000000000..7c23a52af --- /dev/null +++ b/MoltenVK/Tests/PerVertexDynamicTopologyTests.mm @@ -0,0 +1,469 @@ +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Portable PerVertexKHR with VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY. One render pass draws the same vertices with each +// topology of a class (triangle list, strip, fan; or line list, strip), changing the topology between draws. The image +// must equal the same draws through one static pipeline per topology, and differ from the dynamic pipeline kept at its +// first topology, so that each change is applied. Direct, indexed, indirect or indexed indirect draws. The adjacency +// form sets VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY, valid within the triangle class but outside capture and +// replay: recording must fail with VK_ERROR_FEATURE_NOT_PRESENT and nothing is submitted. The ordinary fragment has no +// PerVertexKHR input, so the same draws test MoltenVK's ordinary path, which converts triangle fans to triangles. +// The weights fragment (triangles only) outputs barycentric weights and index-weighted vertex values. Its static and +// dynamic images must also equal an independent reference: the draws triangulated explicitly in the order of the Vulkan +// barycentric order table and drawn without PerVertexKHR (pervertex-dynamic-reference.vert). The same reference with +// every triangle's corners turned by one must differ, so that the check sees vertex order, not only coverage. +// weights-w and weights-w-noperspective give each corner of a triangle a different w (same window position), so that +// perspective and linear interpolation differ: BaryCoordKHR against the reference weight interpolated with perspective, +// BaryCoordNoPerspKHR against the one interpolated without. The reference interpolated the other way must differ, so +// that w reaches the result. +// Usage: pervertex-dynamic-topology triangles|lines direct|indexed|indirect|indexed-indirect|adjacency +// [pervertex|ordinary|weights|weights-w|weights-w-noperspective] +#import +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static void check(VkResult result, const char* step) { + if (result != VK_SUCCESS) { std::fprintf(stderr, "%s: %d\n", step, int(result)); std::exit(1); } +} + +static std::vector readSpirv(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { std::fprintf(stderr, "Cannot open %s\n", path.c_str()); std::exit(2); } + std::vector words(size_t(file.tellg()) / 4); + file.seekg(0); + file.read(reinterpret_cast(words.data()), words.size() * 4); + return words; +} + +static bool hasExtension(const std::vector& extensions, const char* name) { + for (const auto& extension : extensions) if (!strcmp(extension.extensionName, name)) return true; + return false; +} + +constexpr uint32_t kSide = 64; + +struct Buffer { VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; uint8_t* data = nullptr; }; + +int main(int argc, char** argv) { + @autoreleasepool { + if (argc != 4 && argc != 5) { std::fprintf(stderr, "Usage: pervertex-dynamic-topology shader-dir triangles|lines direct|indexed|indirect|indexed-indirect|adjacency [pervertex|ordinary|weights|weights-w|weights-w-noperspective]\n"); return 2; } + const std::string shaders = argv[1], klass = argv[2], form = argv[3], fragmentKind = argc == 5 ? argv[4] : "pervertex"; + const bool indexed = form == "indexed" || form == "indexed-indirect", indirect = form == "indirect" || form == "indexed-indirect"; + const bool adjacency = form == "adjacency", ordinary = fragmentKind == "ordinary"; + const bool variedW = fragmentKind == "weights-w" || fragmentKind == "weights-w-noperspective", noPerspective = fragmentKind == "weights-w-noperspective"; + const bool weights = fragmentKind == "weights" || variedW; + if ((klass != "triangles" && klass != "lines") || (form != "direct" && !indexed && !indirect && !adjacency) || (adjacency && (klass != "triangles" || fragmentKind != "pervertex")) || (weights && klass != "triangles") || (!ordinary && !weights && fragmentKind != "pervertex")) { std::fprintf(stderr, "Unknown case\n"); return 2; } + const std::vector topologies = klass == "triangles" + ? std::vector{VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN} + : std::vector{VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP}; + + uint32_t count = 0; + check(vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr), "Enumerate instance extensions"); + std::vector instanceExtensions(count); + check(vkEnumerateInstanceExtensionProperties(nullptr, &count, instanceExtensions.data()), "Read instance extensions"); + const bool portability = hasExtension(instanceExtensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + const char* portabilityName = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.pApplicationName = "PerVertexKHR dynamic topology"; + app.apiVersion = VK_API_VERSION_1_3; // vkCmdSetPrimitiveTopology is core + VkInstanceCreateInfo instanceInfo{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + instanceInfo.pApplicationInfo = &app; + instanceInfo.flags = portability ? VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR : 0; + instanceInfo.enabledExtensionCount = portability ? 1 : 0; + instanceInfo.ppEnabledExtensionNames = portability ? &portabilityName : nullptr; + VkInstance instance = VK_NULL_HANDLE; + check(vkCreateInstance(&instanceInfo, nullptr, &instance), "Create instance"); + count = 1; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkResult enumerated = vkEnumeratePhysicalDevices(instance, &count, &physical); + if (enumerated != VK_SUCCESS && enumerated != VK_INCOMPLETE) check(enumerated, "Enumerate devices"); + // The combined test library exposes an adapter; shipping libraries have no such symbol. + if (auto enable = reinterpret_cast(dlsym(RTLD_DEFAULT, "mvkEnableTESFixtureDevice"))) enable(physical); + check(vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, nullptr), "Enumerate device extensions"); + std::vector extensions(count); + check(vkEnumerateDeviceExtensionProperties(physical, nullptr, &count, extensions.data()), "Read device extensions"); + std::vector deviceExtensions{VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}; + if (!hasExtension(extensions, deviceExtensions[0])) { std::fprintf(stderr, "Missing %s\n", deviceExtensions[0]); return 3; } + if (hasExtension(extensions, "VK_KHR_portability_subset")) deviceExtensions.push_back("VK_KHR_portability_subset"); + VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR barycentric{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR}; + VkPhysicalDeviceVulkan11Features features11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES}; + features11.pNext = &barycentric; + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + features.pNext = &features11; + vkGetPhysicalDeviceFeatures2(physical, &features); + if (!barycentric.fragmentShaderBarycentric || !features11.shaderDrawParameters || !features.features.vertexPipelineStoresAndAtomics || (indirect && !features.features.drawIndirectFirstInstance)) { std::fprintf(stderr, "Required features are unavailable\n"); return 3; } + VkPhysicalDeviceVulkan11Features enable11{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES}; + enable11.shaderDrawParameters = VK_TRUE; + enable11.pNext = &barycentric; + VkPhysicalDeviceFeatures2 enabled{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + enabled.pNext = &enable11; + enabled.features.vertexPipelineStoresAndAtomics = VK_TRUE; + enabled.features.drawIndirectFirstInstance = indirect; + vkGetPhysicalDeviceQueueFamilyProperties(physical, &count, nullptr); + std::vector families(count); + vkGetPhysicalDeviceQueueFamilyProperties(physical, &count, families.data()); + uint32_t family = 0; + while (family < count && !(families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT)) ++family; + float priority = 1; + VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queueInfo.queueFamilyIndex = family; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &priority; + VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + deviceInfo.pNext = &enabled; + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + deviceInfo.enabledExtensionCount = uint32_t(deviceExtensions.size()); + deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); + VkDevice device = VK_NULL_HANDLE; + check(vkCreateDevice(physical, &deviceInfo, nullptr, &device), "Create device"); + VkQueue queue = VK_NULL_HANDLE; + vkGetDeviceQueue(device, family, 0, &queue); + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physical, &properties); + std::printf("DEVICE=%s class=%s form=%s fragment=%s\n", properties.deviceName, klass.c_str(), form.c_str(), fragmentKind.c_str()); + + auto memoryType = [&](uint32_t bits, VkMemoryPropertyFlags flags) { + VkPhysicalDeviceMemoryProperties memory{}; + vkGetPhysicalDeviceMemoryProperties(physical, &memory); + for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) + if ((bits & (1u << i)) && (memory.memoryTypes[i].propertyFlags & flags) == flags) return i; + std::fprintf(stderr, "No suitable memory type\n"); + std::exit(3); + }; + auto makeBuffer = [&](VkDeviceSize size, VkBufferUsageFlags usage) { + Buffer b; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = size; + info.usage = usage; + check(vkCreateBuffer(device, &info, nullptr, &b.buffer), "Create buffer"); + VkMemoryRequirements requirements{}; + vkGetBufferMemoryRequirements(device, b.buffer, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = memoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + check(vkAllocateMemory(device, &allocation, nullptr, &b.memory), "Allocate buffer"); + check(vkBindBufferMemory(device, b.buffer, b.memory, 0), "Bind buffer"); + void* mapped = nullptr; + check(vkMapMemory(device, b.memory, 0, size, 0, &mapped), "Map buffer"); + b.data = static_cast(mapped); + memset(b.data, 0, size); + return b; + }; + // The VS records its invocations here; identity indices address the corners it reads by gl_VertexIndex. + Buffer effects = makeBuffer(16 + (1u << 18) * 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer indices = makeBuffer(14 * sizeof(uint32_t), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + for (uint32_t i = 0; i < 14; ++i) reinterpret_cast(indices.data)[i] = i; + // One command per draw, with the arguments of the direct and indexed forms. + Buffer commands = makeBuffer(3 * sizeof(VkDrawIndexedIndirectCommand), VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT); + for (uint32_t i = 0; i < 3; ++i) { + if (indexed) reinterpret_cast(commands.data)[i] = {6, 1, 4, 0, 2 + i % 2}; + else reinterpret_cast(commands.data)[i] = {6, 1, 4, 2 + i % 2}; + } + Buffer readback = makeBuffer(kSide * kSide * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent = {kSide, kSide, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + VkImage image = VK_NULL_HANDLE; + check(vkCreateImage(device, &imageInfo, nullptr, &image), "Create image"); + VkMemoryRequirements imageRequirements{}; + vkGetImageMemoryRequirements(device, image, &imageRequirements); + VkMemoryAllocateInfo imageAllocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + imageAllocation.allocationSize = imageRequirements.size; + imageAllocation.memoryTypeIndex = memoryType(imageRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VkDeviceMemory imageMemory = VK_NULL_HANDLE; + check(vkAllocateMemory(device, &imageAllocation, nullptr, &imageMemory), "Allocate image"); + check(vkBindImageMemory(device, image, imageMemory, 0), "Bind image"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = imageInfo.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + check(vkCreateImageView(device, &viewInfo, nullptr, &view), "Create image view"); + VkAttachmentDescription attachment{}; + attachment.format = imageInfo.format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + VkSubpassDependency toTransfer{0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0}; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + passInfo.dependencyCount = 1; + passInfo.pDependencies = &toTransfer; + VkRenderPass renderPass = VK_NULL_HANDLE; + check(vkCreateRenderPass(device, &passInfo, nullptr, &renderPass), "Create render pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = kSide; + framebufferInfo.height = kSide; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + check(vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer), "Create framebuffer"); + + VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = 1; + setLayoutInfo.pBindings = &binding; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + check(vkCreateDescriptorSetLayout(device, &setLayoutInfo, nullptr, &setLayout), "Create set layout"); + VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + VkDescriptorPoolCreateInfo descriptorPoolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + descriptorPoolInfo.maxSets = 1; + descriptorPoolInfo.poolSizeCount = 1; + descriptorPoolInfo.pPoolSizes = &poolSize; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + check(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool), "Create descriptor pool"); + VkDescriptorSetAllocateInfo setInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + setInfo.descriptorPool = descriptorPool; + setInfo.descriptorSetCount = 1; + setInfo.pSetLayouts = &setLayout; + VkDescriptorSet set = VK_NULL_HANDLE; + check(vkAllocateDescriptorSets(device, &setInfo, &set), "Allocate descriptor set"); + VkDescriptorBufferInfo effectsInfo{effects.buffer, 0, VK_WHOLE_SIZE}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &effectsInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + VkPipelineLayout layout = VK_NULL_HANDLE; + check(vkCreatePipelineLayout(device, &layoutInfo, nullptr, &layout), "Create pipeline layout"); + struct ReferenceDraw { uint32_t topology, rotate; }; + VkPushConstantRange referenceRange{VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ReferenceDraw)}; + VkPipelineLayoutCreateInfo referenceLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + referenceLayoutInfo.pushConstantRangeCount = 1; + referenceLayoutInfo.pPushConstantRanges = &referenceRange; + VkPipelineLayout referenceLayout = VK_NULL_HANDLE; + check(vkCreatePipelineLayout(device, &referenceLayoutInfo, nullptr, &referenceLayout), "Create reference pipeline layout"); + + auto module = [&](const std::string& name) { + auto code = readSpirv(shaders + "/" + name); + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = code.size() * 4; + info.pCode = code.data(); + VkShaderModule result = VK_NULL_HANDLE; + check(vkCreateShaderModule(device, &info, nullptr, &result), "Create shader module"); + return result; + }; + // The PORTABLE fragment also reads BaryCoordKHR, so every pipeline takes the portable capture and replay. + VkShaderModule vertex = module(variedW ? "indirect-w.vert.spv" : "indirect.vert.spv"); + VkShaderModule fragment = module(ordinary ? "ordinary.frag.spv" : noPerspective ? "weights-noperspective.frag.spv" : weights ? "weights.frag.spv" : "pervertex-portable.frag.spv"); + auto graphicsPipeline = [&](VkPrimitiveTopology topology, bool dynamicTopology, VkPipeline& result, VkShaderModule vertexModule, VkShaderModule fragmentModule, VkPipelineLayout pipelineLayout) { + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertexModule; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragmentModule; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = topology; + VkViewport viewport{0, 0, float(kSide), float(kSide), 0, 1}; + VkRect2D scissor{{0, 0}, {kSide, kSide}}; + VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportState.viewportCount = 1; + viewportState.pViewports = &viewport; + viewportState.scissorCount = 1; + viewportState.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.lineWidth = 1; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + // Blending makes the order of the overlapping draws part of the result. + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blendAttachment.dstColorBlendFactor = blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + blendAttachment.colorWriteMask = 0xF; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicState = VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 1; + dynamic.pDynamicStates = &dynamicState; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewportState; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pColorBlendState = &blend; + info.pDynamicState = dynamicTopology ? &dynamic : nullptr; + info.layout = pipelineLayout; + info.renderPass = renderPass; + return vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &result); + }; + std::vector statics(topologies.size()); + for (size_t i = 0; i < topologies.size(); ++i) check(graphicsPipeline(topologies[i], false, statics[i], vertex, fragment, layout), "Create static pipeline"); + VkPipeline referencePipeline = VK_NULL_HANDLE; + if (weights) check(graphicsPipeline(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false, referencePipeline, module(variedW ? "reference-w.vert.spv" : "reference.vert.spv"), module(noPerspective ? "reference-noperspective.frag.spv" : "reference.frag.spv"), referenceLayout), "Create reference pipeline"); + VkPipeline otherReferencePipeline = VK_NULL_HANDLE; + if (variedW) check(graphicsPipeline(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false, otherReferencePipeline, module("reference-w.vert.spv"), module(noPerspective ? "reference.frag.spv" : "reference-noperspective.frag.spv"), referenceLayout), "Create reference pipeline interpolated the other way"); + VkPipeline dynamicPipeline = VK_NULL_HANDLE; + VkResult dynamicCreated = graphicsPipeline(topologies[0], true, dynamicPipeline, vertex, fragment, layout); + std::printf("dynamic topology pipeline: vkCreateGraphicsPipelines=%d\n", int(dynamicCreated)); + if (dynamicCreated != VK_SUCCESS) { std::printf("PERVERTEX_DYNAMIC_TOPOLOGY FAIL %s %s %s: the dynamic topology pipeline is refused\n", klass.c_str(), form.c_str(), fragmentKind.c_str()); return 1; } + + VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + poolInfo.queueFamilyIndex = family; + VkCommandPool pool = VK_NULL_HANDLE; + check(vkCreateCommandPool(device, &poolInfo, nullptr, &pool), "Create command pool"); + VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + commandInfo.commandPool = pool; + commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandInfo.commandBufferCount = 1; + VkCommandBuffer cb = VK_NULL_HANDLE; + check(vkAllocateCommandBuffers(device, &commandInfo, &cb), "Allocate command buffer"); + VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + check(vkCreateFence(device, &fenceInfo, nullptr, &fence), "Create fence"); + + // Draw i: corners 4..9 of the VS, one instance at firstInstance 2 or 3 (left or right), so that the draws overlap + // in pairs. mode 0: one static pipeline per topology; 1: the dynamic pipeline, changing topology between draws; + // 2: the dynamic pipeline kept at the first topology; 3: the triangulated reference; 4: the reference turned by one; + // 5: the reference interpolated the other way (weights-w kinds). + struct Result { std::vector rgba; uint32_t invocations = 0; }; + auto render = [&](int mode) { + check(vkResetCommandBuffer(cb, 0), "Reset command buffer"); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + check(vkBeginCommandBuffer(cb, &begin), "Begin command buffer"); + VkClearValue clear{}; + VkRenderPassBeginInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + pass.renderPass = renderPass; + pass.framebuffer = framebuffer; + pass.renderArea = {{0, 0}, {kSide, kSide}}; + pass.clearValueCount = 1; + pass.pClearValues = &clear; + vkCmdBeginRenderPass(cb, &pass, VK_SUBPASS_CONTENTS_INLINE); + if (mode >= 3) { + vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, mode == 5 ? otherReferencePipeline : referencePipeline); + for (uint32_t i = 0; i < uint32_t(topologies.size()); ++i) { + const ReferenceDraw draw{uint32_t(topologies[i]), uint32_t(mode == 4)}; + vkCmdPushConstants(cb, referenceLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(draw), &draw); + // Six vertices: a list has two triangles, a strip or a fan four. + vkCmdDraw(cb, topologies[i] == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST ? 6 : 12, 1, 0, 2 + i % 2); + } + } else if (mode) vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, dynamicPipeline); + for (size_t i = 0; mode < 3 && i < topologies.size(); ++i) { + if (mode == 0) vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, statics[i]); + else vkCmdSetPrimitiveTopology(cb, adjacency && i == 1 ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY : mode == 1 ? topologies[i] : topologies[0]); + vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &set, 0, nullptr); + const uint32_t firstInstance = 2 + uint32_t(i % 2); + if (indexed) vkCmdBindIndexBuffer(cb, indices.buffer, 0, VK_INDEX_TYPE_UINT32); + if (indirect && indexed) vkCmdDrawIndexedIndirect(cb, commands.buffer, i * sizeof(VkDrawIndexedIndirectCommand), 1, sizeof(VkDrawIndexedIndirectCommand)); + else if (indirect) vkCmdDrawIndirect(cb, commands.buffer, i * sizeof(VkDrawIndirectCommand), 1, sizeof(VkDrawIndirectCommand)); + else if (indexed) vkCmdDrawIndexed(cb, 6, 1, 4, 0, firstInstance); + else vkCmdDraw(cb, 6, 1, 4, firstInstance); + } + vkCmdEndRenderPass(cb); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {kSide, kSide, 1}; + vkCmdCopyImageToBuffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.buffer, 1, ©); + VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT; + toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost, 0, nullptr, 0, nullptr); + VkResult ended = vkEndCommandBuffer(cb); + if (adjacency && mode == 1) { + const bool refused = ended == VK_ERROR_FEATURE_NOT_PRESENT; + std::printf("adjacency through the dynamic topology: vkEndCommandBuffer=%d expected=%d, nothing submitted\n", int(ended), int(VK_ERROR_FEATURE_NOT_PRESENT)); + std::printf("PERVERTEX_DYNAMIC_TOPOLOGY %s triangles adjacency\n", refused ? "PASS" : "FAIL"); + std::exit(refused ? 0 : 2); + } + if (ended != VK_SUCCESS) { std::printf("mode %d: vkEndCommandBuffer=%d\n", mode, int(ended)); std::exit(1); } + memset(effects.data, 0, 16); + memset(readback.data, 0, kSide * kSide * 4); + check(vkResetFences(device, 1, &fence), "Reset fence"); + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cb; + check(vkQueueSubmit(queue, 1, &submit, fence), "Submit"); + check(vkWaitForFences(device, 1, &fence, VK_TRUE, 20ull * 1000 * 1000 * 1000), "Wait"); + Result result; + result.rgba.assign(readback.data, readback.data + kSide * kSide * 4); + memcpy(&result.invocations, effects.data, 4); + return result; + }; + Result staticResult = render(0), dynamicResult = render(1), unchanged = render(2); + // Pixels that differ, and the largest channel difference with its position. + auto compare = [&](const Result& a, const Result& b, const char* what) { + uint32_t differing = 0, worst = 0, at = 0; + for (size_t i = 0; i < a.rgba.size(); i += 4) { + differing += memcmp(&a.rgba[i], &b.rgba[i], 4) != 0; + for (size_t c = 0; c < 4; ++c) { + uint32_t d = uint32_t(std::abs(int(a.rgba[i + c]) - int(b.rgba[i + c]))); + if (d > worst) { worst = d; at = uint32_t(i / 4); } + } + } + if (what) std::printf("%s: pixel_mismatches=%u max_channel_difference=%u at (%u, %u)\n", what, differing, worst, at % kSide, at / kSide); + return differing; + }; + uint32_t mismatches = compare(staticResult, dynamicResult, nullptr), covered = 0; + for (size_t i = 0; i < staticResult.rgba.size(); i += 4) covered += staticResult.rgba[i + 3] != 0; + bool referenceMatches = true; + if (weights) { + Result reference = render(3), turned = render(4); + uint32_t referenceCovered = 0; + for (size_t i = 0; i < reference.rgba.size(); i += 4) referenceCovered += reference.rgba[i + 3] != 0; + std::printf("reference: covered=%u\n", referenceCovered); + const uint32_t staticDiffers = compare(staticResult, reference, "static vs reference"); + const uint32_t dynamicDiffers = compare(dynamicResult, reference, "dynamic vs reference"); + const uint32_t turnedDiffers = compare(dynamicResult, turned, "dynamic vs reference turned by one (must differ)"); + uint32_t otherDiffers = 1; + if (variedW) otherDiffers = compare(dynamicResult, render(5), "dynamic vs reference interpolated the other way (must differ)"); + referenceMatches = referenceCovered == covered && staticDiffers == 0 && dynamicDiffers == 0 && turnedDiffers > 0 && otherDiffers > 0; + } + const bool changed = dynamicResult.rgba != unchanged.rgba; + const uint32_t expected = uint32_t(topologies.size()) * 6; + std::printf("static vs dynamic: covered=%u pixel_mismatches=%u; dynamic differs from its first topology kept=%d; VS invocations static=%u dynamic=%u expected=%u\n", + covered, mismatches, int(changed), staticResult.invocations, dynamicResult.invocations, expected); + // The ordinary path may shade a vertex more than once (a converted fan is indexed): it must only match static. + const bool invocationsMatch = ordinary ? staticResult.invocations >= expected && dynamicResult.invocations == staticResult.invocations + : staticResult.invocations == expected && dynamicResult.invocations == expected; + const bool pass = covered > 0 && mismatches == 0 && changed && invocationsMatch && referenceMatches; + std::printf("PERVERTEX_DYNAMIC_TOPOLOGY %s %s %s %s\n", pass ? "PASS" : "FAIL", klass.c_str(), form.c_str(), fragmentKind.c_str()); + vkDeviceWaitIdle(device); + vkDestroyDevice(device, nullptr); + vkDestroyInstance(instance, nullptr); + return pass ? 0 : 2; + } +} diff --git a/MoltenVK/Tests/PerVertexHelperPreflightTests.mm b/MoltenVK/Tests/PerVertexHelperPreflightTests.mm new file mode 100644 index 000000000..831478841 --- /dev/null +++ b/MoltenVK/Tests/PerVertexHelperPreflightTests.mm @@ -0,0 +1,354 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "MVKPerVertexScratch.h" +#include "MVKSubmissionTransaction.h" +#include "HelperPreflightVersion.inc" +#include +#include +#include +#include +#include +#include +using namespace std; + +static atomic liveBuffers, livePipelines; +// No Metal device, queue, compiler or GPU is created. Only these selectors run. +@interface PreflightBuffer : NSObject { +@public + NSUInteger _length; + uint8_t _bytes[512]; +} +@end +@implementation PreflightBuffer +- (NSUInteger)length { return _length; } +- (void*)contents { return _bytes; } +- (void)dealloc { --liveBuffers; [super dealloc]; } +@end +@interface PreflightDevice : NSObject { +@public + unsigned calls, failAt; + std::atomic* deviceResult; +} +@end +@implementation PreflightDevice +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options { + (void)options; + if (++calls == failAt) { + if (deviceResult) { *deviceResult = VK_ERROR_DEVICE_LOST; } + return nil; + } + auto* buffer = [PreflightBuffer new]; + ++liveBuffers; + buffer->_length = length; + return (id)buffer; +} +@end +@interface PreflightPipeline : NSObject { +@public + NSUInteger width, maximum; +} +@end +@implementation PreflightPipeline +- (NSUInteger)threadExecutionWidth { return width; } +- (NSUInteger)maxTotalThreadsPerThreadgroup { return maximum; } +- (void)dealloc { --livePipelines; [super dealloc]; } +@end + +enum class Failure { None, Missing, Timeout, ZeroWidth, ZeroMaximum, DeviceLostNil, DeviceLostValid }; +struct TestCommandPool; +struct TestDevice { + atomic result{VK_SUCCESS}; + Failure failure = Failure::None; + atomic creations[5]{}; + VkResult getConfigurationResult() { return result.load(); } + TestDevice* getCommandResourceFactory() { return this; } + id create(unsigned slot); + id newPerVertexRestartMTLComputePipelineState(TestCommandPool*) { return create(2); } + id newConvertUint8IndicesMTLComputePipelineState(TestCommandPool*, bool preserveValues) { return create(preserveValues ? 1 : 0); } + id newPerVertexIndirectMTLComputePipelineState(TestCommandPool*) { return create(3); } + id newPerVertexTessTopologyMTLComputePipelineState(TestCommandPool*) { return create(4); } + // No scratch pool: the default, without MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1. + const shared_ptr& getPerVertexScratchPool() { static const shared_ptr none; return none; } +}; +struct TestCommandPool { TestDevice* device; TestDevice* getDevice() { return device; } }; +id TestDevice::create(unsigned slot) { + ++creations[slot]; + if (failure == Failure::DeviceLostNil || failure == Failure::DeviceLostValid) { result = VK_ERROR_DEVICE_LOST; } + // The real compiler returns nil for both compilation failure and timeout. + if (failure == Failure::Missing || failure == Failure::Timeout || failure == Failure::DeviceLostNil) { return nil; } + auto* state = [PreflightPipeline new]; + ++livePipelines; + state->width = failure == Failure::ZeroWidth ? 0 : 32; + state->maximum = failure == Failure::ZeroMaximum ? 0 : 256; + return (id)state; +} + +struct MVKCommandEncodingPool { + explicit MVKCommandEncodingPool(TestCommandPool* pool) : _commandPool(pool) {} + TestCommandPool* _commandPool; + mutex _lock; + id _mtlPerVertexRestartComputePipelineState = nil; + id _mtlConvertUint8IndicesComputePipelineState[2] = {}; + id _mtlPerVertexIndirectComputePipelineState = nil; + id _mtlPerVertexTessTopologyComputePipelineState = nil; + id getPerVertexRestartMTLComputePipelineState(); + id getPerVertexIndirectMTLComputePipelineState(); + id getPerVertexTessTopologyMTLComputePipelineState(); + id getConvertUint8IndicesMTLComputePipelineState(bool preserveValues = false); + void clear() { + lock_guard lock(_lock); + [_mtlPerVertexRestartComputePipelineState release]; _mtlPerVertexRestartComputePipelineState = nil; + for (auto& state : _mtlConvertUint8IndicesComputePipelineState) { [state release]; state = nil; } + [_mtlPerVertexIndirectComputePipelineState release]; _mtlPerVertexIndirectComputePipelineState = nil; + [_mtlPerVertexTessTopologyComputePipelineState release]; _mtlPerVertexTessTopologyComputePipelineState = nil; + } + ~MVKCommandEncodingPool() { clear(); } +}; +struct RecordingPool : TestCommandPool { + MVKCommandEncodingPool encoding{this}; + MVKCommandEncodingPool* getCommandEncodingPool() { return &encoding; } +}; +struct MVKCommandBuffer { + TestDevice* _device; + RecordingPool* _commandPool; + id metal; + VkResult configuration = VK_SUCCESS; + bool _prefilledMTLCmdBuffer = false; + vector _perVertexScratchRequests; + MVKPerVertexScratchReservations _prefilledPerVertexScratch; + id getMTLDevice() { return metal; } + VkResult getConfigurationResult() { return configuration; } + bool wasConfigurationSuccessful() { return configuration == VK_SUCCESS; } + VkResult reportError(VkResult result, const char*, ...) { return result; } + VkResult reservePrefilledPerVertexScratch(size_t firstRequest); + VkResult reservePerVertexScratch(MVKPerVertexScratchReservations& scratch, unordered_set& prefilledExecutions); + VkResult reservePerVertexScratch(const vector& requests, MVKPerVertexScratchReservations& scratch); +}; +#include "HelperPreflightMethods.inc" + +static MVKPerVertexScratchRequest request(bool restart) { return {48, 24, 12, 12, restart ? 12u : 6u, restart}; } +#if MVK_TEST_HAS_INDIRECT +// Indirect scratch prepares its planning helper instead of an index helper; the plan fits the fake buffer. +static MVKPerVertexScratchRequest indirectRequest() { + MVKPerVertexScratchRequest indirect = {48, 24, 12, 12, 16}; + indirect.indirectCapacity = 4; + indirect.planSize = 256; + return indirect; +} +#endif +struct Submission { MVKPerVertexScratchReservations scratch; }; + +static bool checkRejection() { + bool passed = true; + for (unsigned kind : {0u, 1u, 2u}) { + bool restart = kind == 1; +#if !MVK_TEST_HAS_INDIRECT + if (kind == 2) { continue; } +#endif + for (Failure failure : {Failure::Missing, Failure::Timeout, Failure::ZeroWidth, Failure::ZeroMaximum, Failure::DeviceLostNil, Failure::DeviceLostValid}) { + for (unsigned prefill : {0u, 1u, 2u}) { + TestDevice device; + RecordingPool pool{{&device}}; + auto* metal = [PreflightDevice new]; +#if MVK_TEST_HAS_INDIRECT + MVKCommandBuffer cb{&device, &pool, (id)metal, VK_SUCCESS, false, {kind == 2 ? indirectRequest() : request(restart)}, {}}; +#else + MVKCommandBuffer cb{&device, &pool, (id)metal, VK_SUCCESS, false, {request(restart)}, {}}; +#endif + device.failure = failure; + unsigned dispatched = 0; + unordered_set prefilled; + VkResult result; + if (prefill) { + // Immediate recording reserves the appended request; deferred end + // reserves the whole list. Neither may enter encode after failure. + result = cb.reservePrefilledPerVertexScratch(0); + if (result == VK_SUCCESS) { ++dispatched; } + } else { + // A late batch failure must prevent dispatch of earlier batches too. + result = mvkSubmitTransaction(2, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + return cb.reservePerVertexScratch(submission.scratch, prefilled); + }, [&](Submission* submission) { ++dispatched; delete submission; return VK_SUCCESS; }); + } + VkResult expected = failure == Failure::DeviceLostNil || failure == Failure::DeviceLostValid ? VK_ERROR_DEVICE_LOST : VK_ERROR_INITIALIZATION_FAILED; + if (result != expected || dispatched || liveBuffers || !cb._prefilledPerVertexScratch.empty()) { + fprintf(stderr, "FAIL: restart=%d failure=%u prefill=%u returned %d, expected %d before dispatch (dispatched=%u, liveBuffers=%u)\n", restart, unsigned(failure), prefill, result, expected, dispatched, liveBuffers.load()); + [metal release]; + passed = false; + continue; + } + assert(device.creations[kind == 2 ? 3 : restart ? 2 : 1] == 1); + [metal release]; + } + } + } + return passed; +} + +static void checkTransactions() { + TestDevice device; + RecordingPool pool{{&device}}; + auto* metal = [PreflightDevice new]; + MVKCommandBuffer cb{&device, &pool, (id)metal, VK_SUCCESS, false, {request(false), request(true)}, {}}; + unordered_set prefilled; + MVKPerVertexScratchReservations saved; + // Allocation failure never reaches helper compilation and leaves no reservation. + metal->failAt = 3; + assert(cb.reservePerVertexScratch(saved, prefilled) == VK_ERROR_OUT_OF_DEVICE_MEMORY); + assert(saved.empty() && !liveBuffers && !device.creations[1] && !device.creations[2]); + metal->failAt = metal->calls + 1; + metal->deviceResult = &device.result; + assert(cb.reservePerVertexScratch(saved, prefilled) == VK_ERROR_DEVICE_LOST); + assert(saved.empty() && !liveBuffers); + device.result = VK_SUCCESS; + metal->deviceResult = nullptr; + metal->failAt = 0; + assert(cb.reservePerVertexScratch(saved, prefilled) == VK_SUCCESS); + assert(saved.size() == 2 && liveBuffers == 13); +#if MVK_TEST_HAS_PREPARED_HELPER + assert(saved[0]->indexPipeline && saved[1]->indexPipeline); +#endif + // Failed replacement leaves existing reservations intact, including helper ownership. + auto* previous = saved[0].get(); + pool.encoding.clear(); + device.failure = Failure::Timeout; + assert(cb.reservePerVertexScratch(saved, prefilled) == VK_ERROR_INITIALIZATION_FAILED); + assert(saved[0].get() == previous && liveBuffers == 13); + device.failure = Failure::None; + // A failure in the later batch rolls back earlier scratch without dispatch/signals. + unsigned batch = 0, dispatched = 0; + assert(mvkSubmitTransaction(2, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + if (batch++) { pool.encoding.clear(); device.failure = Failure::Timeout; } + return cb.reservePerVertexScratch(submission.scratch, prefilled); + }, [&](Submission* submission) { ++dispatched; delete submission; return VK_SUCCESS; }) == VK_ERROR_INITIALIZATION_FAILED); + assert(dispatched == 0 && liveBuffers == 13); + device.failure = Failure::None; + cb._prefilledMTLCmdBuffer = true; + cb._prefilledPerVertexScratch = saved; + // Repeated prefilled command buffers consume the prefill only once; the later + // execution prepares independently. Failure leaves the prefill available. + batch = 0; + assert(mvkSubmitTransaction(2, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + if (batch++) { device.failure = Failure::Timeout; } + return cb.reservePerVertexScratch(submission.scratch, prefilled); + }, [](Submission*) { assert(false); return VK_SUCCESS; }) == VK_ERROR_INITIALIZATION_FAILED); + assert(saved[0].use_count() == 2 && liveBuffers == 13); + device.failure = Failure::None; + prefilled.clear(); + vector> executions; + assert(mvkSubmitTransaction(2, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + return cb.reservePerVertexScratch(submission.scratch, prefilled); + }, [&](Submission* submission) { executions.emplace_back(submission); return VK_SUCCESS; }) == VK_SUCCESS); + assert(executions[0]->scratch[0] == saved[0] && executions[1]->scratch[0] != saved[0]); + assert(liveBuffers == 26); + // A pre-existing device failure also rejects cached/prefilled success. + device.result = VK_ERROR_DEVICE_LOST; + prefilled.clear(); + MVKPerVertexScratchReservations rejected; + assert(cb.reservePerVertexScratch(rejected, prefilled) == VK_ERROR_DEVICE_LOST && rejected.empty()); + [metal release]; +} + +static void checkConcurrentCache() { + TestDevice device; + RecordingPool pool{{&device}}; + vector threads; + atomic ready{0}; + atomic go{false}; + id results[24][3]{}; + for (unsigned i = 0; i < 24; ++i) { + threads.emplace_back([&, i] { + @autoreleasepool { + ++ready; + while (!go) { this_thread::yield(); } + for (unsigned repeat = 0; repeat < 100; ++repeat) { + results[i][0] = pool.encoding.getConvertUint8IndicesMTLComputePipelineState(false); + results[i][1] = pool.encoding.getConvertUint8IndicesMTLComputePipelineState(true); + results[i][2] = pool.encoding.getPerVertexRestartMTLComputePipelineState(); + } + } + }); + } + while (ready != 24) { this_thread::yield(); } + go = true; + for (auto& thread : threads) { thread.join(); } + for (unsigned slot = 0; slot < 3; ++slot) { + assert(device.creations[slot] == 1); + for (unsigned i = 0; i < 24; ++i) { assert(results[i][slot] == results[0][slot]); } + } + assert(results[0][0] != results[0][1]); // Regular UINT8 keeps its sentinel semantics. +} + +#if MVK_TEST_HAS_INDIRECT +static void checkIndirect() { + TestDevice device; + RecordingPool pool{{&device}}; + auto* metal = [PreflightDevice new]; + MVKCommandBuffer cb{&device, &pool, (id)metal, VK_SUCCESS, false, {indirectRequest(), indirectRequest()}, {}}; + unordered_set prefilled; + MVKPerVertexScratchReservations saved; + assert(cb.reservePerVertexScratch(saved, prefilled) == VK_SUCCESS && saved.size() == 2); + for (auto& scratch : saved) { + // Planning helper retained, no index helper, plan buffer present and zeroed before any submission. + assert(scratch->indirectPipeline && !scratch->indexPipeline && scratch->indirectCapacity == 4); + assert(scratch->buffers[12] && scratch->buffers[12].length == 256); + auto* bytes = (const uint8_t*)scratch->buffers[12].contents; + for (unsigned i = 0; i < 256; ++i) { assert(!bytes[i]); } + } + assert(saved[0]->buffers[12] != saved[1]->buffers[12] && device.creations[3] == 1 && !device.creations[1] && !device.creations[2]); + // Restart-capable indirect scratch also retains the restart assembly helper. Its replay tables, including the + // restart state, are reserved only after the GPU plan completes, from the counts it froze. + auto restart = indirectRequest(); + restart.restart = true; + cb._perVertexScratchRequests = {restart}; + MVKPerVertexScratchReservations restarted; + assert(cb.reservePerVertexScratch(restarted, prefilled) == VK_SUCCESS && restarted.size() == 1); + assert(restarted[0]->indirectPipeline && restarted[0]->indexPipeline && device.creations[2] == 1 && !device.creations[1]); + for (auto buffer : restarted[0]->buffers) { assert(!buffer || buffer == restarted[0]->buffers[12]); } + [metal release]; +} +#endif + +#if MVK_TEST_HAS_TES_TOPOLOGY +// TES scratch prepares the GPU topology generator before submission; failures reject the whole call. +static void checkTessTopology() { + for (Failure failure : {Failure::None, Failure::Missing, Failure::ZeroWidth, Failure::DeviceLostValid}) { + TestDevice device; + RecordingPool pool{{&device}}; + auto* metal = [PreflightDevice new]; + MVKPerVertexScratchRequest tes = {48, 24, 12, 12}; + NSUInteger sizes[] = {48, 16, 16, 16, 24, 256}; + std::copy(std::begin(sizes), std::end(sizes), tes.tessSizes); + MVKCommandBuffer cb{&device, &pool, (id)metal, VK_SUCCESS, false, {tes}, {}}; + device.failure = failure; + unordered_set prefilled; + MVKPerVertexScratchReservations saved; + VkResult result = cb.reservePerVertexScratch(saved, prefilled); + if (failure == Failure::None) { + assert(result == VK_SUCCESS && saved.size() == 1 && saved[0]->tessTopologyPipeline && !saved[0]->indexPipeline); + assert(saved[0]->getTessEvalStatus() == 0 && device.creations[4] == 1 && !device.creations[2] && !device.creations[3]); + } else { + assert(result == (failure == Failure::DeviceLostValid ? VK_ERROR_DEVICE_LOST : VK_ERROR_INITIALIZATION_FAILED) && saved.empty() && !liveBuffers); + } + saved.clear(); + [metal release]; + } +} +#endif + +int main() { + @autoreleasepool { + if (!checkRejection()) { return 1; } + checkTransactions(); +#if MVK_TEST_HAS_INDIRECT + checkIndirect(); +#endif +#if MVK_TEST_HAS_TES_TOPOLOGY + checkTessTopology(); +#endif + assert(!liveBuffers && !livePipelines); + checkConcurrentCache(); + assert(!liveBuffers && !livePipelines); + } + puts("PASS: production preflight/cache methods; nil/timeout/invalid dimensions/device loss; no dispatch on failure; prefill/repeated execution/rollback/retry; helper lifetime; concurrent warm and cold caches (CPU only)"); +} diff --git a/MoltenVK/Tests/PerVertexKHR-README.md b/MoltenVK/Tests/PerVertexKHR-README.md new file mode 100644 index 000000000..04bf6cf5a --- /dev/null +++ b/MoltenVK/Tests/PerVertexKHR-README.md @@ -0,0 +1,140 @@ +# Experimental PerVertexKHR capture and replay: tests + +MoltenVK advertises `VK_KHR_fragment_shader_barycentric` where Metal provides barycentric coordinates, and implements it +with them; that native path does not support `PerVertexKHR` inputs. This experimental capture and replay is a separate +path, reachable only through a test entry point, in a library built for that purpose. It does not claim conformance of +the extension. + +## Claimed scope + +The tests below cover this scope: + +- vertex and fragment pipelines from SPIR-V, through the portable capture-then-replay path, on GPUs with MSL 2.4, + with `MVK_USE_METAL_PRIVATE_API` off; +- `PerVertexKHR` fragment inputs: scalars and vectors of `float`, `int` and `uint`, arrays and structures; +- `BaryCoordKHR` and `BaryCoordNoPerspKHR`, for triangles only. Points and lines get no defined barycentric weights; +- point lists, line lists and strips, triangle lists, strips and fans, static or dynamic + (`VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY` within the topology class of the pipeline); +- `vkCmdDraw` and `vkCmdDrawIndexed` with `uint32` indices, a nonzero `firstVertex` or `firstIndex`, and a nonzero + `firstInstance`, with one instance and a zero `vertexOffset`; +- `ClipDistance` written by the vertex shader; +- one sample, filled polygons, first provoking vertex, no multiview; +- alpha blending, which makes the draw order visible; +- refusal before submission of memoryless attachments that would have to cross the capture and replay passes. + +Accepted by the implementation but not covered by these tests: several instances, a nonzero `vertexOffset`, primitive +restart and `uint8` or `uint16` indices, secondary command buffers, Metal command buffer prefill, and a `viewMask` +with a single view. + +Outside the claimed scope, even though code for them is present: indirect and indirect count draws, tessellation, +mesh shaders, the native path for Apple10 GPUs, multisampling, multiview with more than one view, last provoking +vertex, polygon modes other than `FILL`, the private Metal API, and adjacency topologies. + +## Building the experimental library + +```sh +bash MoltenVK/Tests/PerVertexTES/build-library.sh OFF ON +``` + +- Run it from the root of the MoltenVK checkout: the library embeds the `git rev-parse --short HEAD` of the current + directory as its revision. +- `External/SPIRV-Cross` must be checked out at the revision pinned in `ExternalRevisions/SPIRV-Cross_repo_revision`, + with no local changes. +- The arguments select an arm64 Debug build, with tessellation admission off and the test entry point linked + (`MoltenVK/Tests/PerVertexTES/EnableTestDevice.mm`). +- The library is written to `/build/moltenvk/MoltenVK/libMoltenVK.1.4.3.dylib`. + +`MVK_TEST_BUILD_TYPE=Release` selects a Release build instead. The tests below were run on Debug builds only. + +## Activation + +An application looks up `mvkEnableTESFixtureDevice(VkPhysicalDevice)` with `dlsym`, and calls it before creating the +logical device. Despite its historical name, it only selects the experimental capture and replay path for +`VK_KHR_fragment_shader_barycentric`, and only where Metal provides barycentric coordinates. Without that call, the +extension keeps the native path: `BaryCoordKHR` and `BaryCoordNoPerspKHR` read Metal barycentric coordinates, and a +pipeline whose fragment shader reads `PerVertexKHR` inputs is not supported. + +## Tests and expected results + +The scripts need `python3`, `glslangValidator` and `spirv-val` on `PATH`. The CTS and dynamic topology runners +also need GNU `timeout`, installed as `gtimeout`. + +All GPU runs use Metal API and shader validation (`MTL_DEBUG_LAYER=1`, `MTL_SHADER_VALIDATION=1`), one GPU workload at +a time. A skipped or `NotSupported` case in a list expected to pass is a missing result, not a pass. + +| Ref. | Test | Command | Expected | +|---|---|---|---| +| A | Checks of production code paths. On the CPU: `prefill`, `helper-preflight`, `scratch`, `draw-size`, `topology`, `capture-variant`. On the GPU through Metal, without Vulkan: `restart`, `memoryless` | `bash MoltenVK/Tests/run-pervertex--tests.sh [output]`; for the GPU part, `bash MoltenVK/Tests/run-pervertex-restart-tests.sh gpu` and `bash MoltenVK/Tests/run-pervertex-memoryless-tests.sh --metal` | `PASS` for each | +| B | Dynamic primitive topology oracle, 14 cases: `{triangles,lines}:{direct,indexed}:{pervertex,ordinary}` and `triangles:{direct,indexed}:{weights,weights-w,weights-w-noperspective}` | `bash MoltenVK/Tests/run-pervertex-dynamic-topology-tests.sh 0 ...` | `PASS` for each | +| C | Memoryless depth and stencil attachments split across capture and replay | `bash MoltenVK/Tests/run-pervertex-split-tests.sh after memoryless-depth memoryless-stencil` | `REFUSED` (exit 4) for each | +| D | `dEQP-VK.fragment_shading_barycentric` cases of the claimed scope (`PerVertexCTS/experimental-vsfs-barycentric.txt`, 1201 cases) | `bash MoltenVK/Tests/PerVertexCTS/run-pervertex-cts.sh MoltenVK/Tests/PerVertexCTS/experimental-vsfs-barycentric.txt validated` | 1201 `Pass` | +| E | Ordinary topology and triangle fan cases (`PerVertexCTS/ordinary-fan-topology.txt`, 562 cases) | same command with that list | statuses of `PerVertexCTS/ordinary-fan-topology-expected.csv`: 258 `Pass`, 299 `NotSupported`, 5 `Fail` | +| F | List D without activation (native path) | `PERVERTEX_CTS_DIRECT=1` and the command of D | per case, the statuses of an upstream MoltenVK library on the same Mac | +| G | Identity and activation probe | see below | `fragmentShaderBarycentric` 1 before and after activation, where Metal provides barycentric coordinates | + +Notes: + +- Six A checks run on the CPU only, some of them with stand-in Metal objects. `restart` and `memoryless` create a + Metal device and submit work to the GPU only with their GPU argument, `gpu` and `--metal` respectively. Without it, + `restart` runs its CPU checks only, and `memoryless` its storage policy checks only. +- The CTS runner builds `AdapterShim.c`, which calls the test entry point before `deqp-vk` creates its devices. + `PERVERTEX_CTS_DIRECT=1` loads the library directly, so the native path is used. +- The 5 expected failures of E: + - 2 `restart_disabled_triangle_fan` cases also fail with an upstream library, because Metal cannot disable primitive + restart; + - 3 `topology_*_bind_unused_ms` cases are `NotSupported` with an upstream library, which does not support mesh + shaders. `AdapterShim.c` also calls the test entry point of the experimental mesh path (`mvkEnableMeshTestDevice`), + so these cases reach pipeline creation, and end with `VK_ERROR_FEATURE_NOT_PRESENT`. They fail because of the + test shim, not on the default path. +- F compares the native path with an upstream library, case by case. On Apple M4 Pro, Apple M2 and Apple M1, both + fail every case of list D at pipeline creation (`VK_ERROR_INITIALIZATION_FAILED`), with the same message. There, F + shows that the native path is unchanged on list D; it does not show that the native path renders barycentric inputs. +- E's `NotSupported` statuses depend on the features of the Mac. +- List D was selected from the runnable cases of `fragment_shading_barycentric` by + `PerVertexCTS/select-experimental-scope.py`. It excludes these cases: + - 744 adjacency cases; + - 690 mesh cases; + - 108 last-provoking-vertex cases; + - 50 multisampling cases; + - 36 tessellation cases; + - 22 weight cases on points and lines. + +Probe G: + +```sh +xcrun clang++ -arch arm64 -std=c++17 -DVK_ENABLE_BETA_EXTENSIONS -DPROBE_BARYCENTRIC_ADAPTER_ONLY \ + -IExternal/Vulkan-Headers/include MoltenVK/Tests/MacFamilyHarness/probe.mm \ + -fobjc-arc -framework Foundation -framework Metal -o probe +``` + +The probe prints `fragmentShaderBarycentric` before and after the call to the test entry point. It also prints the +first four bytes of `pipelineCacheUUID`, which carry the library's revision. Run it with `DYLD_PRINT_LIBRARIES=1` to +confirm which library is loaded. + +### Converter tests + +These run on the CPU, against the SPIRV-Cross sources (`External/SPIRV-Cross`) and a build of its static libraries +with the `MVK_spirv_cross` namespace: + +```sh +bash MoltenVKShaderConverter/Tests/run-per-vertex-input-tests.sh [output] +bash MoltenVKShaderConverter/Tests/run-pervertex-pipeline-interface-tests.sh [arch] +bash MoltenVKShaderConverter/Tests/run-portable-barycentric-tests.sh +bash MoltenVKShaderConverter/Tests/run-shader-interface-reflection-tests.sh +MVK_CACHE_LIBRARY= MVK_CACHE_ARCH=arm64 MVK_CACHE_CROSS_SOURCE= \ + bash MoltenVKShaderConverter/Tests/run-pipeline-cache-per-vertex-tests.sh +``` + +- They need `glslangValidator`, `spirv-as` and `spirv-val` on `PATH`. Some also compile the generated MSL with + `xcrun metal -fsyntax-only`. +- The reflection test is built with AddressSanitizer. Link it against SPIRV-Cross libraries built with + `-fsanitize=address`: otherwise AddressSanitizer reports false container overflows in the libc++ containers that + cross the boundary. +- The `build/spirv-cross` directory of the library build above holds suitable uninstrumented libraries for the other + scripts. + +## Shared replay tables + +The experimental capture/replay path reuses immutable replay tables per command encoding pool, topology and provoking mode. Single-instance draws and complete instanced lists may share a larger table prefix; primitive restart, instanced strips/fans and incomplete lists retain per-draw tables. Superseded buffers stay alive until pool destruction so previously encoded work remains valid. Allocation failure uses the per-draw path. + +Run `bash MoltenVK/Tests/run-pervertex-shared-replay-tests.sh ` for the CPU prefix and admission checks under ASan/UBSan. This does not enable the experimental path or add a configuration setting. Pipelines needing depth clip emulation remain outside the pinned replay compiler's supported scope and are refused explicitly. diff --git a/MoltenVK/Tests/PerVertexMemorylessTests.mm b/MoltenVK/Tests/PerVertexMemorylessTests.mm new file mode 100644 index 000000000..e525e319d --- /dev/null +++ b/MoltenVK/Tests/PerVertexMemorylessTests.mm @@ -0,0 +1,185 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#import +#import +#include +#include +#include +#include +#include +#include + +struct MVKImage; +struct MVKImageMemoryBinding; +struct MVKDeviceMemory { + MTLStorageMode storage = MTLStorageModeMemoryless; + id _mtlTexture = nil; + VkExternalMemoryHandleTypeFlags _externalMemoryHandleType = 0; + bool dedicated = false; + std::vector _imageMemoryBindings; + MTLStorageMode getMTLStorageMode() { return storage; } + bool isDedicatedAllocation() { return dedicated; } + id getMTLHeap() { return nil; } +}; +struct MVKImageMemoryBinding { MVKDeviceMemory* _deviceMemory = nullptr; MVKImage* _image = nullptr; id _mtlTexelBuffer = nil; }; +struct MVKImagePlane { id _mtlTexture = nil; }; +struct Extension { bool enabled = false; }; +struct Extensions { Extension vk_KHR_fragment_shader_barycentric, vk_NV_fragment_shader_barycentric; }; +template bool mvkContains(const std::vector& values, T value) { return std::find(values.begin(), values.end(), value) != values.end(); } +static bool mvkIsAnyFlagEnabled(uint32_t value, uint32_t flags) { return value & flags; } +// The test entry point sets this flag; without it, the extensions keep their native path. +struct MVKPhysicalDevice { + bool portablePerVertex = true; + bool isPortablePerVertexEnabled() const { return portablePerVertex; } +}; +struct MVKImage { + MVKPhysicalDevice physicalDevice; + MVKPhysicalDevice* getPhysicalDevice() { return &physicalDevice; } + MVKImageMemoryBinding binding; + MVKImagePlane plane; + std::vector _memoryBindings{&binding}; + std::vector _planes{&plane}; + void* _ioSurface = nullptr; + bool _isAliasable = false; + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + Extensions extensions; + MVKImage() { binding._image = this; } + VkImageUsageFlags getCombinedUsage() { return usage; } + const Extensions& getEnabledExtensions() { return extensions; } + bool getIsDepthStencil() { return usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; } + MTLStorageMode getMTLStorageMode(); +}; +#include "ImageStorageMode.inc" + +// An external texture's storageMode is inspectable even without a Metal device. +@interface StorageTexture : NSObject +@property(nonatomic) MTLStorageMode storageMode; +@end +@implementation StorageTexture +@end + +static void checkPolicy() { + MVKDeviceMemory memory; + MVKImage image; + assert(image.getMTLStorageMode() == MTLStorageModePrivate); + image.binding._deviceMemory = &memory; + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + for (bool nv : {false, true}) { + image.extensions.vk_KHR_fragment_shader_barycentric.enabled = !nv; + image.extensions.vk_NV_fragment_shader_barycentric.enabled = nv; + for (auto usage : {VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT}) { + image.usage = usage | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + assert(image.getMTLStorageMode() == MTLStorageModePrivate); + } + for (auto mode : {MTLStorageModePrivate, MTLStorageModeShared, MTLStorageModeManaged}) { + memory.storage = mode; + assert(image.getMTLStorageMode() == mode); + } + memory.storage = MTLStorageModeMemoryless; + // Native path: an enabled extension alone keeps memoryless storage, except for input attachments. + image.physicalDevice.portablePerVertex = false; + image.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + image.usage = VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + assert(image.getMTLStorageMode() == MTLStorageModePrivate); + image.physicalDevice.portablePerVertex = true; + } + image.extensions = {}; + assert(image.getMTLStorageMode() == MTLStorageModePrivate); // Existing input-attachment exception. + image.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT; + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + image.extensions.vk_KHR_fragment_shader_barycentric.enabled = true; + image._ioSurface = ℑ + assert(image.getMTLStorageMode() == MTLStorageModeShared); // IOSurface cannot use private storage. + image._ioSurface = nullptr; + StorageTexture* imported = [StorageTexture new]; + imported.storageMode = MTLStorageModeMemoryless; + image.plane._mtlTexture = (id)imported; + image.binding._deviceMemory = nullptr; // VkImportMetalTextureInfoEXT / vkSetMTLTextureMVK. + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + image.binding._deviceMemory = &memory; + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + MVKImage alias; + alias.extensions = image.extensions; + alias._isAliasable = true; + alias.binding._deviceMemory = &memory; + memory.dedicated = true; + memory._imageMemoryBindings = {&image.binding}; + assert(alias.getMTLStorageMode() == MTLStorageModeMemoryless); + image.plane._mtlTexture = nil; + assert(alias.getMTLStorageMode() == MTLStorageModePrivate); // Internally backed dedicated alias. + memory._externalMemoryHandleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_MTLTEXTURE_BIT_EXT; + memory._mtlTexture = (id)imported; + memory.storage = MTLStorageModePrivate; // Import must win even over a nonlazy Vulkan memory type. + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + assert(alias.getMTLStorageMode() == MTLStorageModeMemoryless); + imported.storageMode = MTLStorageModePrivate; + memory.storage = MTLStorageModeMemoryless; + assert(image.getMTLStorageMode() == MTLStorageModePrivate); + assert(alias.getMTLStorageMode() == MTLStorageModePrivate); + [imported release]; + puts("PASS: production image storage policy; KHR/NV, input attachments, unchanged nonlazy modes, external textures and dedicated aliases"); +} + +static void checkMetal() { + id device = MTLCreateSystemDefaultDevice(); + assert(device && "Metal device required for --metal"); + MVKDeviceMemory memory; + MVKImage image; + image.binding._deviceMemory = &memory; + image.extensions.vk_KHR_fragment_shader_barycentric.enabled = true; + MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO]; + desc.storageMode = image.getMTLStorageMode(); + desc.usage = MTLTextureUsageRenderTarget; + id texture = [device newTextureWithDescriptor:desc]; + assert(texture && texture.storageMode == MTLStorageModePrivate); + if ([device supportsFamily:MTLGPUFamilyApple1]) { + desc.storageMode = MTLStorageModeMemoryless; + id imported = [device newTextureWithDescriptor:desc]; + assert(imported); + image.plane._mtlTexture = imported; + assert(image.getMTLStorageMode() == MTLStorageModeMemoryless); + image.plane._mtlTexture = nil; + [imported release]; + } + NSError* error = nil; + id library = [device newLibraryWithSource:@"#include \nusing namespace metal; kernel void touch(device uint* p [[buffer(0)]]) { *p = 42; }" options:nil error:&error]; + assert(library && !error); + id function = [library newFunctionWithName:@"touch"]; + id pipeline = [device newComputePipelineStateWithFunction:function error:&error]; + assert(pipeline && !error); + id result = [device newBufferWithLength:4096 options:MTLResourceStorageModeShared]; + id queue = [device newCommandQueue]; + id command = [queue commandBuffer]; + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = texture; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].clearColor = MTLClearColorMake(1, 0, 0, 1); + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + [[command renderCommandEncoderWithDescriptor:pass] endEncoding]; + id compute = [command computeCommandEncoder]; + [compute setComputePipelineState:pipeline]; + [compute setBuffer:result offset:0 atIndex:0]; + [compute dispatchThreadgroups:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)]; + [compute endEncoding]; + pass.colorAttachments[0].loadAction = MTLLoadActionLoad; + [[command renderCommandEncoderWithDescriptor:pass] endEncoding]; + id blit = [command blitCommandEncoder]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(16, 16, 1) toBuffer:result destinationOffset:256 destinationBytesPerRow:64 destinationBytesPerImage:1024]; + [blit endEncoding]; + [command commit]; + [command waitUntilCompleted]; + assert(command.status == MTLCommandBufferStatusCompleted && !command.error); + assert(*(uint32_t*)result.contents == 42); + const auto* pixels = (const uint8_t*)result.contents + 256; + for (unsigned i = 0; i < 256; ++i) { assert(pixels[i * 4] == 255 && pixels[i * 4 + 1] == 0 && pixels[i * 4 + 2] == 0 && pixels[i * 4 + 3] == 255); } + [queue release]; [result release]; [pipeline release]; [function release]; [library release]; [texture release]; [device release]; + puts("PASS: Metal render/store -> compute -> render/load preserves all attachment pixels"); +} + +int main(int argc, char** argv) { + @autoreleasepool { + checkPolicy(); + if (argc > 1 && !strcmp(argv[1], "--metal")) { checkMetal(); } + } +} diff --git a/MoltenVK/Tests/PerVertexPrefillTests.cpp b/MoltenVK/Tests/PerVertexPrefillTests.cpp new file mode 100644 index 000000000..3f894dc34 --- /dev/null +++ b/MoltenVK/Tests/PerVertexPrefillTests.cpp @@ -0,0 +1,99 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +struct MVKCommandEncoder; +struct MVKCommand { + MVKCommand* _next = nullptr; + bool released = false; + std::function action; + void encode(MVKCommandEncoder* encoder) { assert(!released); action(encoder); } +}; +struct MVKCommandEncoder { + uint32_t _multiviewPassIndex = 0; + MVKCommand* _lastMultiviewPassCmd = nullptr; + bool _isEncodingStopped = false; + void endEncoding() { assert(_lastMultiviewPassCmd && !_lastMultiviewPassCmd->released); } + void encodeCommandsImpl(MVKCommand*); + void encodeCommands(MVKCommand* command) { encodeCommandsImpl(command); } +}; +constexpr int VK_NOT_READY = 1; +struct MVKCommandBuffer { + bool _canAcceptCommands = true, _isReusable = false; + unsigned _commandCount = 0; + MVKCommandEncoder* _immediateCmdEncoder = nullptr; + int* _immediateCmdEncodingContext = nullptr; + MVKCommand* _head = nullptr; + MVKCommand* _tail = nullptr; + int result = 0; + int reportError(int error, const char*) { return error; } + void setConfigurationResult(int error) { result = error; } + void releaseCommands(MVKCommand* command) { + while (command) { assert(!command->released); command->released = true; command = command->_next; } + } + void addCommand(MVKCommand*); + void releaseRecordedCommands(); + void flushImmediateCmdEncoder(); +}; +#include "PrefillMethods.inc" +int main(int argc, char**) { + // Model command sequencing for no prefill (0), deferred (1), and immediate (2/3). + // Metal encoding, deferred reservations, and autorelease pools are outside this host test. + for (unsigned style : {0u, 1u, 2u, 3u}) for (bool reusable : {false, true}) { + if (argc > 1 && !reusable) continue; + MVKCommandEncoder encoder; + MVKCommandBuffer buffer; + buffer._isReusable = reusable; + bool immediate = style >= 2; + buffer._immediateCmdEncoder = immediate ? new MVKCommandEncoder : nullptr; + buffer._immediateCmdEncodingContext = immediate ? new int(0) : nullptr; + std::vector seen; + unsigned terminators[2]{}; + MVKCommand commands[8]; + for (unsigned subpass = 0; subpass < 2; ++subpass) { + unsigned start = subpass * 3; + commands[start].action = [&, start](auto* e) { e->_multiviewPassIndex = 0; e->_lastMultiviewPassCmd = &commands[start]; }; + commands[start + 1].action = [&, subpass](auto* e) { seen.push_back(10 * subpass + e->_multiviewPassIndex); }; + commands[start + 2].action = [&, subpass](auto* e) { ++terminators[subpass]; if (e->_multiviewPassIndex == 0) { ++e->_multiviewPassIndex; } }; + } + commands[6].action = [&](auto*) { seen.push_back(99); }; + commands[7].action = [&](auto*) { seen.push_back(100); }; + for (auto& command : commands) { buffer.addCommand(&command); } + if (!immediate) { encoder.encodeCommands(buffer._head); } + assert((seen == std::vector{0, 1, 10, 11, 99, 100})); + assert(terminators[0] == 2 && terminators[1] == 2); + assert(buffer._commandCount == 8 && buffer._head == &commands[0] && buffer._tail == &commands[7]); + unsigned retained = 0; + for (auto* c = buffer._head; c; c = c->_next) { assert(!c->released); ++retained; } + assert(retained == 8); + if (style == 1 && !reusable) { buffer.releaseRecordedCommands(); } + buffer.flushImmediateCmdEncoder(); + assert(!buffer._immediateCmdEncoder && !buffer._immediateCmdEncodingContext); + if (style && !reusable) { + assert(!buffer._head && !buffer._tail); + for (const auto& command : commands) assert(command.released); + } else { for (const auto& command : commands) assert(!command.released); } + // Reset may flush and release again, but may never double-release a pooled command. + buffer.flushImmediateCmdEncoder(); + buffer.releaseRecordedCommands(); + buffer.releaseRecordedCommands(); + for (const auto& command : commands) { assert(command.released); } + } + // A device loss inside a command stops the loop: the commands recorded after it are never encoded. + { + MVKCommandEncoder encoder; + std::vector seen; + MVKCommand commands[3]; + commands[0].action = [&](auto*) { seen.push_back(0); }; + commands[1].action = [&](auto* e) { seen.push_back(1); e->_isEncodingStopped = true; }; + commands[2].action = [&](auto*) { seen.push_back(2); }; + commands[0]._next = &commands[1]; + commands[1]._next = &commands[2]; + encoder.encodeCommands(&commands[0]); + assert((seen == std::vector{0, 1})); + } + puts("no-prefill, deferred and immediate sequencing; reusable/one-time, two multiview subpasses; stop after loss: PASS"); +} diff --git a/MoltenVK/Tests/PerVertexPrepassStateTests.mm b/MoltenVK/Tests/PerVertexPrepassStateTests.mm new file mode 100644 index 000000000..5a1719a0f --- /dev/null +++ b/MoltenVK/Tests/PerVertexPrepassStateTests.mm @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Jean-Philippe Meunier + * SPDX-License-Identifier: Apache-2.0 + */ +// CPU only, no Metal device: the Vulkan encoder state the PerVertex prepass saves before a subpass, changes by applying +// the state commands of that subpass, then restores before encoding it (MVK_CONFIG_PERVERTEX_PREPASS=1). Linked against +// libMoltenVK, whose copy operators of MVKVulkanCommonEncoderState it exercises. +#include "MVKCommandEncoderState.h" +#include +#include + +static int failures = 0; +#define EXPECT(cond) do { if (!(cond)) { std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); failures++; } } while (0) + +// Binds and fills a push descriptor, as vkCmdPushDescriptorSetKHR does through a layout with a push descriptor set 0. +static void pushDescriptor(MVKCommandEncoderState& state, uint32_t size, uint8_t value) { + auto& graphics = state.updateDynamicState(MVKRenderStateFlags()); + graphics.ensurePushDescriptorSize(size); + graphics._pushDescriptor.cpuBufferSize = size; + graphics._descriptorSets[0] = &graphics._pushDescriptor; + memset(graphics._pushDescriptor.cpuBuffer, value, size); +} + +static void bindVertexBuffer(MVKCommandEncoderState& state, VkDeviceSize offset) { + MVKVertexMTLBufferBinding binding = {}; + binding.offset = offset; + state.bindVertexBuffers(2, MVKArrayRef(&binding, 1)); +} + +// grow: the subpass pushes a larger descriptor, which reallocates the live push descriptor storage. +static void expectRestored(bool grow) { + MVKCommandEncoderState state{}; + pushDescriptor(state, 64, 0x11); + const uint8_t constants[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + state.pushConstants(0, sizeof(constants), constants); + bindVertexBuffer(state, 48); + { + MVKVulkanStateSnapshot saved = state.saveVulkanState(); + pushDescriptor(state, grow ? 4096 : 64, 0x22); + const uint8_t later[8] = {9, 9, 9, 9, 9, 9, 9, 9}; + state.pushConstants(0, sizeof(later), later); + bindVertexBuffer(state, 96); + state.restoreVulkanState(std::move(saved)); + } + // The snapshot is destroyed: the restored state must own everything it points to. + const auto& graphics = state.vkGraphics(); + bool owned = graphics._pushDescriptor.cpuBuffer == reinterpret_cast(graphics._pushDescData.data()); + EXPECT(owned); + EXPECT(graphics._descriptorSets[0] == &graphics._pushDescriptor); + EXPECT(graphics._pushDescriptor.cpuBufferSize == 64); + if (owned) { + bool same = true; + for (uint32_t i = 0; i < 64; i++) { same = same && uint8_t(graphics._pushDescriptor.cpuBuffer[i]) == 0x11; } + EXPECT(same); + } + // Small push constants live in the vector's inline storage: after the move, inside the live state, not the snapshot. + const char* pushConstants = reinterpret_cast(state.vkShared()._pushConstants.data()); + EXPECT(pushConstants >= reinterpret_cast(&state) && pushConstants < reinterpret_cast(&state + 1)); + EXPECT(state.vkShared()._pushConstants.size() >= sizeof(constants) && !memcmp(pushConstants, constants, sizeof(constants))); + EXPECT(graphics._vertexBuffers[2].offset == 48); +} + +int main() { + expectRestored(false); + expectRestored(true); + if (failures) { std::fprintf(stderr, "prepass state: %d failures\n", failures); return 1; } + std::printf("prepass state: snapshot and restore passed (in place and grown push descriptor)\n"); + return 0; +} diff --git a/MoltenVK/Tests/PerVertexRestartTests.cpp b/MoltenVK/Tests/PerVertexRestartTests.cpp new file mode 100644 index 000000000..88cb4ce1c --- /dev/null +++ b/MoltenVK/Tests/PerVertexRestartTests.cpp @@ -0,0 +1,277 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "MVKPerVertexReplay.h" +#include +#include +#include +#include +using uint = uint32_t; +#ifdef MVK_RESTART_GPU_TESTS +#import +#import +#include +#include +static id gpu; +static id queue; +static id pipeline; +static id capturePipeline; +static bool serialBaseline; +#else +using std::min; +#include "PerVertexRestartKernel.inc" +#endif + +static void assemble(const uint8_t* source, uint* compact, uint* pairs, uint* triplets, uint* corners, uint* args, const uint* values, uint guardWords = 0) { + uint p[10] = {}; + std::copy_n(values, 7, p); +#ifdef MVK_RESTART_GPU_TESTS + uint primitives = mvkPerVertexPrimitiveCount(p[0], VkPrimitiveTopology(p[3])); + uint vertices = mvkPerVertexReplayVertexCount(VkPrimitiveTopology(p[3])); + size_t lengths[] = {size_t(p[0]) * p[2], size_t(mvkPerVertexRestartIndexScratchSize(p[0])) + 8 * guardWords, (size_t(2) * vertices * primitives * p[1] + 2 * guardWords) * 4, (size_t(3) * primitives * p[1] + 2 * guardWords) * 4, (size_t(vertices) * primitives * p[1] + 2 * guardWords) * 4, size_t(68 + 2 * guardWords) * 4}; + const void* host[] = {source, compact - guardWords, pairs - guardWords, triplets - guardWords, corners - guardWords, args - guardWords}; + id staging[6], buffers[6]; + id command = [queue commandBuffer]; + id upload = [command blitCommandEncoder]; + for (uint slot = 0; slot < 6; ++slot) { + staging[slot] = [gpu newBufferWithLength:std::max(lengths[slot], size_t(4)) options:MTLResourceStorageModeShared]; + assert(staging[slot]); + if (lengths[slot]) { std::memcpy(staging[slot].contents, host[slot], lengths[slot]); } + // Match production storage: GPU-local indices, prefix scratch and indirect arguments. + buffers[slot] = slot == 0 || slot == 1 || slot == 5 ? [gpu newBufferWithLength:staging[slot].length options:MTLResourceStorageModePrivate] : staging[slot]; + assert(buffers[slot]); + if (buffers[slot] != staging[slot]) { [upload copyFromBuffer:staging[slot] sourceOffset:0 toBuffer:buffers[slot] destinationOffset:0 size:staging[slot].length]; } + } + [upload endEncoding]; + id encoder = [command computeCommandEncoder]; + [encoder setComputePipelineState:pipeline]; + for (uint slot = 0; slot < 6; ++slot) { [encoder setBuffer:buffers[slot] offset:slot ? 4 * guardWords : 0 atIndex:slot]; } + NSUInteger width = std::min(pipeline.threadExecutionWidth, pipeline.maxTotalThreadsPerThreadgroup); + auto dispatch = [&](uint phase, uint step, uint scanSource, uint64_t count) { + p[7] = phase; p[8] = step; p[9] = scanSource; + [encoder setBytes:p length:sizeof(p) atIndex:6]; + [encoder dispatchThreadgroups:MTLSizeMake((count + width - 1) / width, 1, 1) threadsPerThreadgroup:MTLSizeMake(width, 1, 1)]; + [encoder memoryBarrierWithScope:MTLBarrierScopeBuffers]; + }; + if (serialBaseline) { width = 1; dispatch(0, 0, 0, 1); } + else { mvkDispatchPerVertexRestart(p[0], p[1], VkPrimitiveTopology(p[3]), dispatch); } + // Exercise the SPIRV-Cross capture ABI with production's original stage-in region + // and GPU-compacted indirect dispatch. Metal grid_size comes from the region. + id captured = nil; + if (capturePipeline && p[0] && p[1]) { + captured = [gpu newBufferWithLength:size_t(p[0]) * p[1] * 2 * sizeof(uint) options:MTLResourceStorageModeShared]; + std::memset(captured.contents, 0xcd, captured.length); + [encoder setComputePipelineState:capturePipeline]; + [encoder setBuffer:buffers[1] offset:4 * guardWords atIndex:0]; + [encoder setBuffer:captured offset:0 atIndex:1]; + [encoder setStageInRegion:MTLRegionMake2D(uint(-3), p[5], p[0], p[1])]; + [encoder dispatchThreadgroupsWithIndirectBuffer:buffers[5] indirectBufferOffset:4 * guardWords threadsPerThreadgroup:MTLSizeMake(1, 1, 1)]; + } + [encoder endEncoding]; + id readback = [command blitCommandEncoder]; + for (uint slot : {1u, 5u}) { [readback copyFromBuffer:buffers[slot] sourceOffset:0 toBuffer:staging[slot] destinationOffset:0 size:staging[slot].length]; } + [readback endEncoding]; + dispatch_semaphore_t done = dispatch_semaphore_create(0); + [command addCompletedHandler:^(id) { dispatch_semaphore_signal(done); }]; + [command commit]; + if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) { + std::fputs("FAIL: GPU command exceeded 10 seconds; stop further GPU testing\n", stderr); + std::_Exit(124); + } + if (command.status != MTLCommandBufferStatusCompleted) { + std::fprintf(stderr, "FAIL: Metal command: %s\n", command.error.description.UTF8String); + std::exit(1); + } + for (uint slot = 1; slot < 6; ++slot) { if (lengths[slot]) { std::memcpy(const_cast(host[slot]), staging[slot].contents, lengths[slot]); } } + if (captured) { + const auto* records = static_cast(captured.contents); + uint actualPrimitives = args[4] / vertices; + for (uint instance = 0; instance < p[1]; ++instance) { + for (uint primitive = 0; primitive < actualPrimitives; ++primitive) { + for (uint corner = 0; corner < 3; ++corner) { + uint record = triplets[3 * (instance * actualPrimitives + primitive) + corner]; + assert(record < p[0] * p[1]); + assert(records[2 * record] == compact[triplets[3 * primitive + corner]] + uint(-3)); + assert(records[2 * record + 1] == p[5] + instance); + } + } + } + } + std::printf("GPU: n=%u instances=%u width=%u topology=%u last=%u %.3f ms\n", p[0], p[1], p[2], p[3], p[4], 1000 * (command.GPUEndTime - command.GPUStartTime)); +#else + (void)guardWords; + mvkDispatchPerVertexRestart(p[0], p[1], VkPrimitiveTopology(p[3]), [&](uint phase, uint step, uint scanSource, uint64_t count) { + p[7] = phase; p[8] = step; p[9] = scanSource; + // Reverse traversal checks that scatter/instance work does not rely on invocation order. + for (uint64_t i = count; i; --i) { perVertexRestart(source, compact, pairs, triplets, corners, args, p, uint(i - 1)); } + }); +#endif +} + +static void check(const std::vector& input, uint bytes, VkPrimitiveTopology topology, bool last, uint instances, bool withCorners) { + constexpr uint guard = 0xabcdef01; + uint vertices = mvkPerVertexReplayVertexCount(topology); + uint maxPrimitives = mvkPerVertexPrimitiveCount(uint(input.size()), topology); + std::vector source(bytes * (input.size() + 2), 0xcc); + std::vector expectedCompact, expectedTriplets; + std::vector segment; + auto flush = [&] { + uint primitiveCount = mvkPerVertexPrimitiveCount(uint(segment.size()), topology); + std::vector pairs(2 * vertices * primitiveCount), triplets(3 * primitiveCount); + mvkPopulatePerVertexReplay(uint(segment.size()), 1, topology, last, pairs.data(), triplets.data(), nullptr); + uint base = uint(expectedCompact.size()); + for (uint record : triplets) { expectedTriplets.push_back(base + record); } + expectedCompact.insert(expectedCompact.end(), segment.begin(), segment.end()); + segment.clear(); + }; + for (uint i = 0; i < input.size(); ++i) { + uint index = input[i] < 0 ? ~0u : uint(input[i]); + for (uint b = 0; b < bytes; ++b) { source[(i + 1) * bytes + b] = uint8_t(index >> (8 * b)); } + if (input[i] < 0) { flush(); } else { segment.push_back(index); } + } + flush(); + uint primitives = uint(expectedTriplets.size()) / 3; + std::vector compact(mvkPerVertexRestartIndexScratchSize(uint(input.size())) / sizeof(uint) + 2, guard), pairs(2 * vertices * maxPrimitives * instances + 2, guard), triplets(3 * maxPrimitives * instances + 2, guard), corners(vertices * maxPrimitives * instances + 2, guard); + std::array args; args.fill(guard); + uint p[] = {uint(input.size()), instances, bytes, uint(topology), uint(last), 17, uint(withCorners)}; + assemble(source.data() + bytes, compact.data() + 1, pairs.data() + 1, triplets.data() + 1, corners.data() + 1, args.data() + 1, p, 1); + assert(compact.front() == guard && compact.back() == guard && pairs.front() == guard && pairs.back() == guard && triplets.front() == guard && triplets.back() == guard && corners.front() == guard && corners.back() == guard && args.front() == guard && args.back() == guard); + for (uint i = 0; i < expectedCompact.size(); ++i) { assert(compact[1 + i] == expectedCompact[i]); } + assert(args[1] == expectedCompact.size() && args[2] == (expectedCompact.empty() ? 0 : instances) && args[3] == 1); + assert(args[5] == primitives * vertices && args[6] == (primitives ? instances : 0) && args[7] == 0 && args[8] == 17); + assert(args[65] == 0 && args[66] == 0 && args[67] == 17 && args[68] == primitives * vertices); + for (uint instance = 0; instance < instances; ++instance) { + for (uint primitive = 0; primitive < primitives; ++primitive) { + uint key = instance * primitives + primitive; + for (uint corner = 0; corner < 3; ++corner) { + uint record = instance * uint(input.size()) + expectedTriplets[3 * primitive + corner]; + assert(triplets[1 + 3 * key + corner] == record); + if (corner >= vertices) { continue; } + uint occurrence = key * vertices + corner; + assert(pairs[1 + 2 * occurrence] == record && pairs[2 + 2 * occurrence] == key); + assert(corners[1 + occurrence] == (withCorners ? corner : guard)); + // Compacted occurrences retain the original per-instance capture stride. + assert(compact[1 + record % input.size()] + uint(-3) == expectedCompact[record % input.size()] + uint(-3)); + assert(record / input.size() + 17 == instance + 17); + } + } + } + for (uint i = uint(expectedCompact.size()); i < input.size(); ++i) { assert(compact[1 + i] == guard); } + for (uint i = primitives * instances * 3; i < maxPrimitives * instances * 3; ++i) { assert(triplets[1 + i] == guard); } +} + +static void checkReexecution() { + uint8_t source[] = {7, 8, 9}; + uint compact[27] = {}, pairs[6] = {}, triplets[3] = {}, corners[3] = {}, args[68] = {}; + uint p[] = {3, 1, 1, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, 19, 1}; + assemble(source, compact, pairs, triplets, corners, args, p); + assert(args[0] == 3 && args[4] == 3 && args[5] == 1); + // A later GPU execution can see different index data; all consumer counts must be overwritten. + for (auto& index : source) { index = 255; } + assemble(source, compact, pairs, triplets, corners, args, p); + assert(args[0] == 0 && args[1] == 0 && args[4] == 0 && args[5] == 0 && args[67] == 0); + source[1] = 42; + assemble(source, compact, pairs, triplets, corners, args, p); + assert(args[0] == 1 && args[1] == 1 && compact[0] == 42 && args[4] == 0 && args[5] == 0); +} + +#ifdef MVK_RESTART_GPU_TESTS +int main(int argc, char** argv) { + @autoreleasepool { + assert(argc == 2 || argc == 3); + serialBaseline = argc == 3 && std::strcmp(argv[2], "serial") == 0; + gpu = MTLCreateSystemDefaultDevice(); + if (!gpu) { std::fputs("FAIL: no Metal device\n", stderr); return 1; } + queue = [gpu newCommandQueue]; + NSError* error = nil; + NSString* source = [NSString stringWithContentsOfFile:[NSString stringWithUTF8String:argv[1]] encoding:NSUTF8StringEncoding error:&error]; + if (!source || serialBaseline != [source containsString:@"for (uint i = 0; i < p[0]; ++i)"]) { std::fputs("FAIL: serial mode requires the pre-parallel restart kernel; parallel mode requires the current kernel\n", stderr); return 1; } + source = [source stringByAppendingString:@"\nkernel void testCapture(const device uint* indices [[buffer(0)]], device uint2* records [[buffer(1)]], uint3 gid [[thread_position_in_grid]], uint3 size [[grid_size]], uint3 origin [[grid_origin]]) { if (any(gid >= size)) return; records[gid.y * size.x + gid.x] = uint2(indices[gid.x] + origin.x, gid.y + origin.y); }\n"]; + MTLCompileOptions* options = [MTLCompileOptions new]; + options.languageVersion = MTLLanguageVersion2_4; + id library = [gpu newLibraryWithSource:source options:options error:&error]; + if (!library) { std::fprintf(stderr, "FAIL: Metal library: %s\n", error.description.UTF8String); return 1; } + pipeline = [gpu newComputePipelineStateWithFunction:[library newFunctionWithName:@"perVertexRestart"] error:&error]; + if (!pipeline) { std::fprintf(stderr, "FAIL: Metal pipeline: %s\n", error.description.UTF8String); return 1; } + capturePipeline = [gpu newComputePipelineStateWithFunction:[library newFunctionWithName:@"testCapture"] error:&error]; + if (!capturePipeline) { std::fprintf(stderr, "FAIL: capture pipeline: %s\n", error.description.UTF8String); return 1; } + std::printf("Device: %s\n", gpu.name.UTF8String); + // Optional comparison stays inside the original serial kernel's admitted boundary. + if (argc == 3) { + assert(serialBaseline || std::strcmp(argv[2], "parallel") == 0); + capturePipeline = nil; // Keep the optional serial/parallel timings assembly-only. + std::vector input(65536, 9); + for (uint i = 127; i < input.size(); i += 257) { input[i] = -1; } + for (uint repeat = 0; repeat < 6; ++repeat) { check(input, 4, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, 1, true); } + std::puts("PASS: bounded 65536-index comparison, exact CPU oracle and guards"); + return 0; + } + checkReexecution(); + check({7, 8, 9, -1, 10, 11, 12}, 4, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, 2, true); + for (uint topology = 0; topology <= 5; ++topology) { + @autoreleasepool { + check({}, 4, VkPrimitiveTopology(topology), false, 1, true); + check(std::vector(65537, 9), 4, VkPrimitiveTopology(topology), false, 2, true); + check(std::vector(131073, -1), 1, VkPrimitiveTopology(topology), true, 3, true); + for (uint bytes : {1u, 2u, 4u}) { + for (bool last : {false, true}) { + std::vector input(131073, 9); + for (uint i = 0; i < input.size(); ++i) { input[i] = i % 31; } + for (uint step = 1; step < input.size(); step *= 2) { input[step - 1] = -1; input[step] = -1; if (step + 1 < input.size()) { input[step + 1] = -1; } } + check(input, bytes, VkPrimitiveTopology(topology), last, 3, bytes != 1); + } + } + } + } + check(std::vector(1048577, 9), 4, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, 2, true); + std::puts("PASS: bounded GPU restart matrix, exact CPU oracle and guards"); + } +} +#else +int main() { + checkReexecution(); + assert(mvkCanAssemblePerVertexRestart(65536, 1, true)); + assert(mvkCanAssemblePerVertexRestart(16384, 4, true)); + assert(mvkCanAssemblePerVertexRestart(65537, 1, true)); + assert(mvkCanAssemblePerVertexRestart(16385, 4, true)); + assert(!mvkCanAssemblePerVertexRestart(UINT32_MAX, UINT32_MAX, true)); + assert(!mvkCanAssemblePerVertexRestart(3, 1, false)); + assert(mvkPerVertexRestartIndexScratchSize(UINT32_MAX) == uint64_t(UINT32_MAX) * 36); + unsigned cases = 0; + for (uint topology = 0; topology <= 5; ++topology) { + for (uint bytes : {1u, 2u, 4u}) { + for (bool last : {false, true}) { + for (uint instances : {1u, 3u}) { + for (uint mask = 0; mask < 1024; ++mask) { + std::vector input; + for (uint i = 0; i < 10; ++i) { input.push_back(mask & (1u << i) ? -1 : int(i % 4 + 7)); } + check(input, bytes, VkPrimitiveTopology(topology), last, instances, mask & 1); + ++cases; + } + for (const auto& input : std::vector>{{}, {7}, {-1}, {7, 8}, {-1, 7, 8, 9, 10, 11, -1, -1, 12, 13, 14, 15, 16, 17, -1}}) { check(input, bytes, VkPrimitiveTopology(topology), last, instances, true); ++cases; } + // Width-specific sentinels: 255/65535 remain ordinary larger-width indices. + check({int64_t(bytes == 1 ? 254 : bytes == 2 ? 65534 : UINT32_MAX - 1), 255 - (bytes == 1), 7, -1, 8, 9, 10}, bytes, VkPrimitiveTopology(topology), last, instances, true); + } + } + } + } + check(std::vector(65536, 9), 4, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, 1, true); + check(std::vector(16384, 9), 1, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, false, 4, true); + for (uint topology = 0; topology <= 5; ++topology) { + for (bool last : {false, true}) { + for (uint count : {65535u, 65536u, 65537u, 131073u}) { + std::vector input(count, 9); + check(input, 4, VkPrimitiveTopology(topology), last, 2, true); + // Restart boundaries around prefix-pass powers of two, including consecutive markers. + for (uint step = 1; step < count; step *= 2) { + input[step - 1] = -1; + input[step] = -1; + if (step + 1 < count) { input[step + 1] = -1; } + } + check(input, 2, VkPrimitiveTopology(topology), last, 3, true); + ++cases; + } + } + } + std::printf("PASS: %u restart patterns plus large boundaries, all portable topologies/widths/provoking modes, instances and guards (production parallel kernel, CPU only)\n", cases); +} +#endif diff --git a/MoltenVK/Tests/PerVertexScratchPoolTests.mm b/MoltenVK/Tests/PerVertexScratchPoolTests.mm new file mode 100644 index 000000000..66e981ec5 --- /dev/null +++ b/MoltenVK/Tests/PerVertexScratchPoolTests.mm @@ -0,0 +1,161 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// CPU only: the PerVertex scratch pool (MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1). Size classes, storage modes, return +// to the pool only at the last reference (the completed handler of keepPerVertexScratchResident), residency once per +// created buffer, and the indirect and TES requests kept out of the pool. No Metal device is created. +#include "MVKPerVertexScratch.h" +#include +#include +#include + +static unsigned liveBuffers; +static uint8_t bytes[1 << 16]; + +@interface PoolTestBuffer : NSObject { +@public + NSUInteger _length; + bool _shared; +} +@end +@implementation PoolTestBuffer +- (NSUInteger)length { return _length; } +- (void*)contents { return _shared ? bytes : nullptr; } +- (void)dealloc { --liveBuffers; [super dealloc]; } +@end + +@interface PoolTestDevice : NSObject { +@public + unsigned calls; + unsigned failAt; + NSUInteger lastLength; +} +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options; +@end +@implementation PoolTestDevice +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options { + if (++calls == failAt) { return nil; } + auto* buffer = [PoolTestBuffer new]; + ++liveBuffers; + lastLength = length; + buffer->_length = length; + buffer->_shared = options == MTLResourceStorageModeShared; + return (id)buffer; +} +@end + +// The block is made outside the test body: a block literal in it would keep its own copy until the end of the scope. +using Completion = void (^)(void); +static Completion makeCompletion(std::shared_ptr scratch) { + return [^{ assert(scratch->buffers[0]); } copy]; +} + +int main() { + @autoreleasepool { + // Classes are powers of two from 256 bytes. + assert(MVKPerVertexScratchPool::sizeClass(1) == 8 && MVKPerVertexScratchPool::sizeClass(256) == 8); + assert(MVKPerVertexScratchPool::sizeClass(257) == 9 && MVKPerVertexScratchPool::sizeClass(512) == 9 && MVKPerVertexScratchPool::sizeClass(513) == 10); + assert(MVKPerVertexScratchPool::sizeClass(NSUInteger(1) << 40) == 40 && MVKPerVertexScratchPool::sizeClass((NSUInteger(1) << 40) + 1) == 41); + + auto* device = [PoolTestDevice new]; + std::set residents; + unsigned residentCalls = 0; + auto pool = std::make_shared(NSUInteger(1) << 20, [&](id buffer) { residents.insert(buffer); ++residentCalls; }); + + // A direct indexed draw with restart: capture (private), params, occurrences, primitives, corners (shared), index and + // restart arguments (private). Every buffer is taken from the pool, at its class size, and made resident once. + MVKPerVertexScratchRequest direct{300, 24, 12, 600, 1000, true}; + MVKPerVertexScratchReservations first; + assert(mvkReservePerVertexScratch((id)device, {direct}, first, pool)); + MVKPerVertexScratch* scratch = first[0].get(); + assert(scratch->pool == pool && device->calls == 7 && liveBuffers == 7 && residentCalls == 7); + NSUInteger expected[] = {512, 256, 256, 256, 1024, 1024, 512}; + for (size_t i = 0; i < 7; ++i) { assert(scratch->buffers[i].length == expected[i] && residents.count(scratch->buffers[i])); } + assert(!scratch->buffers[0].contents && scratch->buffers[1].contents && scratch->buffers[4].contents && !scratch->buffers[5].contents); + id taken[7]; + for (size_t i = 0; i < 7; ++i) { taken[i] = scratch->buffers[i]; } + + // The completed handler of the Metal command buffer that encoded the draw holds the last reference: while it is + // pending, the submission releasing its reservations returns nothing, and the next draw gets new buffers. + Completion handler = makeCompletion(first[0]); + first.clear(); + MVKPerVertexScratchReservations second; + assert(mvkReservePerVertexScratch((id)device, {direct}, second, pool)); + assert(device->calls == 14 && liveBuffers == 14); + for (size_t i = 0; i < 7; ++i) { assert(second[0]->buffers[i] != taken[i]); } + second.clear(); // never encoded: its buffers return at once + + // Completion: the handler runs, then Metal releases it. The buffers return to their class and storage mode. + handler(); + [handler release]; + assert(liveBuffers == 14); + MVKPerVertexScratchReservations third; + assert(mvkReservePerVertexScratch((id)device, {direct, direct}, third, pool)); + assert(device->calls == 14 && residentCalls == 14); + std::set reused; + for (auto& s : third) { for (size_t i = 0; i < 7; ++i) { assert(s->buffers[i].length == expected[i]); reused.insert(s->buffers[i]); } } + for (size_t i = 0; i < 7; ++i) { assert(reused.count(taken[i])); } + third.clear(); + + // A smaller request of the same class reuses a buffer; a larger class or the other storage mode does not. + MVKPerVertexScratchReservations smaller; + assert(mvkReservePerVertexScratch((id)device, {{257, 4, 4}}, smaller, pool)); + assert(device->calls == 14 && smaller[0]->buffers[0].length == 512 && !smaller[0]->buffers[0].contents); + smaller.clear(); + MVKPerVertexScratchReservations larger; + assert(mvkReservePerVertexScratch((id)device, {{1025, 4, 4}}, larger, pool)); + assert(device->calls == 15 && device->lastLength == 2048 && larger[0]->buffers[0].length == 2048); + larger.clear(); + // The free 2048-byte buffer is private: shared corners of that class get a new buffer (and so does the 256-byte + // private capture, a class with no free buffer). + MVKPerVertexScratchReservations otherMode; + assert(mvkReservePerVertexScratch((id)device, {{4, 4, 4, 2000}}, otherMode, pool)); + assert(device->calls == 17 && otherMode[0]->buffers[4].length == 2048 && otherMode[0]->buffers[4].contents); + otherMode.clear(); + + // The class size never exceeds the device limit: a 600 KiB request in the 1 MiB class with a 700 KiB limit. + auto clamped = std::make_shared(NSUInteger(700) << 10, nullptr); + MVKPerVertexScratchReservations limit; + assert(mvkReservePerVertexScratch((id)device, {{NSUInteger(600) << 10, 4, 4}}, limit, clamped)); + assert(limit[0]->buffers[0].length == NSUInteger(700) << 10); + limit.clear(); + + // A failed reservation publishes nothing and returns what it took: the retry reuses it. + unsigned before = device->calls; + device->failAt = before + 2; + MVKPerVertexScratchReservations failed; + assert(!mvkReservePerVertexScratch((id)device, {{4096, 4096, 4096}}, failed, pool) && failed.empty()); + device->failAt = 0; + assert(mvkReservePerVertexScratch((id)device, {{4096, 4096, 4096}}, failed, pool)); + assert(device->calls == before + 4); // the private capture taken before the failure is reused + failed.clear(); + + // Indirect plans and TES draws keep individually owned buffers: no pool, released at destruction. + MVKPerVertexScratchRequest indirect{}; + indirect.indirectCapacity = 8; + indirect.planSize = 1024; + MVKPerVertexScratchRequest tes{48, 24, 12}; + tes.tessSizes[0] = 64; + tes.tessSizes[5] = 512; + unsigned live = liveBuffers; + MVKPerVertexScratchReservations outside; + assert(mvkReservePerVertexScratch((id)device, {indirect, tes}, outside, pool)); + assert(!outside[0]->pool && !outside[1]->pool && liveBuffers == live + 1 + 6); + outside.clear(); + assert(liveBuffers == live); + + // Without a pool, the reservation is unchanged: exact lengths, released at destruction. + MVKPerVertexScratchReservations plain; + assert(mvkReservePerVertexScratch((id)device, {direct}, plain)); + assert(!plain[0]->pool && plain[0]->buffers[0].length == 300 && liveBuffers == live + 7); + plain.clear(); + assert(liveBuffers == live); + + // The pool releases its free buffers with it. + pool.reset(); + clamped.reset(); + assert(liveBuffers == 0); + [device release]; + } + puts("scratch pool: all cases passed"); + return 0; +} diff --git a/MoltenVK/Tests/PerVertexScratchTests.mm b/MoltenVK/Tests/PerVertexScratchTests.mm new file mode 100644 index 000000000..a0500ffea --- /dev/null +++ b/MoltenVK/Tests/PerVertexScratchTests.mm @@ -0,0 +1,308 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "MVKPerVertexScratch.h" +#include "MVKSubmissionTransaction.h" +#include +#include +#include + +static_assert(sizeof(MTLDispatchThreadgroupsIndirectArguments) == 12); +static_assert(sizeof(MTLDrawPrimitivesIndirectArguments) == 16); +static_assert(offsetof(MTLDrawPrimitivesIndirectArguments, instanceCount) == 4); +static_assert(offsetof(MTLDrawPrimitivesIndirectArguments, vertexStart) == 8); +static_assert(offsetof(MTLDrawPrimitivesIndirectArguments, baseInstance) == 12); + +static unsigned liveBuffers; + +// These objects only implement selectors used by the reservation code. No Metal +// device, queue, command buffer, Vulkan instance, or GPU process is created. +@interface ScratchTestBuffer : NSObject { +@public + NSUInteger _length; + bool _visible; + alignas(uint32_t) uint8_t _bytes[512]; +} +@end +@implementation ScratchTestBuffer +- (NSUInteger)length { return _length; } +- (void*)contents { return _visible ? _bytes : nullptr; } +- (void)dealloc { --liveBuffers; [super dealloc]; } +@end + +@interface ScratchTestDevice : NSObject { +@public + unsigned calls; + unsigned failAt; + unsigned failureMode; +} +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options; +@end +@implementation ScratchTestDevice +- (id)newBufferWithLength:(NSUInteger)length options:(MTLResourceOptions)options { + bool fail = ++calls == failAt; + if (fail && failureMode == 0) { return nil; } + auto* buffer = [ScratchTestBuffer new]; + ++liveBuffers; + buffer->_length = length - (fail && failureMode == 1 ? 1 : 0); + buffer->_visible = options == MTLResourceStorageModeShared && !(fail && failureMode == 2); + return (id)buffer; +} +@end + +static const std::vector requests = {{48, 24, 12}, {32, 0, 0}, {48, 24, 12}}; + +struct Submission { + MVKPerVertexScratchReservations executions[2]; +}; + +using Completion = void (^)(void); +static Completion makeCompletion(std::shared_ptr scratch) { + return [^{ assert(scratch->buffers[0].length == 48); } copy]; +} + +// One CPU-only sizing test: independent maxima, frozen Count, restart layout and hostile bounds. +static void testFrozenIndirectReplaySizing() { + uint32_t plan[276] = {}; + plan[56] = 2; + // Frozen indexed commands, five words: vertices, instances, first index, vertex offset, first instance. + uint32_t indexed[20] = {6, 1, 3, uint32_t(-4), 7, 3, 4}; + indexed[10] = UINT32_MAX; indexed[11] = UINT32_MAX; // Beyond frozen Count: must not participate. + // The same draws as nonindexed commands, four words each. + uint32_t nonindexed[16] = {6, 1, 3, 7, 3, 4}; + nonindexed[8] = UINT32_MAX; nonindexed[9] = UINT32_MAX; + MVKPerVertexScratchRequest request{}; + NSUInteger gathered = 0; + uint32_t scans = 0; + auto size = [&](const void* data, NSUInteger bytes, const void* snapshot, NSUInteger snapshotBytes, uint32_t count, uint32_t views, uint32_t stride, VkPrimitiveTopology topology, bool isIndexed, bool restart, uint64_t limit) { + return mvkSizePerVertexIndirectReplay(data, bytes, snapshot, snapshotBytes, count, views, stride, topology, isIndexed, restart, true, limit, request, gathered, scans); + }; + auto run = [&] { return size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096); }; + assert(run()); + assert(request.captureSize == 384 && request.occurrenceSize == 192 && request.primitiveSize == 96 && request.cornerSize == 96); + assert(request.indexSize == 240 && request.restart && gathered == 216 && scans == 3); + assert(!request.planSize && !request.snapshotSize && !request.indirectCapacity && !request.widenUint8 && !request.tessSizes[5]); + for (uint32_t status : {1u, 2u, 4u}) { plan[0] = status; assert(!run()); } + plan[0] = 0; + assert(!size(nullptr, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); + assert(!size(plan, 56 * 4, indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); + assert(!size(plan, sizeof(plan), nullptr, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); // No snapshot. + assert(!size(plan, sizeof(plan), indexed, 39, 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); // Two indexed draws need 40 bytes. + assert(!size(plan, sizeof(plan), nonindexed, 31, 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, true, 4096)); // Two nonindexed draws need 32. + for (uint32_t views : {0u, 33u, UINT32_MAX}) { assert(!size(plan, sizeof(plan), indexed, sizeof(indexed), 3, views, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); } + assert(!size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 0, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 4096)); + assert(!size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, true, true, 4096)); + assert(!size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 383)); + plan[56] = 4; assert(!run()); + plan[56] = UINT32_MAX; assert(!size(plan, sizeof(plan), indexed, sizeof(indexed), UINT32_MAX, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, UINT64_MAX)); + plan[56] = 2; + indexed[1] = UINT32_MAX; assert(!run()); // Expanded instances overflow uint32. + indexed[1] = 1; indexed[0] = UINT32_MAX; assert(!run()); // Dense records overflow uint32. + nonindexed[0] = UINT32_MAX; + assert(!size(plan, sizeof(plan), nonindexed, sizeof(nonindexed), 3, 1, 1, VK_PRIMITIVE_TOPOLOGY_POINT_LIST, false, false, UINT64_MAX)); // Triplet word indices overflow. + nonindexed[0] = UINT32_MAX / 6 + 3; + assert(!size(plan, sizeof(plan), nonindexed, sizeof(nonindexed), 3, 1, 1, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, false, UINT64_MAX)); // Pair word indices overflow. + assert(request.captureSize == 384 && request.indexSize == 240 && gathered == 216 && scans == 3); // Failure never publishes partial output. + indexed[0] = 6; nonindexed[0] = 6; + assert(size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 384)); // Exact Metal limit, no power-of-two rounding. + assert(size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, false, 384)); + assert(request.indexSize == 24 && !request.restart && !gathered && !scans); + assert(size(plan, sizeof(plan), nonindexed, sizeof(nonindexed), 3, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, true, 384)); + assert(request.captureSize == 384 && !request.indexSize && !request.restart); // Vulkan ignores restart for nonindexed draws. + plan[56] = 0; + assert(size(plan, sizeof(plan), indexed, sizeof(indexed), 3, 2, 0, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 0)); + assert(!request.captureSize && !request.occurrenceSize && !request.restart && !gathered && !scans); + plan[56] = 2; indexed[0] = 0; indexed[1] = UINT32_MAX; indexed[5] = UINT32_MAX; indexed[6] = 0; + assert(run() && !request.captureSize); // Inactive slots bypass count multiplication. + // A single draw freezes its command in the plan, at word 48. + plan[56] = 1; plan[48] = 1; plan[49] = 1; + assert(!size(plan, 57 * 4, plan + 48, 9 * 4, 1, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 272)); // Missing single-draw snapshot. + plan[55] = 1; + assert(size(plan, 57 * 4, plan + 48, 9 * 4, 1, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 272)); + assert(request.captureSize == 32 && request.occurrenceSize == 24 && request.primitiveSize == 12 && request.cornerSize == 12); + assert(request.indexSize == 40 && gathered == 36 && scans == 0); // Active capture, zero primitives. + assert(!size(plan, 57 * 4, plan + 48, 9 * 4, 1, 2, 16, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, true, 271)); // Restart arguments also obey Metal limits. + plan[48] = 100; plan[49] = 1; + assert(!size(plan, 57 * 4, plan + 48, 9 * 4, 1, 1, 1, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, true, true, 3999)); // Index scratch, not capture, exceeds limit. + std::puts("PASS: frozen indirect replay sizing, two-view restart maxima, indexed and nonindexed snapshots, malformed snapshots, overflow and Metal limits (CPU only)"); +} + +int main() { + testFrozenIndirectReplaySizing(); + @autoreleasepool { + auto* device = [ScratchTestDevice new]; + // Three batches, two executions each, including repeated secondary draws + // and a short draw needing capture but no replay. Ten allocations/execution. + constexpr unsigned allocationCount = 60; + for (unsigned mode = 0; mode < 2; ++mode) { + for (unsigned fail = 1; fail <= allocationCount; ++fail) { + device->calls = 0; device->failAt = fail; device->failureMode = mode; + unsigned dispatched = 0; + VkResult result = mvkSubmitTransaction(3, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + for (auto& execution : submission.executions) { + if (!mvkReservePerVertexScratch((id)device, requests, execution)) { return VK_ERROR_OUT_OF_DEVICE_MEMORY; } + } + return VK_SUCCESS; + }, [&](Submission* submission) { + ++dispatched; delete submission; return VK_SUCCESS; + }); + assert(result == VK_ERROR_OUT_OF_DEVICE_MEMORY); + assert(device->calls == fail && liveBuffers == 0 && dispatched == 0); + } + } + // The optional corner allocation belongs to the same pre-submit transaction. + for (unsigned mode = 0; mode < 3; ++mode) { + device->calls = 0; device->failAt = 5; device->failureMode = mode; + MVKPerVertexScratchReservations corners; + assert(!mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12}}, corners)); + assert(corners.empty() && liveBuffers == 0 && device->calls == 5); + } + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratchReservations corners; + assert(mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12}}, corners)); + assert(corners[0]->buffers[4].length == 12 && corners[0]->buffers[4].contents); + } + assert(liveBuffers == 0); + // UINT8 widening is private GPU scratch and participates in the same transaction. + for (unsigned mode = 0; mode < 2; ++mode) { + device->calls = 0; device->failAt = 6; device->failureMode = mode; + MVKPerVertexScratchReservations widened; + assert(!mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12, 6}}, widened)); + assert(widened.empty() && liveBuffers == 0 && device->calls == 6); + } + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratchReservations widened; + assert(mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12, 6}}, widened)); + assert(widened[0]->buffers[5].length == 6 && !widened[0]->buffers[5].contents); + } + assert(liveBuffers == 0); + // Restart's compact index and indirect argument buffers are reserved before any submission. + for (unsigned mode = 0; mode < 2; ++mode) { + for (unsigned fail = 1; fail <= 7; ++fail) { + device->calls = 0; device->failAt = fail; device->failureMode = mode; + MVKPerVertexScratchReservations restart; + assert(!mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12, 16, true}}, restart)); + assert(restart.empty() && liveBuffers == 0 && device->calls == fail); + } + } + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratchReservations restart; + assert(mvkReservePerVertexScratch((id)device, {{48, 24, 12, 12, 16, true}, {48, 24, 12, 12, 16, true}}, restart)); + assert(restart[0]->buffers[6].length == 272 && !restart[0]->buffers[6].contents); + for (unsigned i = 0; i < 7; ++i) { assert(restart[0]->buffers[i] != restart[1]->buffers[i]); } + } + assert(liveBuffers == 0); + // The GPU plan must survive a later replay reservation, including its failure path. + MVKPerVertexScratchRequest planRequest{0, 0, 0}; + planRequest.planSize = 256; + planRequest.indirectCapacity = 6; + MVKPerVertexScratchRequest replayRequest{48, 24, 12}; + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratch staged; + assert(staged.reservePlan((id)device, planRequest)); + assert(staged.buffers[12] && !staged.buffers[0]); + *(uint32_t*)staged.buffers[12].contents = 2; + device->failAt = device->calls + 2; device->failureMode = 0; + assert(!staged.reserveReplay((id)device, replayRequest)); + assert(staged.getIndirectStatus() == 2); + } + assert(liveBuffers == 0); + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratch staged; + assert(staged.reservePlan((id)device, planRequest)); + *(uint32_t*)staged.buffers[12].contents = 2; + assert(staged.reserveReplay((id)device, replayRequest)); + assert(staged.buffers[0].length == 48 && staged.getIndirectStatus() == 2); + } + assert(liveBuffers == 0); + // Several indirect draws freeze their commands in a separate shared buffer, sized by the request alone. + MVKPerVertexScratchRequest snapshotPlan = planRequest; + snapshotPlan.snapshotSize = 32; + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratch staged; + assert(staged.reservePlan((id)device, snapshotPlan)); + assert(staged.buffers[12].length == 256 && staged.buffers[13].length == 32 && staged.buffers[13].contents); + assert(!staged.reservePlan((id)device, snapshotPlan)); // Never twice. + } + assert(liveBuffers == 0); + device->calls = 0; device->failAt = 2; device->failureMode = 0; + { + MVKPerVertexScratch staged; + assert(!staged.reservePlan((id)device, snapshotPlan)); // A failed snapshot allocation fails the plan. + } + assert(liveBuffers == 0); + MVKPerVertexScratchRequest tesPlan{0, 0, 0}; + tesPlan.tessSizes[5] = 256; + device->calls = 0; device->failAt = 0; + { + MVKPerVertexScratch staged; + assert(staged.reservePlan((id)device, tesPlan)); + *(uint32_t*)staged.buffers[12].contents = 2; + assert(staged.reserveReplay((id)device, replayRequest)); + assert(staged.getTessEvalStatus() == 2); + } + assert(liveBuffers == 0); + // Reject a real-looking shared buffer with no CPU mapping. + device->calls = 0; device->failAt = 2; device->failureMode = 2; + MVKPerVertexScratchReservations unmapped; + assert(!mvkReservePerVertexScratch((id)device, requests, unmapped)); + assert(unmapped.empty() && liveBuffers == 0); + + // Failure of a later batch releases temporary references to a prefill but + // preserves the recorded reservation, so retry does not consume it. + device->calls = 0; device->failAt = 0; + MVKPerVertexScratchReservations prefilled; + assert(mvkReservePerVertexScratch((id)device, requests, prefilled)); + device->failAt = device->calls + 2; + unsigned prepared = 0; + assert(mvkSubmitTransaction(2, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + if (!prepared++) { submission.executions[0] = prefilled; return VK_SUCCESS; } + return mvkReservePerVertexScratch((id)device, requests, submission.executions[0]) ? VK_SUCCESS : VK_ERROR_OUT_OF_DEVICE_MEMORY; + }, [](Submission*) { assert(false); return VK_SUCCESS; }) == VK_ERROR_OUT_OF_DEVICE_MEMORY); + assert(liveBuffers == 10 && prefilled[0].use_count() == 1); + prefilled.clear(); + assert(liveBuffers == 0); + + // Successful retry reserves every batch before the first dispatch. Reusable + // and simultaneous executions must never share their writable scratch. + device->calls = 0; device->failAt = 0; + std::vector> inFlight; + assert(mvkSubmitTransaction(3, [](uint32_t) { return new Submission; }, [&](Submission& submission) { + for (auto& execution : submission.executions) { assert(mvkReservePerVertexScratch((id)device, requests, execution)); } + return VK_SUCCESS; + }, [&](Submission* submission) { + assert(device->calls == allocationCount); + inFlight.emplace_back(submission); return VK_SUCCESS; + }) == VK_SUCCESS); + for (const auto& first : inFlight) { + for (const auto& second : inFlight) { + for (const auto& a : first->executions) { + for (const auto& b : second->executions) { + if (&a != &b) { assert(a[0]->buffers[0] != b[0]->buffers[0]); } + } + } + } + } + // The same C++ shared ownership captured by production's Metal completion + // block survives deletion/reset of submission and recorded-command owners. + Completion completion = makeCompletion(inFlight[0]->executions[0][0]); + inFlight.clear(); + assert(liveBuffers == 4); + completion(); + [completion release]; + assert(liveBuffers == 0); + + // Host allocation failure during preparation also rolls back earlier batches. + assert(mvkSubmitTransaction(2, [](uint32_t i) { if (i) { throw std::bad_alloc(); } return new Submission; }, [&](Submission& submission) { + assert(mvkReservePerVertexScratch((id)device, requests, submission.executions[0])); return VK_SUCCESS; + }, [](Submission*) { assert(false); return VK_SUCCESS; }) == VK_ERROR_OUT_OF_HOST_MEMORY); + assert(liveBuffers == 0); + [device release]; + } + std::puts("PASS: 120 allocation failures, optional corner/restart allocation failures/success, unmapped buffer, whole-call rollback, prefill retry, independent executions, completion ownership, staged plan/replay, host OOM (CPU only)"); +} diff --git a/MoltenVK/Tests/PerVertexSharedReplayTests.cpp b/MoltenVK/Tests/PerVertexSharedReplayTests.cpp new file mode 100644 index 000000000..0c2106f33 --- /dev/null +++ b/MoltenVK/Tests/PerVertexSharedReplayTests.cpp @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Shared single-instance replay tables must equal, as a prefix, the tables each draw would have populated. +#include "MVKPerVertexReplay.h" +#include +#include +#include + +struct Tables { std::vector pairs, indices, corners; }; + +static Tables populate(uint32_t vertexCount, uint32_t instances, VkPrimitiveTopology topology, bool provokingLast) { + uint64_t primitives = std::max(uint64_t(mvkPerVertexPrimitiveCount(vertexCount, topology)) * instances, 1); + uint64_t occurrences = primitives * mvkPerVertexReplayVertexCount(topology); + Tables t{std::vector(occurrences * 2, 0xdeadbeef), std::vector(primitives * 3, 0xdeadbeef), std::vector(occurrences, 0xdeadbeef)}; + mvkPopulatePerVertexReplay(vertexCount, instances, topology, provokingLast, t.pairs.data(), t.indices.data(), t.corners.data()); + return t; +} + +static bool isPrefix(const std::vector& small, const std::vector& large, size_t count) { + return count <= large.size() && std::equal(small.begin(), small.begin() + count, large.begin()); +} + +static bool prefixMatches(uint32_t vertexCount, uint32_t instances, VkPrimitiveTopology topology, bool provokingLast) { + uint64_t primitives = uint64_t(mvkPerVertexPrimitiveCount(vertexCount, topology)) * instances; + uint64_t occurrences = primitives * mvkPerVertexReplayVertexCount(topology); + Tables draw = populate(vertexCount, instances, topology, provokingLast); + Tables shared = populate(mvkPerVertexSharedReplayVertexCount(vertexCount * instances), 1, topology, provokingLast); + return isPrefix(draw.pairs, shared.pairs, occurrences * 2) && isPrefix(draw.indices, shared.indices, primitives * 3) && isPrefix(draw.corners, shared.corners, occurrences); +} + +int main() { + const VkPrimitiveTopology topologies[] = {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}; + const uint32_t larges[] = {1023, 1024, 1025, 4097, 65535, 196608}; + unsigned checks = 0; + for (auto topology : topologies) { + for (bool provokingLast : {false, true}) { + for (uint32_t n = 0; n <= 300; ++n) { assert(prefixMatches(n, 1, topology, provokingLast)); ++checks; } + for (uint32_t n : larges) { assert(prefixMatches(n, 1, topology, provokingLast)); ++checks; } + // Every admitted draw, instanced ones included, reads exactly its own tables from the shared ones. + for (uint32_t instances = 2; instances <= 4; ++instances) { + for (uint32_t n = 0; n <= 120; ++n) { + if (mvkPerVertexReplayIsSizeIndependent(false, topology, n, instances)) { assert(prefixMatches(n, instances, topology, provokingLast)); ++checks; } + } + } + } + } + // The guard is necessary: dangling list vertices and instanced strips shift records by the vertex count. + assert(!mvkPerVertexReplayIsSizeIndependent(false, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 7, 2) && !prefixMatches(7, 2, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false)); + assert(!mvkPerVertexReplayIsSizeIndependent(false, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, 4, 2) && !prefixMatches(4, 2, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false)); + assert(mvkPerVertexReplayIsSizeIndependent(false, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 6, 2)); + assert(!mvkPerVertexReplayIsSizeIndependent(true, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 6, 1)); + assert(!mvkPerVertexReplayIsSizeIndependent(false, VK_PRIMITIVE_TOPOLOGY_POINT_LIST, 0x10000, 0x10000)); + // Capacity covers every count and only doubles. + for (uint32_t n : {0u, 1u, 1024u, 1025u, 0x80000000u, 0x80000001u, UINT32_MAX}) { assert(mvkPerVertexSharedReplayVertexCount(n) >= n); } + printf("shared replay tables: %u prefix checks passed\n", checks); + return 0; +} diff --git a/MoltenVK/Tests/PerVertexTES/EnableTestDevice.mm b/MoltenVK/Tests/PerVertexTES/EnableTestDevice.mm new file mode 100644 index 000000000..ca9d471b4 --- /dev/null +++ b/MoltenVK/Tests/PerVertexTES/EnableTestDevice.mm @@ -0,0 +1,84 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Linked only by the TES and mesh test drivers, never by shipping CMake/Xcode targets. +#include "MVKDevice.h" +#include "MVKCommandPool.h" +#include "MVKSwapchain.h" +struct TestPhysicalDevice : MVKPhysicalDevice { + static void enable(MVKPhysicalDevice* device) { + // Use the same initialization convention as MVKPhysicalDevice::initExtensions. + auto& extensions = const_cast(device->* &TestPhysicalDevice::_supportedExtensions); + // The adapter selects the experimental capture and replay path, where Metal has barycentric coordinates. + extensions.vk_KHR_fragment_shader_barycentric.enabled = device->getMetalFeatures()->shaderBarycentricCoordinates; + device->* &TestPhysicalDevice::_portablePerVertexEnabled = extensions.vk_KHR_fragment_shader_barycentric.enabled; + } + // Reproduces a GPU whose Metal buffers are smaller, before the device is created. + static void limitMetalBufferSize(MVKPhysicalDevice* device, uint64_t size) { + auto& features = const_cast(device->* &TestPhysicalDevice::_metalFeatures); + features.maxMTLBufferSize = std::min(features.maxMTLBufferSize, size); + } + // Mesh shaders need a Metal 3 GPU. + static void enableMesh(MVKPhysicalDevice* device) { + auto& extensions = const_cast(device->* &TestPhysicalDevice::_supportedExtensions); + extensions.vk_EXT_mesh_shader.enabled = [device->getMTLDevice() supportsFamily:MTLGPUFamilyMetal3]; + } +}; +extern "C" void mvkEnableTESFixtureDevice(VkPhysicalDevice device) { + TestPhysicalDevice::enable(MVKPhysicalDevice::getMVKPhysicalDevice(device)); +} +extern "C" void mvkEnableMeshTestDevice(VkPhysicalDevice device) { + TestPhysicalDevice::enableMesh(MVKPhysicalDevice::getMVKPhysicalDevice(device)); +} +extern "C" void mvkTestLimitMetalBufferSize(VkPhysicalDevice device, uint64_t size) { + TestPhysicalDevice::limitMetalBufferSize(MVKPhysicalDevice::getMVKPhysicalDevice(device), size); +} +// Loses the device as an encoder does, so that device loss tests choose the moment of the loss. +extern "C" void mvkTestMarkDeviceLost(VkDevice device) { + MVKDevice::getMVKDevice(device)->markLost(); +} +struct TestDevice : MVKDevice { + static void getLossCounters(MVKDevice* device, int64_t* pendingHandlers, uint32_t* activeEncodings) { + *pendingHandlers = (device->* &TestDevice::_pendingMTLCommandBufferHandlers).load(); + *activeEncodings = (device->* &TestDevice::_activeEncodings).load(); + } +}; +// The counts that a device loss report waits for: handlers of committed Metal command buffers, and queue submissions +// still encoding. A loss test reads them when a host wait returns the loss, which must find both at zero. +extern "C" void mvkTestGetLossCounters(VkDevice device, int64_t* pendingHandlers, uint32_t* activeEncodings) { + TestDevice::getLossCounters(MVKDevice::getMVKDevice(device), pendingHandlers, activeEncodings); +} + +struct TestAllocationPool : MVKMTLBufferAllocationPool { + static uint64_t outstanding(MVKMTLBufferAllocationPool* pool) { + uint64_t count = 0; + for (auto& tracker : pool->* &TestAllocationPool::_mtlBuffers) { count += tracker.allocationCount; } + return count; + } +}; +struct TestAllocator : MVKMTLBufferAllocator { + static uint64_t outstanding(MVKMTLBufferAllocator* allocator) { + uint64_t count = 0; + for (auto* pool : allocator->* &TestAllocator::_regionPools) { count += TestAllocationPool::outstanding(pool); } + return count; + } +}; +struct TestEncodingPool : MVKCommandEncodingPool { + static uint64_t outstanding(MVKCommandEncodingPool* pool) { + return TestAllocator::outstanding(&(pool->* &TestEncodingPool::_mtlBufferAllocator)) + + TestAllocator::outstanding(&(pool->* &TestEncodingPool::_privateMtlBufferAllocator)) + + TestAllocator::outstanding(&(pool->* &TestEncodingPool::_dedicatedMtlBufferAllocator)); + } +}; +// The temporary Metal buffer allocations of a command pool that have not been returned to it: a cleanup test checks +// that each one returns exactly once. +extern "C" uint64_t mvkTestGetOutstandingCommandPoolAllocations(VkCommandPool commandPool) { + return TestEncodingPool::outstanding(((MVKCommandPool*)commandPool)->getCommandEncodingPool()); +} +struct TestSwapchain : MVKSwapchain { + static uint32_t unpresented(MVKSwapchain* swapchain) { return (swapchain->* &TestSwapchain::_unpresentedImageCount).load(); } +}; +// The presentations of a swapchain that have begun and not ended: a presentation test checks that its presentations +// have begun before it destroys the swapchain. +extern "C" uint32_t mvkTestGetUnpresentedImageCount(VkSwapchainKHR swapchain) { + return TestSwapchain::unpresented((MVKSwapchain*)swapchain); +} diff --git a/MoltenVK/Tests/PerVertexTES/build-library.sh b/MoltenVK/Tests/PerVertexTES/build-library.sh new file mode 100755 index 000000000..9a6315722 --- /dev/null +++ b/MoltenVK/Tests/PerVertexTES/build-library.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# Builds an arm64 Debug MoltenVK from this checkout for the TES topology tests. +# Usage: build-library.sh [extra CMake source define] +# ADMISSION defines MVK_TEST_TES_PERVERTEX_ADMISSION; TEST_DEVICE links EnableTestDevice.mm. Neither is +# ever part of shipping CMake/Xcode targets, and the public extension gate is not edited. +# MVK_TEST_PLATFORM=ios (or ios-simulator) builds the same library for iOS devices (or the arm64 simulator), iOS +# 16.0, for the Vulkan trial app. The +# MoltenVK CMake project only targets macOS: its AppKit and IOKit lookups are pointed at UIKit and CoreGraphics. +# MVK_TEST_ARCH (default arm64) selects the architecture, for example x86_64 for an Intel Mac. +# MVK_TEST_BUILD_TYPE (default Debug) selects the CMake build type, for example Release for performance measurements. +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../../.." && pwd) +output=$(mkdir -p "$1" && cd "$1" && pwd) +admission=$2 device=$3 extra=${4:-} +mkdir -p "$output/driver" +cat > "$output/driver/CMakeLists.txt" <> "$output/driver/CMakeLists.txt"; fi +cross=$(cd "$root/External/SPIRV-Cross" && pwd -P) +test "$(git -C "$cross" rev-parse HEAD)" = "$(cat "$root/ExternalRevisions/SPIRV-Cross_repo_revision")" +git -C "$cross" diff --quiet HEAD +platform=${MVK_TEST_PLATFORM:-macos} +platform_flags=() +if [[ $platform == ios || $platform == ios-simulator ]]; then + sysroot=$([[ $platform == ios ]] && echo iphoneos || echo iphonesimulator) + sdk=$(xcrun --sdk "$sysroot" --show-sdk-path) + platform_flags=(-DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT="$sysroot" -DCMAKE_OSX_DEPLOYMENT_TARGET=16.0 + -DAPPKIT_LIBRARY="$sdk/System/Library/Frameworks/UIKit.framework" -DIOKIT_LIBRARY="$sdk/System/Library/Frameworks/CoreGraphics.framework") +elif [[ $platform != macos ]]; then + echo "MVK_TEST_PLATFORM must be macos, ios or ios-simulator"; exit 64 +fi +cmake -S "$output/driver" -B "$output/build" -G Ninja -DCMAKE_BUILD_TYPE="${MVK_TEST_BUILD_TYPE:-Debug}" -DCMAKE_OSX_ARCHITECTURES="${MVK_TEST_ARCH:-arm64}" ${platform_flags[@]+"${platform_flags[@]}"} > "$output/configure.log" 2>&1 +cmake --build "$output/build" --target MoltenVK -j "$(sysctl -n hw.ncpu)" > "$output/build.log" 2>&1 +library=$output/build/moltenvk/MoltenVK/libMoltenVK.1.4.3.dylib +{ + echo "moltenvk_head=$(git -C "$root" rev-parse HEAD) dirty_files=$(git -C "$root" status --porcelain --untracked-files=no | wc -l | tr -d ' ')" + echo "spirv_cross_head=$(git -C "$cross" rev-parse HEAD)" + echo "admission=$admission test_device=$device extra=${extra:-none} platform=$platform$([[ ${MVK_TEST_ARCH:-arm64} == arm64 ]] || echo " arch=$MVK_TEST_ARCH")$([[ ${MVK_TEST_BUILD_TYPE:-Debug} == Debug ]] || echo " build_type=$MVK_TEST_BUILD_TYPE")" + echo "library_sha256=$(shasum -a 256 "$library" | cut -d' ' -f1)" +} | tee "$output/PROVENANCE.txt" diff --git a/MoltenVK/Tests/PerVertexTES/ordinary.frag b/MoltenVK/Tests/PerVertexTES/ordinary.frag new file mode 100644 index 000000000..092516406 --- /dev/null +++ b/MoltenVK/Tests/PerVertexTES/ordinary.frag @@ -0,0 +1,12 @@ +#version 450 +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +layout(location = 0) in vec3 color; +layout(location = 0) out vec4 outputColor; +#ifdef RESOURCES +layout(set = 0, binding = 1) uniform Scale { vec4 scale; } ubo; +#define SCALE * ubo.scale +#else +#define SCALE +#endif +void main() { outputColor = vec4(color, 1.0) SCALE; } diff --git a/MoltenVK/Tests/PerVertexTopologyTests.mm b/MoltenVK/Tests/PerVertexTopologyTests.mm new file mode 100644 index 000000000..ff1a1be2b --- /dev/null +++ b/MoltenVK/Tests/PerVertexTopologyTests.mm @@ -0,0 +1,129 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "MVKCmdDraw.h" +#include +#include +#include +#include +#include "PerVertexUint8Kernel.inc" + +static void checkUint8Kernel() { + std::array source{}; + for (uint32_t i = 0; i < 256; ++i) { source[1 + i] = uint8_t(i); } + // Exact and partial threadgroups must cover the requested range, including index 255. + for (uint32_t count : {1u, 31u, 32u, 33u, 255u, 256u}) { + for (uint32_t width : {16u, 32u, 64u}) { + std::vector result(count + 2, 0xbeef); + uint32_t groups = count / width, remainder = count % width; + for (uint32_t i = 0; i < groups * width; ++i) { convertUint8IndicesRaw(source.data() + 1, result.data() + 1, i); } + for (uint32_t i = 0; i < remainder; ++i) { convertUint8IndicesRaw(source.data() + 1 + groups * width, result.data() + 1 + groups * width, i); } + assert(result.front() == 0xbeef && result.back() == 0xbeef); + for (uint32_t i = 0; i < count; ++i) { assert(result[1 + i] == i); } + } + } +} + +static void checkPayloads() { + struct Case { VkPrimitiveTopology topology; uint32_t count; std::vector triplets; }; + const Case cases[] = { + {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, 3, {0,0,0, 1,1,1, 2,2,2}}, + {VK_PRIMITIVE_TOPOLOGY_LINE_LIST, 5, {0,1,1, 2,3,3}}, + {VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, 4, {0,1,1, 1,2,2, 2,3,3}} + }; + for (const auto& c : cases) { + for (bool last : {false, true}) { + for (uint32_t instances : {1u, 3u}) { + uint32_t primitives = uint32_t(c.triplets.size()) / 3; + uint32_t vertices = mvkPerVertexReplayVertexCount(c.topology); + uint32_t count = primitives * instances * vertices; + constexpr uint32_t guard = 0xcafef00d; + std::vector pairs(2 * count + 2, guard), triplets(3 * primitives * instances + 2, guard), corners(count + 2, guard); + mvkPopulatePerVertexReplay(c.count, instances, c.topology, last, pairs.data() + 1, triplets.data() + 1, corners.data() + 1); + assert(pairs.front() == guard && pairs.back() == guard && triplets.front() == guard && triplets.back() == guard && corners.front() == guard && corners.back() == guard); + for (uint32_t instance = 0; instance < instances; ++instance) { + for (uint32_t primitive = 0; primitive < primitives; ++primitive) { + uint32_t key = instance * primitives + primitive; + for (uint32_t corner = 0; corner < 3; ++corner) { + uint32_t record = instance * c.count + c.triplets[3 * primitive + corner]; + assert(triplets[1 + 3 * key + corner] == record); + if (corner >= vertices) { continue; } + uint32_t occurrence = key * vertices + corner; + assert(pairs[1 + 2 * occurrence] == record && pairs[2 + 2 * occurrence] == key); + assert(corners[1 + occurrence] == corner); // point (1,0,0); line endpoints (1,0,0),(0,1,0). + // Dense capture must retain repeated indices, signed baseVertex and firstInstance. + for (VkIndexType type : {VK_INDEX_TYPE_UINT8, VK_INDEX_TYPE_UINT16, VK_INDEX_TYPE_UINT32}) { + const uint32_t input[] = {99, 99, 255, 7, 255, 9, 10}; + uint32_t firstIndex = 2, firstInstance = 11; + int32_t baseVertex = -3; + std::vector> captured(c.count * instances); + for (uint32_t i = 0; i < instances; ++i) { + for (uint32_t v = 0; v < c.count; ++v) { + uint32_t index = type == VK_INDEX_TYPE_UINT8 ? uint8_t(input[firstIndex + v]) : type == VK_INDEX_TYPE_UINT16 ? uint16_t(input[firstIndex + v]) : input[firstIndex + v]; + captured[i * c.count + v] = {index + uint32_t(baseVertex), i + firstInstance}; + } + } + assert(captured[pairs[1 + 2 * occurrence]][0] == input[firstIndex + c.triplets[3 * primitive + corner]] - 3); + assert(captured[triplets[1 + 3 * key + corner]][1] == instance + firstInstance); + } + // Nonindexed firstVertex is applied during capture, never to dense replay addresses. + assert(pairs[1 + 2 * occurrence] % c.count + 17 == c.triplets[3 * primitive + corner] + 17); + } + uint32_t provokingCorner = last ? vertices - 1 : 0; + assert(pairs[1 + 2 * (key * vertices + provokingCorner)] == instance * c.count + c.triplets[3 * primitive + provokingCorner]); + } + } + auto saved = pairs; + mvkPopulatePerVertexReplay(c.count, instances, c.topology, last, pairs.data() + 1, triplets.data() + 1, nullptr); + assert(saved == pairs); + } + } + } + for (auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP}) { + uint32_t sentinel = 99; + mvkPopulatePerVertexReplay(0, 3, topology, false, &sentinel, &sentinel, &sentinel); + mvkPopulatePerVertexReplay(3, 0, topology, true, &sentinel, &sentinel, &sentinel); + if (topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST) { mvkPopulatePerVertexReplay(1, 3, topology, false, &sentinel, &sentinel, &sentinel); } + assert(sentinel == 99); + } +} + +static void checkBounds() { + assert(!mvkPerVertexRequiresRestartAssembly(false, false)); + assert(!mvkPerVertexRequiresRestartAssembly(false, true)); // Legal nonindexed draws ignore restart. + assert(!mvkPerVertexRequiresRestartAssembly(true, false)); // UINT8 255 is an ordinary index. + assert(mvkPerVertexRequiresRestartAssembly(true, true)); // Reject before capture or scratch use. + const auto point = VK_PRIMITIVE_TOPOLOGY_POINT_LIST, line = VK_PRIMITIVE_TOPOLOGY_LINE_LIST, strip = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + assert(mvkCanEncodePerVertexDraw(2, 1, 1, point, 32)); + assert(!mvkCanEncodePerVertexDraw(3, 1, 1, point, 32)); // triplets, not occurrence pairs, are limiting. + assert(mvkCanEncodePerVertexDraw(4, 1, 1, line, 32)); + assert(!mvkCanEncodePerVertexDraw(6, 1, 1, line, 32)); + assert(mvkCanEncodePerVertexDraw(3, 1, 1, strip, 32)); + assert(!mvkCanEncodePerVertexDraw(4, 1, 1, strip, 32)); + assert(mvkCanEncodePerVertexDraw(1, 2, 1, point, 32)); + assert(!mvkCanEncodePerVertexDraw(1, 3, 1, point, 32)); + assert(!mvkCanEncodePerVertexDraw(1, 1, 1, point, 0)); + assert(!mvkCanEncodePerVertexDraw(1, 1, 0, point, 32)); + assert(mvkCanEncodePerVertexDraw(UINT32_MAX / 3, 1, 1, point, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(UINT32_MAX / 3 + 1, 1, 1, point, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(UINT32_MAX, UINT32_MAX, UINT32_MAX, strip, UINT64_MAX)); + assert(!mvkCanEncodePerVertexDraw(4, 1, 1, VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY, 1024)); + assert(!mvkCanEncodePerVertexDraw(3, 1, 1, VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, 1024)); + for (auto type : {VK_INDEX_TYPE_UINT8, VK_INDEX_TYPE_UINT16, VK_INDEX_TYPE_UINT32}) { + uint64_t size = mvkPerVertexIndexSize(type); + assert(mvkPerVertexIndexRange(3, 2, type, 5 * size, 7, 7 + 5 * size)); + assert(!mvkPerVertexIndexRange(3, 2, type, 5 * size - 1, 7, 7 + 5 * size)); + assert(!mvkPerVertexIndexRange(3, 2, type, 5 * size, 7, 7 + 5 * size - 1)); + assert(!mvkPerVertexIndexRange(3, 2, type, 5 * size, UINT64_MAX, UINT64_MAX)); + assert(!mvkPerVertexIndexRange(3, 2, type, 5 * size, 8, 7)); + assert(!mvkPerVertexIndexRange(1, UINT32_MAX, type, size * UINT32_MAX, 0, UINT64_MAX)); + assert(mvkPerVertexIndexRange(UINT32_MAX, UINT32_MAX, type, 2 * size * UINT32_MAX, 0, UINT64_MAX)); + } + assert(!mvkPerVertexIndexRange(1, 0, VK_INDEX_TYPE_NONE_KHR, 100, 0, 100)); +} + +int main() { + checkUint8Kernel(); + checkPayloads(); + checkBounds(); + std::puts("PASS: point/line-list/line-strip triplets and raster corners, both provoking modes, indexed/nonindexed instance address models, production UINT8 kernel (all byte values and dispatch tails), sentinels, empty/incomplete primitives, unsupported topologies and integer/buffer boundaries (CPU only)"); +} diff --git a/MoltenVK/Tests/pervertex-capacity.vert b/MoltenVK/Tests/pervertex-capacity.vert new file mode 100644 index 000000000..28a35325b --- /dev/null +++ b/MoltenVK/Tests/pervertex-capacity.vert @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +#if DENSE +layout(location = 0) out vec4 values[LOCATIONS - 3]; +layout(location = LOCATIONS - 3) out vec2 tail[3]; +#else +layout(location = 0) out float values[LOCATIONS]; +#endif +void main() { + gl_Position = vec4(float(gl_VertexIndex), 0.0, 0.0, 1.0); +#if DENSE + for (int i = 0; i < LOCATIONS - 3; ++i) { values[i] = vec4(float(i + gl_VertexIndex)); } + for (int i = 0; i < 3; ++i) { tail[i] = vec2(float(i + gl_VertexIndex)); } +#else + for (int i = 0; i < LOCATIONS; ++i) { values[i] = float(i + gl_VertexIndex); } +#endif +} diff --git a/MoltenVK/Tests/pervertex-dynamic-reference.vert b/MoltenVK/Tests/pervertex-dynamic-reference.vert new file mode 100644 index 000000000..f530b74fd --- /dev/null +++ b/MoltenVK/Tests/pervertex-dynamic-reference.vert @@ -0,0 +1,50 @@ +#version 460 +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Explicit triangulation of one draw of PerVertexDynamicTopologyTests.mm, drawn as a triangle list without PerVertexKHR. +// Vertex k is corner k % 3 of triangle k / 3. The corners come from the draw's six vertices (4 to 9) in the order of the +// Vulkan barycentric order table, provoking vertex first, as the CTS encodes it (getDataVertexFormula in +// vktFragmentShadingBarycentricTests.cpp): list (3p, 3p+1, 3p+2); strip (p, even ? p+1 : p+2, even ? p+2 : p+1); fan +// (p+1, p+2, 0). Every vertex carries the three corner values flat and a one-hot barycentric weight. +// rotate = 1 is the negative control: the corners of every triangle turn by one, a consistent permutation. +// Positions and values repeat pervertex-indirect.vert for DrawID 0 and gl_BaseVertex & 3 == 0. VARIED_W repeats its w +// per vertex and adds the barycentric weight interpolated without perspective. +layout(push_constant) uniform Draw { uint topology; uint rotate; } draw; // VkPrimitiveTopology: 3 list, 4 strip, 5 fan +layout(location = 0) flat out vec3 corner0; +layout(location = 1) flat out vec3 corner1; +layout(location = 2) flat out vec3 corner2; +layout(location = 3) out vec3 barycentric; +#ifdef VARIED_W +layout(location = 4) noperspective out vec3 barycentricLinear; +#endif +const vec2 corners[14] = vec2[14](vec2(4), vec2(4), vec2(4), vec2(4), + vec2(-0.8, -0.8), vec2(0.8, -0.8), vec2(0.8, 0.8), vec2(0.8, 0.8), vec2(-0.8, 0.8), vec2(-0.8, -0.8), + vec2(-0.8, -0.8), vec2(0.8, -0.8), vec2(0.8, 0.8), vec2(-0.8, 0.8)); +uint source(uint p, uint corner) { + corner = (corner + draw.rotate) % 3u; + bool even = (p & 1u) == 0u; + uint v = draw.topology == 3u ? 3u * p + corner + : draw.topology == 4u ? (corner == 0u ? p : even == (corner == 1u) ? p + 1u : p + 2u) + : (corner == 0u ? p + 1u : corner == 1u ? p + 2u : 0u); + return 4u + v; +} +vec3 value(uint v, uint instance) { + vec3 color = vec3(float(v % 5u) * 0.2, float(v % 3u) * 0.4 + 0.1 * float(0u), 0.25 * float(uint(gl_BaseVertex) & 3u) + 0.05 * float(gl_BaseInstance)); + return instance >= 3u ? color.gbr : color; +} +void main() { + uint k = uint(gl_VertexIndex), p = k / 3u, corner = k % 3u, instance = uint(gl_InstanceIndex), v = source(p, corner); + corner0 = value(source(p, 0u), instance); + corner1 = value(source(p, 1u), instance); + corner2 = value(source(p, 2u), instance); + barycentric = vec3(corner == 0u, corner == 1u, corner == 2u); +#ifdef VARIED_W + barycentricLinear = barycentric; +#endif + vec2 position = v < 14u ? corners[v] : vec2(4.0); + float x = instance == 2u ? -0.5 : instance >= 3u ? 0.5 : 4.0; + gl_Position = vec4(position * vec2(0.45, 0.6) + vec2(x, -0.2 + 0.25 * float(0u)), 0.0, 1.0); +#ifdef VARIED_W + gl_Position *= 1.0 + 0.75 * float(v % 3u); +#endif +} diff --git a/MoltenVK/Tests/pervertex-dynamic-topology.frag b/MoltenVK/Tests/pervertex-dynamic-topology.frag new file mode 100644 index 000000000..f47dab8a9 --- /dev/null +++ b/MoltenVK/Tests/pervertex-dynamic-topology.frag @@ -0,0 +1,39 @@ +#version 450 +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// ORDINARY (default): the colour interpolated by the rasterizer, without PerVertexKHR, so that the pipeline takes +// MoltenVK's ordinary draw path. +// WEIGHTS: PerVertexKHR values and BaryCoordKHR, through the portable capture and replay. +// REFERENCE: the same output without PerVertexKHR, from the explicit triangulation of pervertex-dynamic-reference.vert. +// NOPERSPECTIVE, with WEIGHTS or REFERENCE: BaryCoordNoPerspKHR, against the weight interpolated without perspective. +// Alpha 0.5 exposes draw order when blending. +#ifdef WEIGHTS +#extension GL_EXT_fragment_shader_barycentric : require +#endif +layout(location = 0) out vec4 outputColor; +// R and G carry the first two barycentric weights; B combines the three vertex values with fixed weights by index. +// A consistent permutation of corners and weights keeps sum(value * weight) but changes all three channels. +vec4 weigh(vec3 weights, vec3 value0, vec3 value1, vec3 value2) { + return vec4(weights.x, weights.y, 0.5 * dot(vec3(0.2, 0.3, 0.5), vec3(value0.x, value1.x, value2.x)) + 0.5 * dot(vec3(0.5, 0.2, 0.3), vec3(value0.y, value1.y, value2.y)), 0.5); +} +#if defined(WEIGHTS) +layout(location = 0) pervertexEXT in vec3 color[3]; +#ifdef NOPERSPECTIVE +void main() { outputColor = weigh(gl_BaryCoordNoPerspEXT, color[0], color[1], color[2]); } +#else +void main() { outputColor = weigh(gl_BaryCoordEXT, color[0], color[1], color[2]); } +#endif +#elif defined(REFERENCE) +layout(location = 0) flat in vec3 corner0; +layout(location = 1) flat in vec3 corner1; +layout(location = 2) flat in vec3 corner2; +#ifdef NOPERSPECTIVE +layout(location = 4) noperspective in vec3 barycentric; +#else +layout(location = 3) in vec3 barycentric; +#endif +void main() { outputColor = weigh(barycentric, corner0, corner1, corner2); } +#else +layout(location = 0) in vec3 color; +void main() { outputColor = vec4(color, 0.5); } +#endif diff --git a/MoltenVK/Tests/pervertex-indirect.frag b/MoltenVK/Tests/pervertex-indirect.frag new file mode 100644 index 000000000..24fbb5bdd --- /dev/null +++ b/MoltenVK/Tests/pervertex-indirect.frag @@ -0,0 +1,20 @@ +#version 450 +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Order-sensitive weights of the three original vertex values; alpha 0.5 exposes draw order when blending. +#extension GL_EXT_fragment_shader_barycentric : require +layout(location = 0) pervertexEXT in vec3 color[3]; +layout(location = 0) out vec4 outputColor; +#ifdef PORTABLE +// Also reads BaryCoordKHR, which adds nothing (a barycentric coordinate is never negative) but makes the pipeline take +// MoltenVK's portable capture and replay on every GPU, including those where plain PerVertexKHR is native. +#define ALPHA (0.5 + min(gl_BaryCoordEXT.x, 0.0)) +#else +#define ALPHA 0.5 +#endif +#ifdef SWAP +// Negative control: the first two corners exchanged must no longer match the reference. +void main() { outputColor = vec4(color[1] * 0.2 + color[0] * 0.3 + color[2] * 0.5, ALPHA); } +#else +void main() { outputColor = vec4(color[0] * 0.2 + color[1] * 0.3 + color[2] * 0.5, ALPHA); } +#endif diff --git a/MoltenVK/Tests/pervertex-indirect.vert b/MoltenVK/Tests/pervertex-indirect.vert new file mode 100644 index 000000000..5ef8ef938 --- /dev/null +++ b/MoltenVK/Tests/pervertex-indirect.vert @@ -0,0 +1,32 @@ +#version 460 +// Copyright 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Records every invocation, then folds DrawID and first/base values into position and colour. +// REFERENCE takes DrawID from a push constant so that direct draws reproduce indirect draw N. +layout(location = 0) out vec3 color; +layout(std430, set = 0, binding = 0) buffer Effects { uint count; uint pad0, pad1, pad2; uvec4 records[]; } effects; +#ifdef REFERENCE +layout(push_constant) uniform Draw { uint drawId; } draw; +#define DRAW_ID draw.drawId +#else +#define DRAW_ID uint(gl_DrawID) +#endif +const vec2 corners[14] = vec2[14](vec2(4), vec2(4), vec2(4), vec2(4), + vec2(-0.8, -0.8), vec2(0.8, -0.8), vec2(0.8, 0.8), vec2(0.8, 0.8), vec2(-0.8, 0.8), vec2(-0.8, -0.8), + vec2(-0.8, -0.8), vec2(0.8, -0.8), vec2(0.8, 0.8), vec2(-0.8, 0.8)); +void main() { + uint v = uint(gl_VertexIndex), instance = uint(gl_InstanceIndex), drawId = DRAW_ID; + uint slot = atomicAdd(effects.count, 1u); + if (slot < 262144u) { effects.records[slot] = uvec4(v, instance, drawId | (uint(gl_BaseInstance) << 16), uint(gl_BaseVertex)); } + vec2 p = v < 14u ? corners[v] : vec2(4.0); + color = vec3(float(v % 5u) * 0.2, float(v % 3u) * 0.4 + 0.1 * float(drawId), 0.25 * float(uint(gl_BaseVertex) & 3u) + 0.05 * float(gl_BaseInstance)); + // Instances from 3 on overlap on the right half, so large instance counts stay visible. + if (instance >= 3u) { color = color.gbr; } + float x = instance == 2u ? -0.5 : instance >= 3u ? 0.5 : 4.0; + gl_Position = vec4(p * vec2(0.45, 0.6) + vec2(x, -0.2 + 0.25 * float(drawId)), 0.0, 1.0); +#ifdef VARIED_W + // Same window position, a different w for each corner of a triangle: perspective and linear interpolation differ. + gl_Position *= 1.0 + 0.75 * float(v % 3u); +#endif + gl_PointSize = 1.0; +} diff --git a/MoltenVK/Tests/pervertex-split.frag b/MoltenVK/Tests/pervertex-split.frag new file mode 100644 index 000000000..2e1a6c594 --- /dev/null +++ b/MoltenVK/Tests/pervertex-split.frag @@ -0,0 +1,8 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// Reads the three PerVertexKHR corner colors: a portable capture and replay on GPUs without native vertex_value. +#version 450 +#extension GL_EXT_fragment_shader_barycentric : require +layout(location = 0) pervertexEXT in vec3 color[]; +layout(location = 0) out vec4 fragColor; +void main() { fragColor = vec4(color[0] * 0.5 + color[1] * 0.25 + color[2] * 0.25, 1.0); } diff --git a/MoltenVK/Tests/pervertex-split.vert b/MoltenVK/Tests/pervertex-split.vert new file mode 100644 index 000000000..ee122c9a2 --- /dev/null +++ b/MoltenVK/Tests/pervertex-split.vert @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +// A full-screen triangle at depth 0.5, green at every corner (PerVertexAttachmentSplitTests.mm). +#version 450 +layout(location = 0) out vec3 color; +void main() { + vec2 corners[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(corners[gl_VertexIndex], 0.5, 1.0); + color = vec3(0.0, 1.0, 0.0); +} diff --git a/MoltenVK/Tests/run-pervertex-capture-variant-tests.sh b/MoltenVK/Tests/run-pervertex-capture-variant-tests.sh new file mode 100755 index 000000000..3670724b5 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-capture-variant-tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:?Output directory required} +mkdir -p "$output" +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import os, sys +root, output = map(Path, sys.argv[1:]) +objects = root / 'MoltenVK/MoltenVK/GPUObjects' +source = Path(os.environ.get('MVK_MULTIVIEW_PIPELINE_SOURCE', objects / 'MVKPipeline.mm')).read_text() +header = (objects / 'MVKPipeline.h').read_text() +start = source.index('void MVKGraphicsPipeline::adjustVertexInputForMultiview(') +parts = [source[start:source.index('\n}', start) + 2]] +for name, return_type, args in [('getPerVertexCapturePipelineState', 'id', 'uint32_t viewCount'), ('getPerVertexIndexedCapturePipelineState', 'id', 'bool index32, uint32_t viewCount')]: + start = header.index(' const {', header.index(name + '(')) + parts.append(return_type + ' MVKGraphicsPipeline::' + name + '(' + args + ')' + header[start:header.index('\n\t}', start) + 3]) +start = source.index('bool captured = ', source.index('bool MVKGraphicsPipeline::addPerVertexReplayShaderToPipeline(')) +parts.append('bool MVKGraphicsPipeline::build(MTLRenderPipelineDescriptor* plDesc, const VkGraphicsPipelineCreateInfo* pCreateInfo) { int shaderConfig = 0; const void* pVertexSS = nullptr; (void)shaderConfig; (void)pVertexSS;\n' + source[start:source.index('\n\tplDesc.inputPrimitiveTopology', start)] + '\nreturn captured;\n}') +start = source.index('\tfor (uint32_t i = 0; i < 31; i++)', source.index('bool MVKGraphicsPipeline::addPerVertexIndexedCapturePipelines(')) +parts.append('bool MVKGraphicsPipeline::addPerVertexIndexedCapturePipelines(MTLVertexDescriptor* vertexDesc, int, const void*, uint32_t viewCount) { auto* plDesc = [MTLComputePipelineDescriptor new]; plDesc.stageInputDescriptor = [MTLStageInputOutputDescriptor stageInputOutputDescriptor];\n' + source[start:source.index('\n\tMSLPerVertexInputBuffer expectedLayout', start)] + '\nauto& states = _perVertexCapturePipelineStates[viewCount]; states.index16 = (id)[plDesc.stageInputDescriptor copy]; states.index32 = (id)[plDesc.stageInputDescriptor copy]; [plDesc release]; return true;\n}') +(output / 'CaptureVariantMethods.inc').write_text('\n'.join(parts)) +PY +xcrun clang++ -std=c++17 -Wall -Wextra -Werror -Wno-unused-parameter -fsanitize=address,undefined -fno-sanitize-recover=all \ + -I"$output" -I"$root/External/Vulkan-Headers/include" "$test_dir/PerVertexCaptureVariantTests.mm" -framework Foundation -framework Metal -o "$output/capture-variant-tests" +"$output/capture-variant-tests" diff --git a/MoltenVK/Tests/run-pervertex-draw-size-tests.sh b/MoltenVK/Tests/run-pervertex-draw-size-tests.sh new file mode 100644 index 000000000..40e9c312f --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-draw-size-tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-draw-size.XXXXXX")} +mkdir -p "$output" +includes=(-I"$root/Common" -I"$root/MoltenVK/include" -I"$root/External/Vulkan-Headers/include") +for directory in API Commands GPUObjects Layers OS Utility Vulkan; do + includes+=(-I"$root/MoltenVK/MoltenVK/$directory") +done +xcrun clang++ -std=c++17 -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations "${includes[@]}" "$test_dir/PerVertexDrawSizeTests.mm" -framework Foundation -framework Metal -o "$output/draw-size-tests" +"$output/draw-size-tests" diff --git a/MoltenVK/Tests/run-pervertex-dynamic-topology-tests.sh b/MoltenVK/Tests/run-pervertex-dynamic-topology-tests.sh new file mode 100755 index 000000000..421a93397 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-dynamic-topology-tests.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# Portable PerVertexKHR with a dynamic primitive topology, under Metal API and GPU validation, one case at a time. +# Each case is class:form[:fragment] (triangles or lines; direct, indexed, indirect, indexed-indirect or adjacency; pervertex, +# the default, ordinary, weights, weights-w or weights-w-noperspective); the list stops at the first unexpected result. +# Usage: run-pervertex-dynamic-topology-tests.sh ... +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +library=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +output=$(mkdir -p "$2" && cd "$2" && pwd) +expected=$3 +shaders=$output/shaders +program=$output/pervertex-dynamic-topology +mkdir -p "$shaders" "$output/runs" +exec > >(tee -a "$output/run.log") 2>&1 +if [[ ! -x $program ]]; then + sw_vers + echo "test_tree=$(git -C "$root" rev-parse HEAD) dirty_tests=$(git -C "$root" status --porcelain -- "$test_dir/PerVertexDynamicTopologyTests.mm" "$test_dir/pervertex-dynamic-topology.frag" "$test_dir/pervertex-dynamic-reference.vert" "$test_dir/pervertex-indirect.vert" "$test_dir/pervertex-indirect.frag" "$0" | wc -l | tr -d ' ')" + [[ -f $(dirname "$library")/PROVENANCE.txt ]] && cat "$(dirname "$library")/PROVENANCE.txt" + compile() { glslangValidator -V --target-env vulkan1.1 "${@:3}" "$test_dir/$1" -o "$shaders/$2" > /dev/null; spirv-val --target-env vulkan1.1 "$shaders/$2"; } + compile pervertex-indirect.vert indirect.vert.spv + compile pervertex-indirect.frag pervertex-portable.frag.spv -DPORTABLE + compile pervertex-dynamic-topology.frag ordinary.frag.spv + compile pervertex-dynamic-topology.frag weights.frag.spv -DWEIGHTS + compile pervertex-dynamic-topology.frag reference.frag.spv -DREFERENCE + compile pervertex-dynamic-reference.vert reference.vert.spv + compile pervertex-indirect.vert indirect-w.vert.spv -DVARIED_W + compile pervertex-dynamic-reference.vert reference-w.vert.spv -DVARIED_W + compile pervertex-dynamic-topology.frag weights-noperspective.frag.spv -DWEIGHTS -DNOPERSPECTIVE + compile pervertex-dynamic-topology.frag reference-noperspective.frag.spv -DREFERENCE -DNOPERSPECTIVE + xcrun clang++ -arch arm64 -std=c++17 -O2 -Wall -Wextra -Werror -Wno-missing-field-initializers -I"$root/External/Vulkan-Headers/include" \ + "$test_dir/PerVertexDynamicTopologyTests.mm" "$library" -Wl,-rpath,"$(dirname "$library")" -framework Metal -framework Foundation -o "$program" + shasum -a 256 "$library" "$shaders"/*.spv "$program" "$test_dir/PerVertexDynamicTopologyTests.mm" "$test_dir/pervertex-dynamic-topology.frag" "$test_dir/pervertex-dynamic-reference.vert" > "$output/INPUTS.sha256" +fi +for step in "${@:4}"; do + IFS=: read -r klass form fragment <<< "$step" + fragment=${fragment:-pervertex} + log=$output/runs/$klass-$form-$fragment.log + if [[ -e $log ]]; then echo "STOP: $log already exists"; exit 1; fi + rc=0 + (cd "$output/runs" && env -u DYLD_LIBRARY_PATH -u DYLD_INSERT_LIBRARIES MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 MVK_CONFIG_LOG_LEVEL=2 \ + gtimeout -k 2 60 "$program" "$shaders" "$klass" "$form" "$fragment") > "$log" 2>&1 || rc=$? + verdict=PASS + [[ $rc == "$expected" ]] || verdict=FAIL + grep -q 'Metal API Validation Enabled' "$log" || verdict=FAIL + grep -q 'Metal GPU Validation Enabled' "$log" || verdict=FAIL + if grep -qiE 'Shader Validation|GPU Address Sanitizer|validation error|Execution of the command buffer was aborted|execution failed' "$log"; then verdict=FAIL; fi + [[ $expected != 0 ]] || grep -q '^PERVERTEX_DYNAMIC_TOPOLOGY PASS' "$log" || verdict=FAIL + printf '%s:%s:%s exit=%s %s\n' "$klass" "$form" "$fragment" "$rc" "$verdict" + grep -E '^(dynamic topology|static vs|dynamic vs|reference|PERVERTEX_DYNAMIC|mode )|mvk-error' "$log" | sed 's/^/ /' | cut -c1-200 || true + if [[ $verdict != PASS ]]; then echo "STOP: inspect $log before any further GPU work"; exit 1; fi +done diff --git a/MoltenVK/Tests/run-pervertex-helper-preflight-tests.sh b/MoltenVK/Tests/run-pervertex-helper-preflight-tests.sh new file mode 100644 index 000000000..3d889b5c5 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-helper-preflight-tests.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-helper-preflight.XXXXXX")} +mkdir -p "$output" +# Compile the production preflight/cache method bodies against CPU-only doubles. +# Set MVK_TEST_REVISION=9f1cb26a to exercise the original implementation. +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import os, re, subprocess, sys +root, output = map(Path, sys.argv[1:]) +revision = os.environ.get('MVK_TEST_REVISION') +def read(path): + return subprocess.check_output(['git', 'show', f'{revision}:{path}'], cwd=root, text=True) if revision else (root / path).read_text() +commands = 'MoltenVK/MoltenVK/Commands/' +source = read(commands + 'MVKCommandBuffer.mm') +methods = re.findall(r'VkResult MVKCommandBuffer::reserve(?:Prefilled)?PerVertexScratch\([^\n]*\) \{\n.*?\n\}', source, re.S) +assert len(methods) in (2, 3), 'Update production-method extraction for changed signatures' +pool = read(commands + 'MVKCommandEncodingPool.mm') +getters = re.findall(r'id MVKCommandEncodingPool::get(?:ConvertUint8Indices|PerVertexRestart|PerVertexIndirect|PerVertexTessTopology)MTLComputePipelineState\([^\n]*\) \{\n.*?\n\}', pool, re.S) +assert len(getters) in (2, 3, 4) +macro = re.search(r'#define MVK_ENC_REZ_ACCESS.*?(?=\n\n)', pool, re.S).group() +(output / 'HelperPreflightMethods.inc').write_text(macro + '\n' + '\n'.join(getters + methods)) +scratch = read(commands + 'MVKPerVertexScratch.h') +(output / 'MVKPerVertexScratch.h').write_text(scratch) +(output / 'HelperPreflightVersion.inc').write_text('#define MVK_TEST_HAS_PREPARED_HELPER ' + str(int('indexPipeline' in scratch)) + '\n#define MVK_TEST_HAS_INDIRECT ' + str(int('indirectPipeline' in scratch)) + '\n#define MVK_TEST_HAS_TES_TOPOLOGY ' + str(int('tessTopologyPipeline' in scratch)) + '\n') +PY +xcrun clang++ -std=c++17 -fblocks -arch arm64 -Wall -Wextra -Werror -fsanitize="${MVK_TEST_SANITIZERS:-address,undefined}" -I"$output" -I"$root/MoltenVK/MoltenVK/Commands" -I"$root/MoltenVK/MoltenVK/GPUObjects" -I"$root/External/Vulkan-Headers/include" "$test_dir/PerVertexHelperPreflightTests.mm" -framework Foundation -framework Metal -o "$output/helper-preflight-tests" +"$output/helper-preflight-tests" diff --git a/MoltenVK/Tests/run-pervertex-memoryless-tests.sh b/MoltenVK/Tests/run-pervertex-memoryless-tests.sh new file mode 100644 index 000000000..04162485c --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-memoryless-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-memoryless.XXXXXX")} +mkdir -p "$output" +# Compile the production storage decision, not a copy of the policy. +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import sys +root, output = map(Path, sys.argv[1:]) +source = (root / 'MoltenVK/MoltenVK/GPUObjects/MVKImage.mm').read_text() +start = source.index('MTLStorageMode MVKImage::getMTLStorageMode()') +(output / 'ImageStorageMode.inc').write_text(source[start:source.index('\n}', start) + 2]) +PY +xcrun clang++ -std=c++17 -fblocks -I"$output" -I"$root/External/Vulkan-Headers/include" "$test_dir/PerVertexMemorylessTests.mm" -framework Foundation -framework Metal -o "$output/memoryless-tests" +"$output/memoryless-tests" "${2:-}" diff --git a/MoltenVK/Tests/run-pervertex-prefill-tests.sh b/MoltenVK/Tests/run-pervertex-prefill-tests.sh new file mode 100755 index 000000000..413f2d640 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-prefill-tests.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:?Output directory required} +mkdir -p "$output" +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import os, sys +root, output = map(Path, sys.argv[1:]) +source = Path(os.environ.get('MVK_MULTIVIEW_COMMAND_SOURCE', root / 'MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm')).read_text() +parts = [] +for signature in ('void MVKCommandBuffer::addCommand(', 'void MVKCommandEncoder::encodeCommandsImpl(', 'void MVKCommandBuffer::releaseRecordedCommands(', 'void MVKCommandBuffer::flushImmediateCmdEncoder('): + start = source.index(signature) + parts.append(source[start:source.index('\n}', start) + 2]) +(output / 'PrefillMethods.inc').write_text('\n'.join(parts)) +PY +xcrun clang++ -std=c++17 -Wall -Wextra -Werror -fsanitize=address,undefined -fno-sanitize-recover=all -I"$output" "$test_dir/PerVertexPrefillTests.cpp" -o "$output/prefill-tests" +"$output/prefill-tests" diff --git a/MoltenVK/Tests/run-pervertex-prepass-state-tests.sh b/MoltenVK/Tests/run-pervertex-prepass-state-tests.sh new file mode 100644 index 000000000..88bc8c8bc --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-prepass-state-tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# CPU only, no Metal device: PerVertex prepass state snapshot and restore, linked against the given x86_64 libMoltenVK. +# Usage: run-pervertex-prepass-state-tests.sh +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +mvk="$root/MoltenVK/MoltenVK" +dylib=$1 +output=$2 +mkdir -p "$output/lib" +ln -sfn "$dylib" "$output/lib/libMoltenVK.1.dylib" +xcrun clang++ -x objective-c++ -arch x86_64 -std=gnu++17 -mmacosx-version-min=11.0 -g -O0 -w -DMVK_CONFIG_LOG_LEVEL=3 -DMVK_EXCLUDE_SPIRV_TOOLS=1 -DMVK_FRAMEWORK_VERSION=1.4.3 -DMVK_USE_METAL_PRIVATE_API=0 -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross -I"$root/Common" -I"$root/External/cereal/include" -I"$root/External/SPIRV-Cross" -I"$root/External/Vulkan-Headers/include" -I"$root/MoltenVK/include" -I"$mvk/API" -I"$mvk/Commands" -I"$mvk/GPUObjects" -I"$mvk/Layers" -I"$mvk/OS" -I"$mvk/Utility" -I"$mvk/Vulkan" -I"$root/MoltenVKShaderConverter" "$test_dir/PerVertexPrepassStateTests.mm" -x none "$output/lib/libMoltenVK.1.dylib" -Wl,-rpath,"$output/lib" -framework Metal -framework Foundation -o "$output/prepass-state-tests" +arch -x86_64 "$output/prepass-state-tests" diff --git a/MoltenVK/Tests/run-pervertex-restart-tests.sh b/MoltenVK/Tests/run-pervertex-restart-tests.sh new file mode 100755 index 000000000..873e7a69e --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-restart-tests.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-restart.XXXXXX")} +mode=${2:-cpu} +mkdir -p "$output" +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import re, sys +root, output = map(Path, sys.argv[1:]) +source = (root / 'MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h').read_text() +start = source.index('uint perVertexRestartPrimitiveCount(') +end = source.index('\n}\n', source.index('kernel void perVertexRestart(', start)) + 3 +kernel = source[start:end] +host = re.sub(r'\[\[[^]]+\]\]', '', kernel).replace('kernel ', '').replace('device ', '').replace('constant ', 'const ') +(output / 'PerVertexRestartKernel.inc').write_text(host) +(output / 'restart.metal').write_text('#include \nusing namespace metal;\n' + kernel) +factory = source[source.index('@R"('):].replace('@R"(', 'R"(', 1) +(output / 'factory-source.cpp').write_text('#include \nint main() { const char* source = ' + factory + '\nstd::fputs(source, stdout); }\n') +PY +xcrun clang++ -std=c++17 -I"$root/MoltenVK/MoltenVK/Utility" "$output/factory-source.cpp" -o "$output/factory-source" +"$output/factory-source" > "$output/factory.metal" +xcrun metal -x metal -std=macos-metal2.4 -fsyntax-only -fno-modules "$output/restart.metal" +xcrun metal -x metal -std=macos-metal2.4 -fsyntax-only -fno-modules "$output/factory.metal" +if [[ "$mode" == gpu ]]; then + xcrun clang++ -x objective-c++ -arch arm64 -std=c++17 -O1 -g -fobjc-arc -DMVK_RESTART_GPU_TESTS -Wall -Wextra -I"$root/External/Vulkan-Headers/include" -I"$root/MoltenVK/MoltenVK/Commands" "$test_dir/PerVertexRestartTests.cpp" -framework Foundation -framework Metal -o "$output/restart-gpu-tests" + MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 "$output/restart-gpu-tests" "$output/factory.metal" + exit +fi +[[ "$mode" == cpu ]] || { echo "Usage: $0 [output] [cpu|gpu]" >&2; exit 1; } +xcrun clang++ -arch arm64 -std=c++17 -O1 -g -fsanitize=address,undefined -Wall -Wextra -I"$output" -I"$root/External/Vulkan-Headers/include" -I"$root/MoltenVK/MoltenVK/Commands" "$test_dir/PerVertexRestartTests.cpp" -o "$output/restart-tests" +"$output/restart-tests" diff --git a/MoltenVK/Tests/run-pervertex-scratch-pool-tests.sh b/MoltenVK/Tests/run-pervertex-scratch-pool-tests.sh new file mode 100644 index 000000000..e8b057483 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-scratch-pool-tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# CPU only, no Metal device: PerVertex scratch pool. Recompiles the test from source on every run. +# Usage: run-pervertex-scratch-pool-tests.sh [MoltenVK source root whose headers are tested] +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +output=$1 +root=${2:-$(cd "$test_dir/../.." && pwd)} +mkdir -p "$output" +rm -f "$output/scratch-pool-tests" +xcrun clang++ -std=c++17 -fblocks -Wall -Wextra -Werror -I"$root/MoltenVK/MoltenVK/Commands" -I"$root/MoltenVK/MoltenVK/GPUObjects" -I"$root/External/Vulkan-Headers/include" "$test_dir/PerVertexScratchPoolTests.mm" -framework Foundation -framework Metal -o "$output/scratch-pool-tests" +"$output/scratch-pool-tests" diff --git a/MoltenVK/Tests/run-pervertex-scratch-tests.sh b/MoltenVK/Tests/run-pervertex-scratch-tests.sh new file mode 100644 index 000000000..f81d6af54 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-scratch-tests.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-scratch.XXXXXX")} +architecture=${2:-$(uname -m)} +mkdir -p "$output" +xcrun clang++ -std=c++17 -fblocks -arch "$architecture" -Wall -Wextra -Werror -I"$root/MoltenVK/MoltenVK/Commands" -I"$root/MoltenVK/MoltenVK/GPUObjects" -I"$root/External/Vulkan-Headers/include" "$test_dir/PerVertexScratchTests.mm" -framework Foundation -framework Metal -o "$output/scratch-tests-$architecture" +if [[ "$architecture" == "$(uname -m)" ]]; then + "$output/scratch-tests-$architecture" +else + echo "COMPILED: scratch-tests-$architecture (execution skipped on $(uname -m) host)" +fi diff --git a/MoltenVK/Tests/run-pervertex-shared-replay-tests.sh b/MoltenVK/Tests/run-pervertex-shared-replay-tests.sh new file mode 100644 index 000000000..d5855f998 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-shared-replay-tests.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# CPU only: shared single-instance replay tables against per-draw population (ASan/UBSan). +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d /private/tmp/mvk-shared-replay.XXXXXX)} +mkdir -p "$output" +xcrun clang++ -arch arm64 -std=c++17 -O1 -g -fsanitize=address,undefined -Wall -Wextra -I"$root/External/Vulkan-Headers/include" -I"$root/MoltenVK/MoltenVK/Commands" "$test_dir/PerVertexSharedReplayTests.cpp" -o "$output/shared-replay-tests" +"$output/shared-replay-tests" diff --git a/MoltenVK/Tests/run-pervertex-split-tests.sh b/MoltenVK/Tests/run-pervertex-split-tests.sh new file mode 100755 index 000000000..3c77f30ac --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-split-tests.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +# Attachment content across the capture and replay of a direct non-indexed portable PerVertexKHR draw +# (PerVertexAttachmentSplitTests.mm), under Metal API and shader validation, one run at a time under gtimeout. +# Expected outcomes by library: +# - reference: capture and replay in one render pass with a memory barrier (before a564af04): every variant renders; +# - before: a render pass boundary (a564af04) loses imported memoryless attachments (wrong image or Metal assertion); +# - after: memoryless attachments are refused before submission; ordinary MSAA still renders. +# The indirect-count variants lower the Metal buffer limit to 1 GiB (test builds only): a legal DrawIndirectCount with +# a 1 GiB argument buffer is refused before the snapshot fix and renders Count 0 or 1 after it. +# Usage: run-pervertex-split-tests.sh [variants...] +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +library=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +output=$(mkdir -p "$2" && cd "$2" && pwd) +side=${3:?reference, before or after} +# The runner runs again as a child whose output tee records, so that the log is complete when this parent hashes it. +if [[ -z ${PERVERTEX_RUNNER_CHILD:-} ]]; then + rc=0 + PERVERTEX_RUNNER_CHILD=1 bash "$0" "$@" 2>&1 | tee "$output/split.log" || rc=$? + (cd "$output" && shasum -a 256 results.csv split.log runs/*.log > EVIDENCE.sha256) + exit "$rc" +fi +shift 3 +variants=("$@") +[[ ${#variants[@]} -gt 0 ]] || variants=(memoryless-depth memoryless-stencil msaa msaa-memoryless indirect-count-0 indirect-count-1) +mkdir -p "$output/bin/lib" "$output/runs" "$output/shaders" +ln -sf "$library" "$output/bin/lib/libMoltenVK.1.dylib" +for stage in vert frag; do + glslangValidator -V --target-env vulkan1.1 "$test_dir/pervertex-split.$stage" -o "$output/shaders/split.$stage.spv" > /dev/null + spirv-val --target-env vulkan1.1 "$output/shaders/split.$stage.spv" +done +# The sources are copied into the output and hashed there: the evidence stays checkable after they change. +mkdir -p "$output/source" && cp "$test_dir/PerVertexAttachmentSplitTests.mm" "$output/source/" +xcrun clang++ -arch arm64 -std=c++17 -O2 -Wall -Wextra -Werror -Wno-missing-field-initializers -I"$root/External/Vulkan-Headers/include" \ + "$test_dir/PerVertexAttachmentSplitTests.mm" "$library" -Wl,-rpath,@executable_path/lib -framework Metal -framework Foundation -o "$output/bin/pervertex-split" +shasum -a 256 "$library" "$output/bin/pervertex-split" "$output"/shaders/*.spv "$output/source/PerVertexAttachmentSplitTests.mm" > "$output/INPUTS.sha256" +echo 'variant,expected,actual,verdict' > "$output/results.csv" +failures=0 +for variant in "${variants[@]}"; do + case $side:$variant in + reference:indirect-count-*) expected=3 ;; # no Metal buffer limit hook in that build + before:indirect-count-*) expected=4 ;; # the argument snapshot, sized by maxDrawCount, exceeds the limit + after:indirect-count-*) expected=0 ;; + reference:*|*:msaa) expected=0 ;; + before:msaa-memoryless) expected='0|1|134|139' ;; # observed to render on an M4, which Metal does not guarantee + before:*) expected='1|134|139' ;; + after:*) expected=4 ;; + esac + log=$output/runs/$variant.log rc=0 + env -u DYLD_LIBRARY_PATH -u DYLD_INSERT_LIBRARIES MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 MVK_CONFIG_LOG_LEVEL=2 \ + gtimeout -k 2 40 "$output/bin/pervertex-split" "$output/shaders" "$variant" > "$log" 2>&1 || rc=$? + verdict=PASS + [[ "|$expected|" == *"|$rc|"* ]] || verdict=FAIL + grep -q 'Metal API Validation Enabled' "$log" || verdict=FAIL + [[ $verdict == PASS ]] || failures=$((failures + 1)) + echo "$variant,$expected,$rc,$verdict" >> "$output/results.csv" + printf '%-20s expected=%-8s exit=%-4s %s | %s\n' "$variant" "$expected" "$rc" "$verdict" "$(grep -E '^SPLIT_ATTACHMENT|failed assertion|^submit=' "$log" | tr '\n' ' ' | cut -c1-160)" +done +echo "SPLIT ATTACHMENTS $side: $failures unexpected" +exit $(( failures ? 1 : 0 )) diff --git a/MoltenVK/Tests/run-pervertex-topology-tests.sh b/MoltenVK/Tests/run-pervertex-topology-tests.sh new file mode 100644 index 000000000..d3447fae4 --- /dev/null +++ b/MoltenVK/Tests/run-pervertex-topology-tests.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +output=${1:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-topology.XXXXXX")} +mkdir -p "$output" +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import re, sys +root, output = map(Path, sys.argv[1:]) +source = (root / 'MoltenVK/MoltenVK/Commands/MVKCommandPipelineStateFactoryShaderSource.h').read_text() +kernel, = re.findall(r'kernel void convertUint8IndicesRaw\([^}]+\}', source) +# Execute the actual production kernel body on CPU; only remove Metal declarations/attributes. +host = re.sub(r'\[\[[^]]+\]\]', '', kernel).replace('kernel ', '').replace('device ', '').replace('uint pos', 'uint32_t pos') +(output / 'PerVertexUint8Kernel.inc').write_text(host) +(output / 'uint8-raw.metal').write_text('#include \nusing namespace metal;\n' + kernel) +PY +xcrun metal -x metal -std=macos-metal2.4 -fsyntax-only -fno-modules "$output/uint8-raw.metal" +includes=(-I"$output" -I"$root/Common" -I"$root/MoltenVK/include" -I"$root/External/Vulkan-Headers/include") +for directory in API Commands GPUObjects Layers OS Utility Vulkan; do + includes+=(-I"$root/MoltenVK/MoltenVK/$directory") +done +xcrun clang++ -arch arm64 -std=c++17 -fsanitize=address,undefined -Wall -Wextra -Wno-unused-parameter -Wno-deprecated-declarations "${includes[@]}" "$test_dir/PerVertexTopologyTests.mm" -framework Foundation -framework Metal -o "$output/topology-tests" +"$output/topology-tests" diff --git a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVReflection.h b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVReflection.h index 6e58d48c0..e4b726b21 100644 --- a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVReflection.h +++ b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVReflection.h @@ -22,8 +22,10 @@ #include #include #include +#include #include #include +#include #include @@ -51,6 +53,9 @@ namespace mvk { /** The number of control points output by the tessellation control shader. */ uint32_t numControlPoints = 0; + + /** Whether both shaders can exchange float32 tessellation levels (SPIRV-Cross tessellation_factors_float32). */ + bool float32TessLevels = false; }; #pragma mark - @@ -88,6 +93,9 @@ namespace mvk { /** Whether this variable is actually used (read or written) by the shader. */ bool isUsed; + + /** Whether this fragment input is read per vertex, including leaves of a PerVertexKHR block. */ + bool perVertex = false; }; typedef SPIRVShaderInterfaceVariable SPIRVShaderOutput; @@ -95,6 +103,92 @@ namespace mvk { #pragma mark - #pragma mark Functions + /** Returns whether a type has a PerVertexKHR-decorated member, including nested structs and arrays. */ + static inline bool hasPerVertexInputMember(const SPIRV_CROSS_NAMESPACE::CompilerReflection& reflect, const SPIRV_CROSS_NAMESPACE::SPIRType& type) { + for (uint32_t member = 0; member < type.member_types.size(); member++) { + if (reflect.has_member_decoration(type.self, member, spv::DecorationPerVertexKHR) || hasPerVertexInputMember(reflect, reflect.get_type(type.member_types[member]))) { return true; } + } + return false; + } + + /** + * Reports PerVertexKHR decorations on statically active fragment input variables or their members. + * Activity is per interface variable (including whole blocks), as in SPIRV-Cross reflection. + * An empty entryName selects the module's default entry point, which must be a fragment entry point. + * Returns success separately from usesPerVertexInput; on failure the latter is false and errorLog is set. + * Like the other reflection helpers, parser errors are recoverable when SPIRV-Cross exceptions are enabled. + */ + template + static inline bool getFragmentShaderUsesPerVertexInput(const Vs& spirv, const std::string& entryName, bool& usesPerVertexInput, std::string& errorLog) { + usesPerVertexInput = false; + errorLog.clear(); +#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS + try { +#endif + SPIRV_CROSS_NAMESPACE::Parser parser(spirv); + parser.parse(); + SPIRV_CROSS_NAMESPACE::CompilerReflection reflect(parser.get_parsed_ir()); + if (!entryName.empty()) { + bool found = false; + for (const auto& entry : reflect.get_entry_points_and_stages()) { + if (entry.name == entryName && entry.execution_model == spv::ExecutionModelFragment) { found = true; break; } + } + if (!found) { + errorLog = "Fragment entry point not found: " + entryName; + return false; + } + reflect.set_entry_point(entryName, spv::ExecutionModelFragment); + } + if (reflect.get_execution_model() != spv::ExecutionModelFragment) { + errorLog = "PerVertexKHR input reflection requires a fragment entry point."; + return false; + } + for (auto varID : reflect.get_active_interface_variables()) { + if (reflect.get_storage_class(varID) != spv::StorageClassInput) { continue; } + if (reflect.has_decoration(varID, spv::DecorationPerVertexKHR) || hasPerVertexInputMember(reflect, reflect.get_type_from_variable(varID))) { + usesPerVertexInput = true; + break; + } + } + return true; +#ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS + } catch (SPIRV_CROSS_NAMESPACE::CompilerError& ex) { + errorLog = ex.what(); + return false; + } +#endif + } + + /** + * Returns the Locations of the fragment PerVertexKHR inputs that the corner copies of a mesh pipeline support: + * arrays of one to three 16- or 32-bit scalars or vectors below Location 32, outside blocks. + * Returns zero if any other form is read. Parser errors throw, as in the other mesh admission checks. + */ + template + static inline uint32_t getMeshPerVertexCornerLocations(const Vs& spirv, const std::string& entryName) { + SPIRV_CROSS_NAMESPACE::CompilerReflection reflect(spirv); + reflect.set_entry_point(entryName, spv::ExecutionModelFragment); + uint32_t locations = 0; + for (auto varID : reflect.get_active_interface_variables()) { + if (reflect.get_storage_class(varID) != spv::StorageClassInput) { continue; } + const auto& type = reflect.get_type(reflect.get_type_from_variable(varID).parent_type); + if (!reflect.has_decoration(varID, spv::DecorationPerVertexKHR)) { + if (hasPerVertexInputMember(reflect, type)) { return 0; } + continue; + } + if (type.array.size() != 1 || !type.array_size_literal[0] || type.array[0] < 1 || type.array[0] > 3) { return 0; } + const auto& element = reflect.get_type(type.parent_type); + bool scalarOrVector = element.array.empty() && element.columns == 1 && + (element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Float || element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Half || + element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Int || element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::UInt || + element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Short || element.basetype == SPIRV_CROSS_NAMESPACE::SPIRType::UShort); + uint32_t location = reflect.get_decoration(varID, spv::DecorationLocation); + if (!scalarOrVector || reflect.has_decoration(varID, spv::DecorationBuiltIn) || !reflect.has_decoration(varID, spv::DecorationLocation) || location >= 32) { return 0; } + locations |= 1u << location; + } + return locations; + } + /** * Given a tessellation control shader and a tessellation evaluation shader, * both in SPIR-V format, returns tessellation reflection data. @@ -181,6 +275,16 @@ namespace mvk { return false; } + // Same execution modes as the MSL conversion, which sets the patch kind and control points on both stages. + SPIRV_CROSS_NAMESPACE::CompilerMSL tescMSL(tesc), teseMSL(tese); + if (!tescEntryName.empty()) { tescMSL.set_entry_point(tescEntryName, spv::ExecutionModelTessellationControl); } + if (!teseEntryName.empty()) { teseMSL.set_entry_point(teseEntryName, spv::ExecutionModelTessellationEvaluation); } + for (auto* compiler : {&tescMSL, &teseMSL}) { + compiler->set_execution_mode(reflectData.patchKind); + compiler->set_execution_mode(spv::ExecutionModeOutputVertices, reflectData.numControlPoints); + } + reflectData.float32TessLevels = tescMSL.get_tessellation_factors_float32_incompatibility().empty() && teseMSL.get_tessellation_factors_float32_incompatibility().empty(); + return true; #ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS @@ -235,12 +339,12 @@ namespace mvk { template static inline uint32_t getShaderInterfaceStructMembers(const SPIRV_CROSS_NAMESPACE::CompilerReflection& reflect, - Vi& vars, SPIRVShaderInterfaceVariable* pParentFirstMember, + Vi& vars, size_t parentFirstMember, const SPIRV_CROSS_NAMESPACE::SPIRType* structType, spv::StorageClass storage, - bool patch, uint32_t loc) { + bool patch, uint32_t loc, bool perVertex = false) { bool isUsed = true; auto biType = spv::BuiltInMax; - SPIRVShaderInterfaceVariable* pFirstMember = nullptr; + const size_t firstMember = vars.size(); size_t mbrCnt = structType->member_types.size(); for (uint32_t mbrIdx = 0; mbrIdx < mbrCnt; mbrIdx++) { // Each member may have a location decoration. If not, each member @@ -256,37 +360,37 @@ namespace mvk { isUsed = reflect.has_active_builtin(biType, storage); } const SPIRV_CROSS_NAMESPACE::SPIRType* type = &reflect.get_type(structType->member_types[mbrIdx]); - uint32_t elemCnt = (type->array.empty() ? 1 : type->array[0]) * type->columns; + bool memberPerVertex = storage == spv::StorageClassInput && reflect.get_execution_model() == spv::ExecutionModelFragment && reflect.has_member_decoration(structType->self, mbrIdx, spv::DecorationPerVertexKHR); + if (memberPerVertex && !type->array.empty()) { type = &reflect.get_type(type->parent_type); } + uint32_t elemCnt = type->columns; + for (uint32_t count : type->array) { elemCnt *= count; } for (uint32_t elemIdx = 0; elemIdx < elemCnt; elemIdx++) { if (type->basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Struct) - loc = getShaderInterfaceStructMembers(reflect, vars, pFirstMember, type, storage, patch, loc); + loc = getShaderInterfaceStructMembers(reflect, vars, firstMember, type, storage, patch, loc, perVertex || memberPerVertex); else { // The alignment of a structure is the same as the largest member of the structure. // Consequently, the first flattened member of a structure should align with structure itself. - vars.push_back({type->basetype, type->vecsize, loc, cmp, 0, biType, patch, isUsed}); - auto& currOutput = vars.back(); - if ( !pFirstMember ) { pFirstMember = &currOutput; } - pFirstMember->firstStructMemberAlignment = std::max(pFirstMember->firstStructMemberAlignment, getShaderOutputSize(currOutput)); + vars.push_back({type->basetype, type->vecsize, loc, cmp, 0, biType, patch, isUsed, perVertex || memberPerVertex}); + vars[firstMember].firstStructMemberAlignment = std::max(vars[firstMember].firstStructMemberAlignment, getShaderOutputSize(vars.back())); loc = addSat(loc, 1); } } } // Set the parent's first member alignment to the largest alignment found so far. - if ( !pParentFirstMember ) { - pParentFirstMember = pFirstMember; - } else if (pParentFirstMember && pFirstMember) { - pParentFirstMember->firstStructMemberAlignment = std::max(pParentFirstMember->firstStructMemberAlignment, pFirstMember->firstStructMemberAlignment); + // Indices remain valid when appending leaves reallocates the container. + if (parentFirstMember < vars.size() && firstMember < vars.size()) { + vars[parentFirstMember].firstStructMemberAlignment = std::max(vars[parentFirstMember].firstStructMemberAlignment, vars[firstMember].firstStructMemberAlignment); } return loc; } template static inline uint32_t getShaderOutputStructMembers(const SPIRV_CROSS_NAMESPACE::CompilerReflection& reflect, - Vo& outputs, SPIRVShaderOutput* pParentFirstMember, + Vo& outputs, size_t parentFirstMember, const SPIRV_CROSS_NAMESPACE::SPIRType* structType, spv::StorageClass storage, bool patch, uint32_t loc) { - return getShaderInterfaceStructMembers(reflect, outputs, pParentFirstMember, structType, storage, patch, loc); + return getShaderInterfaceStructMembers(reflect, outputs, parentFirstMember, structType, storage, patch, loc); } /** Given a shader in SPIR-V format, returns interface reflection data. */ @@ -298,6 +402,37 @@ namespace mvk { #endif SPIRV_CROSS_NAMESPACE::Parser parser(spirv); parser.parse(); + if (model == spv::ExecutionModelFragment && storage == spv::StorageClassInput) { + // Match the MSL backend's builtin analysis: access chains must see through + // copied/aliased Input pointers before deciding which block members are active. + // Only the parser's private IR is changed; entry-point reachability is retained. + auto& ir = parser.get_parsed_ir(); + std::unordered_map pointerAliases; + ir.for_each_typed_id([&](uint32_t, SPIRV_CROSS_NAMESPACE::SPIRBlock& block) { + for (auto& op : block.ops) { + bool emptyAccessChain = (op.op == spv::OpAccessChain || op.op == spv::OpInBoundsAccessChain) && op.length == 3; + if ((op.op != spv::OpCopyObject && !emptyAccessChain) || op.length < 3) { continue; } + const auto* args = ir.spirv.data() + op.offset; + const auto& type = SPIRV_CROSS_NAMESPACE::variant_get(ir.ids[args[0]]); + if (type.pointer && type.storage == spv::StorageClassInput) { + pointerAliases[args[1]] = args[2]; + // Empty access chains are identity copies, but ActiveBuiltinHandler + // stops traversal on their three operands. A copy keeps it walking. + op.op = spv::OpCopyObject; + } + } + }); + ir.for_each_typed_id([&](uint32_t, const SPIRV_CROSS_NAMESPACE::SPIRBlock& block) { + for (const auto& op : block.ops) { + if ((op.op != spv::OpAccessChain && op.op != spv::OpInBoundsAccessChain) || op.length < 3) { continue; } + auto* args = ir.spirv.data() + op.offset; + for (size_t count = 0; pointerAliases.count(args[2]); ++count) { + if (count == pointerAliases.size()) { using namespace SPIRV_CROSS_NAMESPACE; SPIRV_CROSS_THROW("Cyclic input pointer aliases."); } + args[2] = pointerAliases.at(args[2]); + } + } + }); + } SPIRV_CROSS_NAMESPACE::CompilerReflection reflect(parser.get_parsed_ir()); if (!entryName.empty()) { reflect.set_entry_point(entryName, model); @@ -323,6 +458,9 @@ namespace mvk { if (reflect.has_decoration(varID, spv::DecorationBuiltIn)) { biType = (spv::BuiltIn)reflect.get_decoration(varID, spv::DecorationBuiltIn); isUsed = reflect.has_active_builtin(biType, storage); + // The active interface includes whole-variable InterpolateAt* operands, + // which update_active_builtins() does not count as loads. + if (model == spv::ExecutionModelFragment && storage == spv::StorageClassInput && (biType == spv::BuiltInBaryCoordKHR || biType == spv::BuiltInBaryCoordNoPerspKHR)) { isUsed = true; } } uint32_t loc = -1; uint32_t cmp = 0; @@ -332,20 +470,28 @@ namespace mvk { if (reflect.has_decoration(varID, spv::DecorationComponent)) { cmp = reflect.get_decoration(varID, spv::DecorationComponent); } + // Mesh outputs are arrays over the emitted vertices or primitives, and the primitive index builtins + // describe connectivity, not varyings. + bool meshOutput = model == spv::ExecutionModelMeshEXT && storage == spv::StorageClassOutput; + if (meshOutput && (biType == spv::BuiltInPrimitivePointIndicesEXT || biType == spv::BuiltInPrimitiveLineIndicesEXT || biType == spv::BuiltInPrimitiveTriangleIndicesEXT)) { continue; } // For tessellation shaders, peel away the initial array type. SPIRV-Cross adds the array back automatically. // Only some builtins will be arrayed here. - if ((model == spv::ExecutionModelTessellationControl || (model == spv::ExecutionModelTessellationEvaluation && storage == spv::StorageClassInput)) && !patch && - (biType == spv::BuiltInMax || biType == spv::BuiltInPosition || biType == spv::BuiltInPointSize || - biType == spv::BuiltInClipDistance || biType == spv::BuiltInCullDistance)) + if (((model == spv::ExecutionModelTessellationControl || (model == spv::ExecutionModelTessellationEvaluation && storage == spv::StorageClassInput)) && !patch && + (biType == spv::BuiltInMax || biType == spv::BuiltInPosition || biType == spv::BuiltInPointSize || + biType == spv::BuiltInClipDistance || biType == spv::BuiltInCullDistance)) || + (meshOutput && !type->array.empty())) type = &reflect.get_type(type->parent_type); - uint32_t elemCnt = (type->array.empty() ? 1 : type->array[0]) * type->columns; + // PerVertexKHR's outer array selects a vertex, not additional varying locations. + bool perVertex = model == spv::ExecutionModelFragment && storage == spv::StorageClassInput && reflect.has_decoration(varID, spv::DecorationPerVertexKHR); + if (perVertex && !type->array.empty()) { type = &reflect.get_type(type->parent_type); } + uint32_t elemCnt = type->columns; + for (uint32_t count : type->array) { elemCnt *= count; } for (uint32_t i = 0; i < elemCnt; i++) { if (type->basetype == SPIRV_CROSS_NAMESPACE::SPIRType::Struct) { - SPIRVShaderInterfaceVariable* pFirstMember = nullptr; - loc = getShaderInterfaceStructMembers(reflect, vars, pFirstMember, type, storage, patch, loc); + loc = getShaderInterfaceStructMembers(reflect, vars, size_t(-1), type, storage, patch, loc, perVertex); } else { - vars.push_back({type->basetype, type->vecsize, loc, cmp, 0, biType, patch, isUsed}); + vars.push_back({type->basetype, type->vecsize, loc, cmp, 0, biType, patch, isUsed, perVertex}); loc = addSat(loc, 1); } } diff --git a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.cpp b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.cpp index 7a6272ca2..b989b5f37 100644 --- a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.cpp +++ b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.cpp @@ -22,6 +22,7 @@ #include "FileSupport.h" #include "SPIRVSupport.h" #include +#include using namespace mvk; using namespace std; @@ -131,6 +132,19 @@ MVK_PUBLIC_SYMBOL bool mvk::DescriptorBinding::matches(const mvk::DescriptorBind return true; } +MVK_PUBLIC_SYMBOL bool mvk::MSLPerVertexInputBuffer::matches(const mvk::MSLPerVertexInputBuffer& other) const { + if (enabled != other.enabled) { return false; } + if (!enabled) { return true; } + if (layout.stride != other.layout.stride) { return false; } + if (binding.vertex_buffer_index != other.binding.vertex_buffer_index || binding.primitive_index_buffer_index != other.binding.primitive_index_buffer_index || binding.primitive_index_location != other.binding.primitive_index_location || binding.primitive_id_buffer_index != other.binding.primitive_id_buffer_index) { return false; } + if (!std::equal(layout.builtins.begin(), layout.builtins.end(), other.layout.builtins.begin(), other.layout.builtins.end(), [](const auto& a, const auto& b) { + return a.builtin == b.builtin && a.array_index == b.array_index && a.component == b.component && a.byte_offset == b.byte_offset && a.scalar_type == b.scalar_type; + })) { return false; } + return std::equal(layout.components.begin(), layout.components.end(), other.layout.components.begin(), other.layout.components.end(), [](const auto& a, const auto& b) { + return a.location == b.location && a.component == b.component && a.byte_offset == b.byte_offset && a.scalar_type == b.scalar_type; + }); +} + MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::stageSupportsVertexAttributes() const { return (options.entryPointStage == ExecutionModelVertex || options.entryPointStage == ExecutionModelTessellationControl || @@ -191,6 +205,9 @@ MVK_PUBLIC_SYMBOL void SPIRVToMSLConversionConfiguration::markAllInterfaceVarsAn MVK_PUBLIC_SYMBOL bool SPIRVToMSLConversionConfiguration::matches(const SPIRVToMSLConversionConfiguration& other) const { if ( !options.matches(other.options) ) { return false; } + if (!perVertexInputBuffer.matches(other.perVertexInputBuffer)) { return false; } + if (!fragmentBarycentricInput.matches(other.fragmentBarycentricInput)) { return false; } + if (exportCapturedVertexLayout != other.exportCapturedVertexLayout) { return false; } for (const auto& si : shaderInputs) { if (si.outIsUsedByShader && !containsMatching(other.shaderInputs, si)) { return false; } @@ -281,6 +298,7 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfigur CompilerMSL* pMSLCompiler = nullptr; bool wasConverted = true; + conversionResult.resultInfo.capturedVertexLayout = {}; #ifndef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS try { @@ -305,6 +323,12 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfigur // Establish the MSL options for the compiler // This needs to be done in two steps...for CompilerMSL and its superclass. pMSLCompiler->set_msl_options(shaderConfig.options.mslOptions); + if (shaderConfig.fragmentBarycentricInput.enabled) { + pMSLCompiler->set_msl_fragment_barycentric_input(shaderConfig.fragmentBarycentricInput.binding); + } + if (shaderConfig.perVertexInputBuffer.enabled) { + pMSLCompiler->set_msl_per_vertex_input_buffer(shaderConfig.perVertexInputBuffer.layout, shaderConfig.perVertexInputBuffer.binding); + } auto scOpts = pMSLCompiler->get_common_options(); scOpts.vertex.flip_vert_y = shaderConfig.options.shouldFlipVertexY; @@ -347,6 +371,11 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfigur } } conversionResult.msl = pMSLCompiler->compile(); + if (shaderConfig.exportCapturedVertexLayout && pMSLCompiler->needs_output_buffer() && + (pMSLCompiler->get_execution_model() == ExecutionModelVertex || + (pMSLCompiler->get_execution_model() == ExecutionModelTessellationEvaluation && shaderConfig.options.mslOptions.tese_as_compute))) { + conversionResult.resultInfo.capturedVertexLayout = pMSLCompiler->get_msl_captured_vertex_layout(); + } if (shouldLogMSL) { logSource(conversionResult.resultLog, conversionResult.msl, "MSL", "Converted"); } @@ -354,7 +383,8 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfigur } catch (CompilerError& ex) { string errMsg("SPIR-V to MSL conversion error: "); errMsg += ex.what(); - logError(conversionResult.resultLog, errMsg.data()); + wasConverted = logError(conversionResult.resultLog, errMsg.data()); + conversionResult.msl.clear(); if (shouldLogMSL && pMSLCompiler) { auto partialMSL = pMSLCompiler->get_partial_source(); logSource(conversionResult.resultLog, partialMSL, "MSL", "Partially converted"); @@ -376,6 +406,7 @@ MVK_PUBLIC_SYMBOL bool SPIRVToMSLConverter::convert(SPIRVToMSLConversionConfigur conversionResult.resultInfo.needsViewRangeBuffer = pMSLCompiler && pMSLCompiler->needs_view_mask_buffer(); conversionResult.resultInfo.needsDrawId = pMSLCompiler && pMSLCompiler->has_active_builtin(spv::BuiltInDrawIndex, spv::StorageClassInput); conversionResult.resultInfo.needsDepthClipStateBuffer = pMSLCompiler && pMSLCompiler->needs_depth_clip_state_buffer(); + conversionResult.resultInfo.needsPerVertexInputBuffer = wasConverted && pMSLCompiler && pMSLCompiler->needs_per_vertex_input_buffer(); conversionResult.resultInfo.usesPhysicalStorageBufferAddressesCapability = usesPhysicalStorageBufferAddressesCapability(pMSLCompiler); populateSpecializationMacros(pMSLCompiler, conversionResult.resultInfo.specializationMacros); diff --git a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h index 6163b541b..2979d5f9d 100644 --- a/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h +++ b/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h @@ -25,6 +25,25 @@ #include #include +namespace SPIRV_CROSS_NAMESPACE { + + // Field-wise pipeline cache serialization; no dependency on a particular archive implementation. + template + void serialize(Archive& archive, MSLCapturedVertexComponent& component) { + archive(component.location, component.component, component.byte_offset, component.scalar_type); + } + + template + void serialize(Archive& archive, MSLCapturedVertexBuiltin& builtin) { + archive(builtin.builtin, builtin.array_index, builtin.component, builtin.byte_offset, builtin.scalar_type); + } + + template + void serialize(Archive& archive, MSLCapturedVertexLayout& layout) { + archive(layout.stride, layout.components, layout.builtins); + } + +} namespace mvk { @@ -140,6 +159,29 @@ namespace mvk { } DescriptorBinding; + /** Explicit captured producer ABI. Include this value in the pipeline cache archive. */ + struct MSLPerVertexInputBuffer { + bool enabled = false; + SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout layout; + SPIRV_CROSS_NAMESPACE::MSLPerVertexInputBinding binding; + + bool matches(const MSLPerVertexInputBuffer& other) const; + + template + void serialize(Archive& archive) { + archive(enabled, layout, binding.vertex_buffer_index, binding.primitive_index_buffer_index, binding.primitive_index_location, binding.primitive_id_buffer_index); + } + }; + + /** Explicit portable barycentric varying ABI; disabled bindings do not affect cache identity. */ + struct MSLFragmentBarycentricInput { + bool enabled = false; + SPIRV_CROSS_NAMESPACE::MSLFragmentBarycentricInputBinding binding; + bool matches(const MSLFragmentBarycentricInput& other) const { return enabled == other.enabled && (!enabled || (binding.perspective_location == other.binding.perspective_location && binding.no_perspective_location == other.binding.no_perspective_location)); } + template + void serialize(Archive& archive) { archive(enabled, binding.perspective_location, binding.no_perspective_location); } + }; + /** * Configuration passed to the SPIRVToMSLConverter. * @@ -153,6 +195,14 @@ namespace mvk { std::vector resourceBindings; std::vector discreteDescriptorSets; std::vector dynamicBufferDescriptors; + MSLPerVertexInputBuffer perVertexInputBuffer; + MSLFragmentBarycentricInput fragmentBarycentricInput; + bool exportCapturedVertexLayout = false; + + /** Opt in to portable PerVertexKHR inputs using the actual producer layout and reserved resources. */ + void setPerVertexInputBuffer(const SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout& layout, const SPIRV_CROSS_NAMESPACE::MSLPerVertexInputBinding& binding) { perVertexInputBuffer = {true, layout, binding}; } + + void setFragmentBarycentricInput(const SPIRV_CROSS_NAMESPACE::MSLFragmentBarycentricInputBinding& binding) { fragmentBarycentricInput = {true, binding}; } /** Returns whether the pipeline stage being converted supports vertex attributes. */ bool stageSupportsVertexAttributes() const; @@ -178,8 +228,9 @@ namespace mvk { /** Marks all interface variables and resources as being used by the shader. */ void markAllInterfaceVarsAndResourcesUsed(); - /** - * Returns whether this configuration matches the other configuration. It does if + /** + * Returns whether this configuration matches the other configuration. It does if + * the enabled captured producer ABI matches exactly, * the respective options match and any vertex attributes and resource bindings used * by this configuration can be found in the other configuration. Vertex attributes * and resource bindings that are in the other configuration but are not used by @@ -262,8 +313,11 @@ namespace mvk { bool needsViewRangeBuffer = false; bool needsDrawId = false; bool needsDepthClipStateBuffer = false; + bool needsPerVertexInputBuffer = false; bool usesPhysicalStorageBufferAddressesCapability = false; std::map specializationMacros; + /** Physical vertex capture ABI; zero stride when no vertex capture record was exported. */ + SPIRV_CROSS_NAMESPACE::MSLCapturedVertexLayout capturedVertexLayout; } SPIRVToMSLConversionResultInfo; diff --git a/MoltenVKShaderConverter/Tests/PerVertexInputTests.cpp b/MoltenVKShaderConverter/Tests/PerVertexInputTests.cpp new file mode 100644 index 000000000..672fb06fc --- /dev/null +++ b/MoltenVKShaderConverter/Tests/PerVertexInputTests.cpp @@ -0,0 +1,292 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "SPIRVToMSLConverter.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace mvk; +using namespace SPIRV_CROSS_NAMESPACE; + +static void check(bool value, const char* message) { + if (!value) { throw std::runtime_error(message); } +} + +static std::vector readSPIRV(const char* path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + check(bool(file), "Cannot open fixture"); + auto size = file.tellg(); + check(size > 0 && size % 4 == 0, "Invalid fixture size"); + std::vector words(size_t(size) / 4); + file.seekg(0); + file.read(reinterpret_cast(words.data()), size); + check(bool(file), "Cannot read fixture"); + return words; +} + +static SPIRVToMSLConversionConfiguration configuration() { + SPIRVToMSLConversionConfiguration config; + config.options.entryPointName = "main"; + config.options.entryPointStage = spv::ExecutionModelFragment; + config.options.mslOptions.set_msl_version(2, 4); + MSLCapturedVertexLayout layout; + layout.stride = 96; + auto add = [&](uint32_t location, uint32_t component, uint32_t offset, SPIRType::BaseType type) { layout.components.push_back({location, component, offset, type}); }; + add(0, 0, 16, SPIRType::Float); + add(0, 1, 24, SPIRType::Float); + add(0, 2, 20, SPIRType::Float); + add(1, 2, 0, SPIRType::Float); + add(1, 3, 4, SPIRType::Float); + for (uint32_t c = 0; c < 4; c++) { add(2, c, 32 + 4 * c, SPIRType::Int); } + for (uint32_t c = 0; c < 3; c++) { add(3, c, 48 + 4 * c, SPIRType::UInt); } + for (uint32_t c = 0; c < 2; c++) { add(4, c, 8 + 2 * c, SPIRType::Half); } + for (uint32_t c = 0; c < 3; c++) { add(5, c, 64 + 2 * c, SPIRType::Short); } + for (uint32_t c = 0; c < 4; c++) { add(6, c, 72 + 2 * c, SPIRType::UShort); } + add(7, 3, 80, SPIRType::Float); + config.setPerVertexInputBuffer(layout, {28, 29, 15}); + mvk::MSLResourceBinding resource; + resource.resourceBinding.stage = spv::ExecutionModelFragment; + resource.resourceBinding.desc_set = 0; + resource.resourceBinding.binding = 0; + resource.resourceBinding.msl_buffer = 5; + config.resourceBindings.push_back(resource); + return config; +} + +static std::string archive(MSLPerVertexInputBuffer input) { + std::ostringstream bytes; + cereal::BinaryOutputArchive output(bytes); + output(input); + return bytes.str(); +} + +static void checkCaptureLayout(SPIRVToMSLConverter& converter, SPIRVToMSLConversionResult& result, const std::string& directory) { + auto equal = [](const MSLCapturedVertexLayout& a, const MSLCapturedVertexLayout& b) { + return MSLPerVertexInputBuffer{true, a, {}}.matches(MSLPerVertexInputBuffer{true, b, {}}); + }; + for (bool complex : {false, true}) { + for (bool alternate : {false, true}) { + auto words = readSPIRV((directory + (complex ? "/capture-complex.spv" : "/capture-simple.spv")).c_str()); + SPIRVToMSLConversionConfiguration config; + config.options.entryPointName = "main"; + config.options.entryPointStage = spv::ExecutionModelVertex; + config.options.mslOptions.set_msl_version(2, 4); + config.options.mslOptions.capture_output_to_buffer = true; + config.exportCapturedVertexLayout = true; + config.options.mslOptions.vertex_for_tessellation = !complex && alternate; + config.options.mslOptions.force_native_arrays = complex && alternate; + if (complex) { + mvk::MSLShaderInterfaceVariable output; + output.shaderVar.builtin = spv::BuiltInPosition; + output.shaderVar.location = 1; + config.shaderOutputs.push_back(output); + } + CompilerMSL reference(words); + reference.set_msl_options(config.options.mslOptions); + for (const auto& output : config.shaderOutputs) { reference.add_msl_shader_output(output.shaderVar); } + reference.compile(); + auto expected = reference.get_msl_captured_vertex_layout(); + converter.setSPIRV(words.data(), words.size()); + check(converter.convert(config, result), "Capture conversion failed"); + auto layout = result.resultInfo.capturedVertexLayout; + check(result.resultInfo.needsOutputBuffer && equal(layout, expected), "Exported layout differs from compiler reflection"); + check(layout.stride == (complex ? 112u : 32u) && layout.components.size() == (complex ? 13u : 3u), "Wrong physical ABI or builtin/padding lane leaked"); + check(layout.builtins.size() == (complex ? 9u : 4u), "Missing captured builtin scalars"); + std::stringstream bytes; + { cereal::BinaryOutputArchive output(bytes); output(layout); } + MSLCapturedVertexLayout restored; + { cereal::BinaryInputArchive input(bytes); input(restored); } + check(equal(restored, layout), "Captured result layout lost fields in round-trip"); + for (unsigned field = 0; field < 12; field++) { + auto changed = restored; + switch (field) { + case 0: changed.stride += 4; break; + case 1: changed.components[0].location++; break; + case 2: changed.components[0].component++; break; + case 3: changed.components[0].byte_offset += 4; break; + case 4: changed.components[0].scalar_type = SPIRType::Int; break; + case 5: changed.components.pop_back(); break; + case 6: changed.builtins[0].builtin = spv::BuiltInPointSize; break; + case 7: changed.builtins[0].array_index++; break; + case 8: changed.builtins[0].component++; break; + case 9: changed.builtins[0].byte_offset += 4; break; + case 10: changed.builtins[0].scalar_type = SPIRType::Int; break; + case 11: changed.builtins.pop_back(); break; + } + check(!equal(changed, layout), "Layout equality ignored a physical ABI field"); + check(archive(MSLPerVertexInputBuffer{true, changed, {}}) != archive(MSLPerVertexInputBuffer{true, layout, {}}), "Layout serialization ignored a physical ABI field"); + } + std::ofstream output(directory + "/capture-" + (complex ? "complex" : "simple") + (alternate ? "-alternate" : "") + ".metal"); + output << result.msl << "\nstatic_assert(sizeof(main0_out) == " << layout.stride << ", \"captured stride\");\n"; + check(bool(output), "Cannot write capture MSL"); + if (!complex) { + auto consumer = readSPIRV((directory + "/capture-consumer.spv").c_str()); + converter.setSPIRV(consumer.data(), consumer.size()); + SPIRVToMSLConversionConfiguration fragment; + fragment.options.entryPointName = "main"; + fragment.options.entryPointStage = spv::ExecutionModelFragment; + fragment.options.mslOptions.set_msl_version(2, 4); + fragment.setPerVertexInputBuffer(restored, {28, 29, 15}); + check(converter.convert(fragment, result), "Restored producer layout rejected by fragment consumer"); + check(result.resultInfo.needsPerVertexInputBuffer, "Fragment did not use restored producer layout"); + check(equal(result.resultInfo.capturedVertexLayout, {}), "Fragment retained stale producer layout"); + std::ofstream fragmentOutput(directory + (alternate ? "/capture-consumer-alternate.metal" : "/capture-consumer.metal")); + fragmentOutput << result.msl; + check(bool(fragmentOutput), "Cannot write consumer MSL"); + } + converter.setSPIRV(words.data(), words.size()); + config.options.mslOptions.capture_output_to_buffer = false; + config.exportCapturedVertexLayout = false; + config.options.mslOptions.vertex_for_tessellation = false; + result.resultInfo.capturedVertexLayout = restored; + check(converter.convert(config, result), "Ordinary vertex conversion failed"); + check(equal(result.resultInfo.capturedVertexLayout, {}), "Ordinary conversion retained stale producer layout"); + } + } + // Ordinary tessellation also captures vertex output, but must not request the portable physical ABI. + { + auto words = readSPIRV((directory + "/capture-complex.spv").c_str()); + converter.setSPIRV(words.data(), words.size()); + SPIRVToMSLConversionConfiguration ordinary; + ordinary.options.entryPointName = "main"; + ordinary.options.entryPointStage = spv::ExecutionModelVertex; + ordinary.options.mslOptions.set_msl_version(2, 4); + ordinary.options.mslOptions.capture_output_to_buffer = true; + ordinary.options.mslOptions.vertex_for_tessellation = true; + check(converter.convert(ordinary, result), "Ordinary tessellation capture rejected by portable layout export"); + check(result.resultInfo.capturedVertexLayout.stride == 0, "Ordinary tessellation unexpectedly exported a portable layout"); + } + // TES compute capture exports the same physical layout contract as vertex capture. + for (const char* fixture : {"capture-tese", "capture-tese-block"}) { + auto words = readSPIRV((directory + "/" + fixture + ".spv").c_str()); + SPIRVToMSLConversionConfiguration tess; + tess.options.entryPointName = "main"; + tess.options.entryPointStage = spv::ExecutionModelTessellationEvaluation; + tess.options.numTessControlPoints = 3; + tess.options.shouldFlipVertexY = false; + tess.options.mslOptions.set_msl_version(2, 4); + tess.options.mslOptions.capture_output_to_buffer = true; + tess.options.mslOptions.raw_buffer_tese_input = true; + tess.options.mslOptions.tese_as_compute = true; + tess.exportCapturedVertexLayout = true; + CompilerMSL reference(words); + reference.set_execution_mode(spv::ExecutionModeOutputVertices, 3); + reference.set_msl_options(tess.options.mslOptions); + reference.compile(); + auto expected = reference.get_msl_captured_vertex_layout(); + converter.setSPIRV(words.data(), words.size()); + check(converter.convert(tess, result), "TES capture conversion failed"); + check(result.resultInfo.needsOutputBuffer && equal(result.resultInfo.capturedVertexLayout, expected), "TES converter did not export the captured physical layout"); + std::ofstream output(directory + "/" + fixture + ".metal"); + output << result.msl; + check(bool(output), "Cannot write TES capture MSL"); + } + auto unsupported = readSPIRV((directory + "/capture-unsupported.spv").c_str()); + converter.setSPIRV(unsupported.data(), unsupported.size()); + SPIRVToMSLConversionConfiguration config; + config.options.mslOptions.set_msl_version(2, 4); + config.options.mslOptions.capture_output_to_buffer = true; + config.exportCapturedVertexLayout = true; + // Leave entryPointStage unspecified to exercise execution-model inference from SPIR-V. + result.resultInfo.capturedVertexLayout.stride = 123; + result.resultInfo.capturedVertexLayout.components.push_back({0, 0, 0, SPIRType::Float}); + result.resultInfo.capturedVertexLayout.builtins.push_back({spv::BuiltInPosition, 0, 0, 16, SPIRType::Float}); + result.resultLog.clear(); + check(!converter.convert(config, result), "Unsupported capture layout returned success"); + check(result.resultLog.find("16/32-bit") != std::string::npos, result.resultLog.c_str()); + check(result.msl.empty() && equal(result.resultInfo.capturedVertexLayout, {}), "Failed export retained partial or stale output"); + std::cout << "PASS: 4 producer exports match compiler reflection and round-trip, 48 equality/serialization mutations including builtins, 2 restored-layout consumers, inactive/failed result reset\n"; +} + +int main(int argc, char** argv) { + try { + check(argc == 4, "Usage: test typed-pervertex.spv ordinary.spv output-directory"); + auto original = configuration(); + check(!SPIRVToMSLConversionConfiguration{}.perVertexInputBuffer.enabled, "Portable input must default off"); + check(!SPIRVToMSLConversionResultInfo{}.needsPerVertexInputBuffer, "Used flag must default off"); + std::vector> changes = { + [](auto& value) { value.enabled = false; }, + [](auto& value) { value.layout.stride += 4; }, + [](auto& value) { value.layout.components[0].location++; }, + [](auto& value) { value.layout.components[0].component++; }, + [](auto& value) { value.layout.components[0].byte_offset += 4; }, + [](auto& value) { value.layout.components[0].scalar_type = SPIRType::Int; }, + [](auto& value) { value.layout.components.pop_back(); }, + [](auto& value) { value.binding.vertex_buffer_index--; }, + [](auto& value) { value.binding.primitive_index_buffer_index--; }, + [](auto& value) { value.binding.primitive_index_location++; }, + [](auto& value) { value.binding.primitive_id_buffer_index = 27; } + }; + for (auto& change : changes) { + auto changed = original; + change(changed.perVertexInputBuffer); + check(!original.matches(changed) && !changed.matches(original), "ABI change lost in cache matching"); + check(archive(original.perVertexInputBuffer) != archive(changed.perVertexInputBuffer), "ABI change lost in serialization"); + std::istringstream bytes(archive(changed.perVertexInputBuffer)); + cereal::BinaryInputArchive input(bytes); + MSLPerVertexInputBuffer restored; + input(restored); + check(archive(restored) == archive(changed.perVertexInputBuffer), "ABI round-trip lost a field"); + } + auto config = original; + check(config.matches(original), "Copy should match"); + config.alignWith(original); + check(config.matches(original), "Alignment changed the input ABI"); + SPIRVToMSLConverter converter; + auto words = readSPIRV(argv[1]); + converter.setSPIRV(words.data(), words.size()); + SPIRVToMSLConversionResult result; + for (bool arguments : {false, true}) { + for (bool arrays : {false, true}) { + config = original; + config.options.mslOptions.argument_buffers = arguments; + config.options.mslOptions.force_native_arrays = arrays; + check(converter.convert(config, result), result.resultLog.c_str()); + check(result.resultInfo.needsPerVertexInputBuffer, "Active shader lost used flag"); + check(result.msl.find("* 96ul + 24ul") != std::string::npos, "Explicit physical ABI lost"); + check(result.msl.find("[[buffer(28)]]") != std::string::npos && result.msl.find("[[buffer(29)]]") != std::string::npos, "Explicit buffer indices lost"); + check(result.msl.find("user(locn15)") != std::string::npos, "Private location lost"); + check(result.msl.find("vertex_value") == std::string::npos && result.msl.find("[[primitive_id]]") == std::string::npos, "Portable output uses native path"); + std::ofstream file(std::string(argv[3]) + "/" + (arguments ? "arguments" : "classic") + (arrays ? "-arrays" : "") + ".metal"); + file << result.msl; + check(bool(file), "Cannot write MSL"); + } + } + auto rejects = [&](const std::function& change, const char* diagnostic) { + config = original; + change(config); + result.resultInfo.needsPerVertexInputBuffer = true; + result.msl = "stale shader"; + result.resultLog.clear(); + check(!converter.convert(config, result), "Rejected configuration returned success"); + check(!result.resultInfo.needsPerVertexInputBuffer && result.msl.empty(), "Failed conversion retained successful result"); + check(result.resultLog.find(diagnostic) != std::string::npos, result.resultLog.c_str()); + }; + rejects([](auto& c) { c.perVertexInputBuffer.enabled = false; }, "PerVertexKHR"); + rejects([](auto& c) { c.options.mslOptions.set_msl_version(2, 3); }, "MSL 2.4"); + rejects([](auto& c) { c.options.mslOptions.supports_per_vertex_fragment_input = true; }, "mutually exclusive"); + rejects([](auto& c) { c.perVertexInputBuffer.layout.stride = 0; }, "nonzero producer stride"); + rejects([](auto& c) { c.perVertexInputBuffer.binding.vertex_buffer_index = 5; }, "collides"); + rejects([](auto& c) { c.perVertexInputBuffer.layout.components[0].scalar_type = SPIRType::Int; }, "type mismatch"); + words = readSPIRV(argv[2]); + converter.setSPIRV(words.data(), words.size()); + config = original; + result.resultInfo.needsPerVertexInputBuffer = true; + check(converter.convert(config, result), result.resultLog.c_str()); + check(!result.resultInfo.needsPerVertexInputBuffer, "Inactive shader retained used flag"); + auto ordinaryMSL = result.msl; + config.perVertexInputBuffer.enabled = false; + check(converter.convert(config, result), result.resultLog.c_str()); + check(result.msl == ordinaryMSL, "Inactive portable input changed ordinary shader"); + std::cout << "PASS: 11 cache identity/serialization mutations, 4 active variants, 6 rejections, inactive reuse and unchanged ordinary MSL\n"; + checkCaptureLayout(converter, result, argv[3]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/MoltenVKShaderConverter/Tests/PerVertexPipelineInterfaceTests.cpp b/MoltenVKShaderConverter/Tests/PerVertexPipelineInterfaceTests.cpp new file mode 100644 index 000000000..425155080 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/PerVertexPipelineInterfaceTests.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "SPIRVReflection.h" +#include "SPIRVToMSLConverter.h" +#include +#include +#include +#include + +using namespace mvk; +using namespace SPIRV_CROSS_NAMESPACE; + +static void check(bool value, const char* message) { + if (!value) { throw std::runtime_error(message); } +} + +static std::vector readSPIRV(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + check(bool(file), "Cannot open fixture"); + auto size = file.tellg(); + check(size > 0 && size % 4 == 0, "Invalid fixture size"); + std::vector words(size_t(size) / 4); + file.seekg(0); + file.read(reinterpret_cast(words.data()), size); + check(bool(file), "Cannot read fixture"); + return words; +} + +int main(int argc, char** argv) { + try { + check(argc == 2 || argc == 3, "Usage: pervertex-pipeline-interface-test fixture-directory [consumer.spv]"); + std::string consumerName = argc == 3 ? argv[2] : "interface.frag.spv"; + auto consumer = readSPIRV(std::string(argv[1]) + "/" + consumerName); + std::vector inputs; + inputs.reserve(128); + std::string error; + check(getShaderInputs(consumer, spv::ExecutionModelFragment, "main", inputs, error), error.c_str()); + check(inputs.size() == 17, "PerVertex vertex dimension consumed varying locations (expected 16 user leaves plus FragCoord)"); + check(std::count_if(inputs.begin(), inputs.end(), [](const auto& input) { return input.perVertex; }) == 15, "Missing captured block, matrix or inner-array leaves"); + auto producer = readSPIRV(std::string(argv[1]) + "/interface.vert.spv"); + std::vector outputs; + outputs.reserve(128); + check(getShaderOutputs(producer, spv::ExecutionModelVertex, "main", outputs, error), error.c_str()); + SPIRVToMSLConverter converter; + SPIRVToMSLConversionConfiguration capture; + capture.options.entryPointName = "main"; + capture.options.entryPointStage = spv::ExecutionModelVertex; + capture.options.mslOptions.set_msl_version(2, 4); + capture.options.mslOptions.capture_output_to_buffer = true; + capture.exportCapturedVertexLayout = true; + SPIRVToMSLConversionResult result; + converter.setSPIRV(producer.data(), producer.size()); + check(converter.convert(capture, result), result.resultLog.c_str()); + auto layout = result.resultInfo.capturedVertexLayout; + check(layout.stride != 0, "Producer capture layout missing"); + std::unordered_set locations; + for (const auto& output : outputs) { if (output.builtin == spv::BuiltInMax) { locations.insert(output.location); } } + for (const auto& input : inputs) { if (input.builtin == spv::BuiltInMax) { locations.insert(input.location); } } + for (const auto& component : layout.components) { locations.insert(component.location); } + uint32_t key = 0; + while (key < 16 && locations.count(key)) { key++; } + check(key == 15, "Dense interface lost its only free private-key location"); + SPIRVToMSLConversionConfiguration fragment; + fragment.options.entryPointName = "main"; + fragment.options.entryPointStage = spv::ExecutionModelFragment; + fragment.options.mslOptions.set_msl_version(2, 4); + fragment.setPerVertexInputBuffer(layout, {28, 29, key}); + // Populate the same producer remaps as addPrevStageOutputToShaderConversionConfig. + for (const auto& output : outputs) { + if (!output.isUsed) { continue; } + mvk::MSLShaderInput input; + input.shaderVar.location = output.location; + input.shaderVar.component = output.component; + input.shaderVar.builtin = output.builtin; + input.shaderVar.vecsize = output.vecWidth; + fragment.shaderInputs.push_back(input); + } + converter.setSPIRV(consumer.data(), consumer.size()); + auto oldRemaps = fragment; + CompilerReflection reflect(consumer); + reflect.set_entry_point("main", spv::ExecutionModelFragment); + for (auto id : reflect.get_active_interface_variables()) { + if (reflect.get_storage_class(id) != spv::StorageClassInput || !reflect.has_decoration(id, spv::DecorationPerVertexKHR)) { continue; } + auto location = reflect.get_decoration(id, spv::DecorationLocation); + auto component = reflect.get_decoration(id, spv::DecorationComponent); + auto& remaps = oldRemaps.shaderInputs; + remaps.erase(std::remove_if(remaps.begin(), remaps.end(), [&](const auto& input) { return input.shaderVar.builtin == spv::BuiltInMax && input.shaderVar.location == location && input.shaderVar.component == component; }), remaps.end()); + } + check(!converter.convert(oldRemaps, result), "Old base-only remap removal unexpectedly succeeded"); + check(result.resultLog.find("remapping for captured inputs") != std::string::npos, result.resultLog.c_str()); + // Apply the pipeline's flattened leaf filtering, keeping ordinary shared-location components. + for (const auto& leaf : inputs) { + if (!leaf.perVertex) { continue; } + auto& remaps = fragment.shaderInputs; + remaps.erase(std::remove_if(remaps.begin(), remaps.end(), [&](const auto& input) { return input.shaderVar.builtin == spv::BuiltInMax && input.shaderVar.location == leaf.location && input.shaderVar.component == leaf.component; }), remaps.end()); + } + check(std::count_if(fragment.shaderInputs.begin(), fragment.shaderInputs.end(), [](const auto& input) { return input.shaderVar.builtin == spv::BuiltInMax; }) == 1, "Captured remaps remain or ordinary component was removed"); + check(std::any_of(fragment.shaderInputs.begin(), fragment.shaderInputs.end(), [](const auto& input) { return input.shaderVar.location == 14 && input.shaderVar.component == 2; }), "Ordinary component remap lost"); + check(converter.convert(fragment, result), result.resultLog.c_str()); + check(result.resultInfo.needsPerVertexInputBuffer, "Linked consumer did not use capture buffers"); + check(result.msl.find("user(locn15)") != std::string::npos, "Private key missing from linked consumer"); + std::ofstream(std::string(argv[1]) + "/" + consumerName + ".metal") << result.msl; + CompilerMSL replay(producer); + auto options = replay.get_msl_options(); + options.set_msl_version(2, 4); + replay.set_msl_options(options); + std::string replayMSL = replay.compile_captured_output_replay(layout, {0, 1, 2, key}); + check(replayMSL.find("user(locn15)") != std::string::npos, "Replay key does not match fragment"); + std::ofstream(std::string(argv[1]) + "/interface.replay.metal") << replayMSL; + std::cout << "PASS: linked capture/replay/consumer, block/matrix/nested-array locations, shared component, dense private key, old-remap rejection\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/MoltenVKShaderConverter/Tests/PipelineCachePerVertexTests.cpp b/MoltenVKShaderConverter/Tests/PipelineCachePerVertexTests.cpp new file mode 100644 index 000000000..9d244477b --- /dev/null +++ b/MoltenVKShaderConverter/Tests/PipelineCachePerVertexTests.cpp @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "SPIRVToMSLConverter.h" +#include +#include +#include +#include +#include "PipelineCacheSerializers.inc" +#include +#include +#include + +using namespace mvk; +using namespace SPIRV_CROSS_NAMESPACE; + +static void check(bool condition, const char* message) { + if (!condition) { throw std::runtime_error(message); } +} + +static void checkLayout(const MSLCapturedVertexLayout& actual, const MSLCapturedVertexLayout& expected, const char* message) { + check(actual.stride == expected.stride && actual.components.size() == expected.components.size() && actual.builtins.size() == expected.builtins.size(), message); + for (size_t i = 0; i < expected.components.size(); i++) { + const auto& a = actual.components[i]; + const auto& e = expected.components[i]; + check(a.location == e.location && a.component == e.component && a.byte_offset == e.byte_offset && a.scalar_type == e.scalar_type, message); + } + for (size_t i = 0; i < expected.builtins.size(); i++) { + const auto& a = actual.builtins[i]; + const auto& e = expected.builtins[i]; + check(a.builtin == e.builtin && a.array_index == e.array_index && a.component == e.component && a.byte_offset == e.byte_offset && a.scalar_type == e.scalar_type, message); + } +} + +int main() { + try { + for (bool layered : {false, true}) { + MSLCapturedVertexLayout layout; + layout.stride = 36; + layout.builtins.push_back({spv::BuiltInLayer, 0, 0, 32, SPIRType::UInt}); + for (uint32_t c = 0; c < 3; c++) { layout.components.push_back({2, c, 4 * c, SPIRType::Float}); } + for (uint32_t c = 0; c < 4; c++) { layout.builtins.push_back({spv::BuiltInPosition, 0, c, 16 + 4 * c, SPIRType::Float}); } + SPIRVToMSLConversionConfiguration cfg; + cfg.options.entryPointName = "main"; + cfg.options.entryPointStage = spv::ExecutionModelFragment; + cfg.options.mslOptions.set_msl_version(2, 4); + cfg.options.mslOptions.multiview = true; + cfg.options.mslOptions.multiview_layered_rendering = layered; + auto otherMode = cfg; + otherMode.options.mslOptions.multiview_layered_rendering = !layered; + check(!cfg.matches(otherMode) && !otherMode.matches(cfg), "cfg layered mode aliases cache identity"); + cfg.setPerVertexInputBuffer(layout, {28, 29, 15}); + cfg.exportCapturedVertexLayout = true; + SPIRVToMSLConversionResultInfo scr{}; + scr.entryPoint.mtlFunctionName = "cacheRoundTrip"; + scr.entryPoint.fpFastMathFlags = 17; + scr.specializationMacros.emplace(7, MSLSpecializationMacroInfo{"testMacro", true, false}); + scr.needsPerVertexInputBuffer = true; + scr.needsDepthClipStateBuffer = true; + scr.capturedVertexLayout = layout; + std::stringstream bytes; + { cereal::BinaryOutputArchive output(bytes); output(cfg, scr); } + SPIRVToMSLConversionConfiguration restoredCfg; + SPIRVToMSLConversionResultInfo restoredScr{}; + { cereal::BinaryInputArchive input(bytes); input(restoredCfg, restoredScr); } + check(restoredCfg.perVertexInputBuffer.enabled, "cfg.perVertexInputBuffer lost"); + check(restoredCfg.exportCapturedVertexLayout, "cfg.exportCapturedVertexLayout lost"); + checkLayout(restoredCfg.perVertexInputBuffer.layout, layout, "cfg layout user/builtin fields lost"); + const auto& binding = restoredCfg.perVertexInputBuffer.binding; + check(binding.vertex_buffer_index == 28 && binding.primitive_index_buffer_index == 29 && binding.primitive_index_location == 15, "cfg binding fields lost"); + check(cfg.matches(restoredCfg) && restoredCfg.matches(cfg), "cfg round-trip changed cache identity"); + check(restoredScr.needsPerVertexInputBuffer, "scr.needsPerVertexInputBuffer lost"); + check(restoredScr.needsDepthClipStateBuffer, "scr.needsDepthClipStateBuffer lost"); + checkLayout(restoredScr.capturedVertexLayout, layout, "scr.capturedVertexLayout user/builtin fields lost"); + check(restoredScr.entryPoint.mtlFunctionName == "cacheRoundTrip" && restoredScr.entryPoint.fpFastMathFlags == 17 && restoredScr.specializationMacros.at(7).name == "testMacro", "scr surrounding metadata lost"); + check(bytes.peek() == std::char_traits::eof(), "Archive not fully consumed"); + std::cout << "PASS: actual MVKPipeline cfg/scr serializers preserve user fields, Position/Layer, multiview options, binding, used flag and cache identity (" << bytes.str().size() << " bytes)\n"; + } + return 0; + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 2; + } +} diff --git a/MoltenVKShaderConverter/Tests/PortableBarycentricTests.cpp b/MoltenVKShaderConverter/Tests/PortableBarycentricTests.cpp new file mode 100644 index 000000000..11e2c8fb3 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/PortableBarycentricTests.cpp @@ -0,0 +1,267 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "SPIRVToMSLConverter.h" +#include "SPIRVReflection.h" +#include "MVKPerVertexReplay.h" +#include "MVKPerVertexCapacity.h" +#include +#include +#include +#include +#include "PipelineCacheSerializers.inc" +#include +#include +#include +#include +using namespace mvk; +using namespace SPIRV_CROSS_NAMESPACE; +static void check(bool value, const char* message) { if (!value) { throw std::runtime_error(message); } } +static std::vector readSPIRV(const std::string& path) { + std::ifstream stream(path, std::ios::binary | std::ios::ate); + check(bool(stream), "Missing SPIR-V fixture"); + auto size = stream.tellg(); + check(size > 0 && size % 4 == 0, "Invalid fixture size"); + std::vector result(size_t(size) / 4); + stream.seekg(0); + stream.read(reinterpret_cast(result.data()), size); + check(bool(stream), "Failed fixture read"); + return result; +} +static unsigned pipelineBarycentricMask(const std::vector& inputs) { + bool _usesPerspectiveBarycentrics = false, _usesNoPerspectiveBarycentrics = false; +#include "PipelineBarycentricSelection.inc" + return unsigned(_usesPerspectiveBarycentrics) | (unsigned(_usesNoPerspectiveBarycentrics) << 1); +} +static void checkActiveBarycentrics(const std::string& directory) { + struct Case { const char* fixture; const char* entry; unsigned mask; }; + const Case cases[] = { + {"active-block", "perspective", 1}, {"active-block", "linear", 2}, + {"active-block", "ordinary", 0}, {"active-block", "both", 3}, {"active-block", "unused", 0}, + {"active-block", "explicit_perspective", 1}, {"active-block", "explicit_linear", 2}, + {"explicit-only", "main", 3}, {"copied-pointer", "main", 3}, + {"copied-pointer", "direct_helper", 3}, {"copied-pointer", "block_helper", 3}, + {"copied-pointer", "array_helper", 1}, {"copied-pointer", "component_alias", 2} + }; + unsigned failures = 0; + for (const auto& c : cases) { + try { + auto words = readSPIRV(directory + "/" + c.fixture + ".spv"); + const auto original = words; + std::vector inputs; + std::string error; + check(getShaderInputs(words, spv::ExecutionModelFragment, c.entry, inputs, error), error.c_str()); + check(words == original, "Reflection modified the input module"); + unsigned mask = pipelineBarycentricMask(inputs); + unsigned reflectedMask = 0; + for (const auto& input : inputs) { + if (input.isUsed && input.builtin == spv::BuiltInBaryCoordKHR) { reflectedMask |= 1; } + if (input.isUsed && input.builtin == spv::BuiltInBaryCoordNoPerspKHR) { reflectedMask |= 2; } + } + check(mask == c.mask, "Pipeline selected an inactive basis or omitted an active basis"); + check(reflectedMask == c.mask, "Reflection omitted explicit interpolation or included an inactive member"); + bool perVertex = true; + check(getFragmentShaderUsesPerVertexInput(words, c.entry, perVertex, error) && !perVertex, "Unexpected PerVertex input"); + check((perVertex || mask) == bool(c.mask), "Wrong portable path selection"); + if (!mask) { + // Selection-only: native MSL lowering of mixed builtin blocks is outside this regression. + std::cout << "PASS activity (selection only): " << c.fixture << "/" << c.entry << " mask=0\n"; + continue; + } + SPIRVToMSLConverter converter; + SPIRVToMSLConversionConfiguration cfg; + cfg.options.entryPointName = c.entry; + cfg.options.entryPointStage = spv::ExecutionModelFragment; + cfg.options.mslOptions.set_msl_version(2, 4); + if (mask) { cfg.setFragmentBarycentricInput({mask & 1 ? 8u : ~0u, mask & 2 ? 9u : ~0u}); } + converter.setSPIRV(words.data(), words.size()); + SPIRVToMSLConversionResult result; + check(converter.convert(cfg, result), result.resultLog.c_str()); + check((result.msl.find("user(locn8)") != std::string::npos) == bool(mask & 1), "Wrong perspective varying"); + check((result.msl.find("user(locn9)") != std::string::npos) == bool(mask & 2), "Wrong linear varying"); + auto contains = [&](const char* text) { return result.msl.find(text) != std::string::npos; }; + if (std::string(c.entry).find("explicit_") == 0) { + check(contains(".interpolate_at_centroid()") && contains(".interpolate_at_sample(1u).y") && contains(" + 0.4375).y"), "Copied block/scalar pointers lost their explicit interpolation or component"); + } + if (std::string(c.fixture) == "copied-pointer") { + std::string entry = c.entry; + check(!contains("_m4294967295"), "Copied pointer lost its interface member"); + if (entry == "component_alias") { check(contains("in.gl_BaryCoordNoPerspEXT.interpolate_at_centroid().y"), "Empty access chain lost its scalar component"); } + if (entry == "main") { check(contains("in.gl_BaryCoordNoPerspEXT.interpolate_at_centroid()"), "Copied vector lost explicit interpolation"); } + if (entry == "array_helper") { check(contains(" + 0.4375).z"), "Copied array pointer lost its scalar component"); } + if (entry == "direct_helper" || entry == "block_helper") { + check(contains("in.gl_BaryCoordEXT.interpolate_at_centroid().y") && contains("in.gl_BaryCoordNoPerspEXT.interpolate_at_sample(1u)") && contains(" + 0.4375)[") && contains(".interpolate_at_sample(1u)["), "Copied helper pointer lost its source, interpolation or dynamic component"); + } + } + std::ofstream(directory + "/activity-" + c.fixture + "-" + c.entry + ".metal") << result.msl; + if (mask == 1 || mask == 2) { + for (uint32_t limit : {60u, 124u}) { + auto producer = readSPIRV(directory + "/dense-" + std::to_string(limit / 4) + ".spv"); + SPIRVToMSLConversionConfiguration capture; + capture.options.entryPointName = "main"; + capture.options.entryPointStage = spv::ExecutionModelVertex; + capture.options.mslOptions.set_msl_version(2, 4); + capture.options.mslOptions.capture_output_to_buffer = true; + capture.exportCapturedVertexLayout = true; + converter.setSPIRV(producer.data(), producer.size()); + check(converter.convert(capture, result), result.resultLog.c_str()); + const auto& layout = result.resultInfo.capturedVertexLayout; + check(mvkPerVertexReplayVaryingComponents(layout, mask & 1, mask & 2) == limit - 2, "Dense producer charged an inactive basis"); + check(mvkPerVertexReplayVaryingComponents(layout, true, true) == limit + 1, "Dense fixture does not reproduce F1 capacity rejection"); + } + } + std::cout << "PASS activity: " << c.fixture << "/" << c.entry << " mask=" << mask << '\n'; + } catch (const std::exception& e) { ++failures; std::cerr << "FAIL activity: " << c.fixture << "/" << c.entry << ": " << e.what() << '\n'; } + } + check(!failures, "Barycentric activity regression failed"); +} +static void checkDraws() { + struct Case { VkPrimitiveTopology topology; bool last; uint32_t count; std::vector records; }; + const Case cases[] = { + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, false, 7, {0, 1, 2, 3, 4, 5}}, + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, true, 7, {0, 1, 2, 3, 4, 5}}, + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, 5, {0, 1, 2, 1, 3, 2, 2, 3, 4}}, + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, true, 5, {0, 1, 2, 2, 1, 3, 2, 3, 4}}, + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, false, 5, {1, 2, 0, 2, 3, 0, 3, 4, 0}}, + {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, true, 5, {0, 1, 2, 0, 2, 3, 0, 3, 4}} + }; + for (const auto& c : cases) { + size_t n = c.records.size(); + std::vector pairs(n * 4, ~0u), indices(n * 2, ~0u), corners(n * 2, ~0u); + mvkPopulatePerVertexReplay(c.count, 2, c.topology, c.last, pairs.data(), indices.data(), corners.data()); + for (size_t occurrence = 0; occurrence < 2 * n; ++occurrence) { + check(pairs[2 * occurrence] == c.records[occurrence % n] + (occurrence / n) * c.count, "Wrong capture record or instance"); + check(pairs[2 * occurrence + 1] == occurrence / 3 && indices[occurrence] == pairs[2 * occurrence], "Primitive key/triplet mismatch"); + check(corners[occurrence] == occurrence % 3, "Corner basis does not match triplet order"); + } + // Legacy path writes identical pairs/triplets without a corner buffer. + auto original = pairs; + mvkPopulatePerVertexReplay(c.count, 2, c.topology, c.last, pairs.data(), indices.data(), nullptr); + check(pairs == original, "Optional basis changed legacy occurrence pairs"); + } + uint32_t sentinel = 99; + mvkPopulatePerVertexReplay(2, 2, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, &sentinel, &sentinel, &sentinel); + check(sentinel == 99, "Incomplete primitive wrote payload"); + std::unordered_set occupied{0, 2, 5}; + check(mvkAllocatePerVertexVaryingLocation(occupied, 6) == 1, "Private key collided with occupied location"); + check(mvkAllocatePerVertexVaryingLocation(occupied, 6) == 3, "Perspective basis collided with private key"); + check(mvkAllocatePerVertexVaryingLocation(occupied, 6) == 4, "Linear basis collided with perspective basis"); + check(mvkAllocatePerVertexVaryingLocation(occupied, 6) == ~0u, "Dense varying interface overflow accepted"); +} +int main(int argc, char** argv) { + try { + check(argc == 2, "Usage: portable-barycentric-test output-directory"); + std::string directory = argv[1]; + checkActiveBarycentrics(directory); + checkDraws(); + auto producer = readSPIRV(directory + "/producer.spv"); + SPIRVToMSLConverter converter; + SPIRVToMSLConversionConfiguration capture; + capture.options.entryPointName = "main"; + capture.options.entryPointStage = spv::ExecutionModelVertex; + capture.options.mslOptions.set_msl_version(2, 4); + capture.options.mslOptions.capture_output_to_buffer = true; + capture.exportCapturedVertexLayout = true; + SPIRVToMSLConversionResult result; + converter.setSPIRV(producer.data(), producer.size()); + check(converter.convert(capture, result), result.resultLog.c_str()); + auto layout = result.resultInfo.capturedVertexLayout; + check(layout.stride > 0, "Capture layout absent"); + std::ofstream(directory + "/capture.metal") << result.msl; + for (unsigned mask = 1; mask < 4; ++mask) { + for (unsigned captured = 0; captured < 2; ++captured) { + std::string name = std::to_string(mask) + "-" + std::to_string(captured); + auto spirv = readSPIRV(directory + "/fragment-" + name + ".spv"); + SPIRVToMSLConversionConfiguration cfg; + cfg.options.entryPointName = "main"; + cfg.options.entryPointStage = spv::ExecutionModelFragment; + cfg.options.mslOptions.set_msl_version(2, 4); + MSLFragmentBarycentricInputBinding binding{mask & 1 ? 2u : ~0u, mask & 2 ? 3u : ~0u}; + cfg.setFragmentBarycentricInput(binding); + if (captured) { cfg.setPerVertexInputBuffer(layout, {28, 29, 1}); } + converter.setSPIRV(spirv.data(), spirv.size()); + check(converter.convert(cfg, result), result.resultLog.c_str()); + check(result.resultInfo.needsPerVertexInputBuffer == bool(captured), "Barycentric-only conversion incorrectly requires captured inputs"); + check(result.msl.find("barycentric_coord") == std::string::npos, "Native barycentric attribute leaked into portable fragment"); + std::ofstream(directory + "/fragment-" + name + ".metal") << result.msl; + CompilerMSL replay(producer); + auto options = replay.get_msl_options(); options.set_msl_version(2, 4); replay.set_msl_options(options); + auto source = replay.compile_captured_output_replay(layout, {0, 1, 2, 1}, {3, binding.perspective_location, binding.no_perspective_location}); + check(source.find("barycentric_coord") == std::string::npos, "Native attribute leaked into replay"); + std::ofstream(directory + "/replay-" + name + ".metal") << source; + std::stringstream bytes; + { cereal::BinaryOutputArchive archive(bytes); archive(cfg); } + SPIRVToMSLConversionConfiguration restored; + { cereal::BinaryInputArchive archive(bytes); archive(restored); } + check(cfg.matches(restored) && restored.matches(cfg), "Production cache round-trip lost barycentric ABI"); + for (unsigned field = 0; field < 3; ++field) { + auto changed = restored; + if (field == 0) { changed.fragmentBarycentricInput.enabled = false; } + if (field == 1) { changed.fragmentBarycentricInput.binding.perspective_location ^= 4; } + if (field == 2) { changed.fragmentBarycentricInput.binding.no_perspective_location ^= 4; } + check(!cfg.matches(changed) && !changed.matches(cfg), "Cache identity ignores opt-in or private location"); + } + auto missing = cfg; + missing.setFragmentBarycentricInput({}); + check(!converter.convert(missing, result), "Missing private locations accepted"); + } + } + // Capture explicit PointSize; the pinned replay API must not synthesize a default. + auto pointProducer = readSPIRV(directory + "/point-producer.spv"); + auto pointCapture = capture; + pointCapture.options.mslOptions.enable_point_size_builtin = true; + pointCapture.options.mslOptions.enable_point_size_default = true; + converter.setSPIRV(pointProducer.data(), pointProducer.size()); + check(converter.convert(pointCapture, result), result.resultLog.c_str()); + auto pointLayout = result.resultInfo.capturedVertexLayout; + check(std::any_of(pointLayout.builtins.begin(), pointLayout.builtins.end(), [](const auto& field) { return field.builtin == spv::BuiltInPointSize; }), "PointSize missing from captured layout"); + std::ofstream(directory + "/point-capture.metal") << result.msl; + for (unsigned index = 0; index < 2; ++index) { + auto indexed = pointCapture; + indexed.options.mslOptions.vertex_for_tessellation = true; + indexed.options.mslOptions.vertex_index_type = index ? CompilerMSL::Options::IndexType::UInt32 : CompilerMSL::Options::IndexType::UInt16; + check(converter.convert(indexed, result), result.resultLog.c_str()); + MSLPerVertexInputBuffer expected{true, pointLayout, {}}, actual{true, result.resultInfo.capturedVertexLayout, {}}; + check(expected.matches(actual), "Indexed point capture layout differs from nonindexed capture"); + std::ofstream(directory + "/point-indexed-" + std::to_string(index) + ".metal") << result.msl; + } + CompilerMSL pointReplay(pointProducer); + auto pointOptions = pointCapture.options.mslOptions; + pointOptions.capture_output_to_buffer = false; + pointOptions.enable_point_size_default = false; + pointReplay.set_msl_options(pointOptions); + auto pointSource = pointReplay.compile_captured_output_replay(pointLayout, {0, 1, 2, 1}, {3, 2, 3}); + check(pointSource.find("[[point_size]]") != std::string::npos, "Replay lost PointSize raster attribute"); + check(pointSource.find("spvReplayCorners[spvReplayOccurrence]") != std::string::npos, "Point replay lost corner basis"); + std::ofstream(directory + "/point-replay.metal") << pointSource; + // The replay API cannot reproduce the depth transform; it must refuse it instead of ignoring the option. + CompilerMSL depthClipReplay(pointProducer); + auto depthClipOptions = pointOptions; + depthClipOptions.emulate_depth_clip_enable = true; + depthClipReplay.set_msl_options(depthClipOptions); + bool depthClipRefused = false; + try { depthClipReplay.compile_captured_output_replay(pointLayout, {0, 1, 2, 1}, {3, 2, 3}); } + catch (const CompilerError&) { depthClipRefused = true; } + check(depthClipRefused, "Replay silently accepted unsupported depth clip emulation"); + auto explicitOnly = readSPIRV(directory + "/explicit-only.spv"); + std::vector inputs; + std::string error; + check(getShaderInputs(explicitOnly, spv::ExecutionModelFragment, "main", inputs, error), error.c_str()); + check(std::any_of(inputs.begin(), inputs.end(), [](const auto& i) { return i.builtin == spv::BuiltInBaryCoordKHR; }), "Explicit-only builtin absent from pipeline reflection"); + SPIRVToMSLConversionConfiguration cfg; + cfg.options.entryPointName = "main"; cfg.options.entryPointStage = spv::ExecutionModelFragment; cfg.options.mslOptions.set_msl_version(2, 4); + cfg.setFragmentBarycentricInput({2, 3}); + converter.setSPIRV(explicitOnly.data(), explicitOnly.size()); + check(converter.convert(cfg, result), result.resultLog.c_str()); + std::ofstream(directory + "/explicit-only.metal") << result.msl; + SPIRVToMSLConversionConfiguration empty; + auto disabled = empty; + disabled.fragmentBarycentricInput.binding = {8, 9}; + check(empty.matches(disabled) && disabled.matches(empty), "Disabled binding changed cache identity"); + std::stringstream bytes; + { cereal::BinaryOutputArchive archive(bytes); archive(empty); } + check(sizeof(empty) - bytes.str().size() == 154, "Pipeline archive padding expectation is stale"); + std::cout << "Configuration sizeof=" << sizeof(empty) << " empty archive=" << bytes.str().size() << "\n"; + std::cout << "PASS: six matching capture/replay/fragment variants, explicit-only reflection, production cache round-trip/mutations, list/strip/fan corners, instances, optional payload and incomplete primitives\n"; + return 0; + } catch (const std::exception& e) { std::cerr << "FAIL: " << e.what() << '\n'; return 1; } +} diff --git a/MoltenVKShaderConverter/Tests/ShaderInterfaceReflectionTests.cpp b/MoltenVKShaderConverter/Tests/ShaderInterfaceReflectionTests.cpp new file mode 100644 index 000000000..93dd34677 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/ShaderInterfaceReflectionTests.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#include "SPIRVReflection.h" +#include "MVKSmallVector.h" +#include +#include +#include +using namespace mvk; +static void check(bool value, const char* message) { if (!value) { throw std::runtime_error(message); } } +static std::vector readSPIRV(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + auto size = file.tellg(); + check(file && size > 0 && size % 4 == 0, "Invalid SPIR-V fixture"); + std::vector words(size_t(size) / 4); + file.seekg(0); + file.read(reinterpret_cast(words.data()), size); + check(bool(file), "Cannot read fixture"); + return words; +} +static void checkActivity(const std::string& directory) { + struct Case { const char* entry; unsigned mask; }; + const Case cases[] = {{"perspective", 1}, {"linear", 2}, {"both", 3}, {"ordinary", 0}, {"unused", 0}, {"explicit_perspective", 1}, {"explicit_linear", 2}}; + unsigned failures = 0; + for (const char* fixture : {"active-block", "root-alias"}) { + auto words = readSPIRV(directory + "/" + fixture + ".spv"); + const auto original = words; + for (const auto& c : cases) { + std::vector inputs; + inputs.reserve(128); // Isolate R2 from R4; alignment mode deliberately forces reallocations. + std::string error; + check(getShaderInputs(words, spv::ExecutionModelFragment, c.entry, inputs, error), error.c_str()); + check(words == original, "Reflection mutated the source module"); + unsigned mask = 0; + for (const auto& input : inputs) { + if (input.isUsed && input.builtin == spv::BuiltInBaryCoordKHR) { mask |= 1; } + if (input.isUsed && input.builtin == spv::BuiltInBaryCoordNoPerspKHR) { mask |= 2; } + } + bool passed = mask == c.mask; + failures += !passed; + std::cout << (passed ? "PASS " : "FAIL ") << fixture << '/' << c.entry << ": mask=" << mask << " expected=" << c.mask << '\n'; + } + } + check(!failures, "Root pointer aliases lost active members or activated inactive members"); +} +template +static void checkAlignment(const std::vector& words, bool output, unsigned head, Variables& vars) { + std::string error; + bool success = output ? getShaderOutputs(words, spv::ExecutionModelVertex, "main", vars, error) : getShaderInputs(words, spv::ExecutionModelFragment, "main", vars, error); + check(success, error.c_str()); + check(vars.size() == 45 + head, "Wrong flattened array/struct size"); + for (size_t i = 0; i < vars.size(); ++i) { + unsigned width = i < head ? 1 : i == 44 + head ? 4 : (i - head) % 22 == 0 ? 1 : (i - head) % 22 == 1 ? 4 : 2; + unsigned alignment = i == 0 || i == head || i == head + 22 ? 16 : 0; + check(vars[i].location == i && vars[i].component == 0 && vars[i].vecWidth == width && vars[i].isUsed, "Flattened leaf metadata changed"); + check(vars[i].firstStructMemberAlignment == alignment, "Parent/nested struct alignment lost across reallocation"); + check(getShaderInterfaceVariableAlignment(vars[i]) == (alignment ? alignment : 4 * width), "Incorrect effective member alignment"); + } +} +static void checkReallocation(const std::string& directory) { + // Reproduce the review's unreserved three-member builtin block before larger nested interfaces. + std::vector block; + std::string error; + check(getShaderInputs(readSPIRV(directory + "/active-block.spv"), spv::ExecutionModelFragment, "perspective", block, error), error.c_str()); + check(block.size() == 3 && block[0].firstStructMemberAlignment == 16, "Builtin block alignment lost"); + for (bool output : {false, true}) { + for (unsigned head : {0u, 1u}) { + auto words = readSPIRV(directory + "/alignment-" + std::to_string(head) + (output ? ".vert.spv" : ".frag.spv")); + for (unsigned reserve : {0u, 1u, 128u}) { + std::vector vars; + vars.reserve(reserve); + checkAlignment(words, output, head, vars); + checkAlignment(words, output, head, vars); // Reused container must match the first reflection. + } + MVKSmallVector vars; + checkAlignment(words, output, head, vars); // Same inline capacity as production, then heap growth. + checkAlignment(words, output, head, vars); + } + } + std::cout << "PASS: unreserved builtin block; nested arrays, inputs/outputs, parent alignment, std::vector and MVKSmallVector growth/reuse\n"; +} +int main(int argc, char** argv) { + try { + check(argc == 3, "Usage: reflection-tests fixture-directory activity|alignment"); + if (std::string(argv[2]) == "activity") { checkActivity(argv[1]); } + else { check(std::string(argv[2]) == "alignment", "Unknown test mode"); checkReallocation(argv[1]); } + return 0; + } catch (const std::exception& error) { std::cerr << "FAIL: " << error.what() << '\n'; return 1; } +} diff --git a/MoltenVKShaderConverter/Tests/barycentric-active-block.spvasm b/MoltenVKShaderConverter/Tests/barycentric-active-block.spvasm new file mode 100644 index 000000000..947027aca --- /dev/null +++ b/MoltenVKShaderConverter/Tests/barycentric-active-block.spvasm @@ -0,0 +1,147 @@ +; Copyright (c) 2026 Jean-Philippe Meunier +; SPDX-License-Identifier: Apache-2.0 +; F1: only statically active members require a portable barycentric basis. +OpCapability Shader +OpCapability FragmentBarycentricKHR +OpCapability InterpolationFunction +OpCapability SampleRateShading +OpExtension "SPV_KHR_fragment_shader_barycentric" +%glsl = OpExtInstImport "GLSL.std.450" +OpMemoryModel Logical GLSL450 +OpEntryPoint Fragment %perspective "perspective" %weights %color +OpEntryPoint Fragment %linear "linear" %weights %color +OpEntryPoint Fragment %ordinary "ordinary" %weights %color +OpEntryPoint Fragment %both "both" %weights %color +OpEntryPoint Fragment %unused "unused" %weights %color +OpEntryPoint Fragment %explicit_perspective "explicit_perspective" %weights %color +OpEntryPoint Fragment %explicit_linear "explicit_linear" %weights %color +OpExecutionMode %perspective OriginUpperLeft +OpExecutionMode %linear OriginUpperLeft +OpExecutionMode %ordinary OriginUpperLeft +OpExecutionMode %both OriginUpperLeft +OpExecutionMode %unused OriginUpperLeft +OpExecutionMode %explicit_perspective OriginUpperLeft +OpExecutionMode %explicit_linear OriginUpperLeft +OpDecorate %block Block +OpMemberDecorate %block 0 BuiltIn BaryCoordKHR +OpMemberDecorate %block 1 BuiltIn BaryCoordNoPerspKHR +OpMemberDecorate %block 2 BuiltIn FragCoord +OpDecorate %color Location 0 +%void = OpTypeVoid +%float = OpTypeFloat 32 +%uint = OpTypeInt 32 0 +%vec2 = OpTypeVector %float 2 +%vec3 = OpTypeVector %float 3 +%vec4 = OpTypeVector %float 4 +%block = OpTypeStruct %vec3 %vec3 %vec4 +%inblock = OpTypePointer Input %block +%in1 = OpTypePointer Input %float +%in3 = OpTypePointer Input %vec3 +%in4 = OpTypePointer Input %vec4 +%out4 = OpTypePointer Output %vec4 +%fn = OpTypeFunction %void +%vec_fn = OpTypeFunction %vec3 +%u0 = OpConstant %uint 0 +%u1 = OpConstant %uint 1 +%u2 = OpConstant %uint 2 +%f0 = OpConstant %float 0 +%zero4 = OpConstantComposite %vec4 %f0 %f0 %f0 %f0 +%offset = OpConstantComposite %vec2 %f0 %f0 +%weights = OpVariable %inblock Input +%color = OpVariable %out4 Output + +%perspective = OpFunction %void None %fn +%perspective_entry = OpLabel +%perspective_ptr = OpAccessChain %in3 %weights %u0 +%perspective_v = OpLoad %vec3 %perspective_ptr +%perspective_value = OpCompositeConstruct %vec4 %perspective_v %f0 +OpStore %color %perspective_value +OpReturn +OpFunctionEnd + +%linear = OpFunction %void None %fn +%linear_entry = OpLabel +%linear_ptr = OpAccessChain %in3 %weights %u1 +%linear_v = OpLoad %vec3 %linear_ptr +%linear_value = OpCompositeConstruct %vec4 %linear_v %f0 +OpStore %color %linear_value +OpReturn +OpFunctionEnd + +%ordinary = OpFunction %void None %fn +%ordinary_entry = OpLabel +%ordinary_ptr = OpAccessChain %in4 %weights %u2 +%ordinary_value = OpLoad %vec4 %ordinary_ptr +OpStore %color %ordinary_value +OpReturn +OpFunctionEnd + +%both = OpFunction %void None %fn +%both_entry = OpLabel +%both_p = OpAccessChain %in3 %weights %u0 +%both_n = OpAccessChain %in3 %weights %u1 +%both_pv = OpLoad %vec3 %both_p +%both_nv = OpLoad %vec3 %both_n +%both_v = OpFAdd %vec3 %both_pv %both_nv +%both_value = OpCompositeConstruct %vec4 %both_v %f0 +OpStore %color %both_value +OpReturn +OpFunctionEnd + +%unused = OpFunction %void None %fn +%unused_entry = OpLabel +OpStore %color %zero4 +OpReturn +OpFunctionEnd + +%explicit_perspective = OpFunction %void None %fn +%explicit_perspective_entry = OpLabel +%explicit_perspective_v = OpFunctionCall %vec3 %explicit_perspective_read +%explicit_perspective_value = OpCompositeConstruct %vec4 %explicit_perspective_v %f0 +OpStore %color %explicit_perspective_value +OpReturn +OpFunctionEnd + +%explicit_linear = OpFunction %void None %fn +%explicit_linear_entry = OpLabel +%explicit_linear_v = OpFunctionCall %vec3 %explicit_linear_read +%explicit_linear_value = OpCompositeConstruct %vec4 %explicit_linear_v %f0 +OpStore %color %explicit_linear_value +OpReturn +OpFunctionEnd + +; No loads: helpers read copied block/vector/scalar pointers with all three InterpolateAt forms. +%explicit_perspective_read = OpFunction %vec3 None %vec_fn +%explicit_perspective_label = OpLabel +%explicit_perspective_root = OpCopyObject %inblock %weights +%explicit_perspective_root2 = OpCopyObject %inblock %explicit_perspective_root +%explicit_perspective_base = OpInBoundsAccessChain %in3 %explicit_perspective_root2 %u0 +%explicit_perspective_copy = OpCopyObject %in3 %explicit_perspective_base +%explicit_perspective_scalar = OpAccessChain %in1 %explicit_perspective_copy %u1 +%explicit_perspective_alias = OpAccessChain %in1 %explicit_perspective_scalar +%explicit_perspective_scalar_copy = OpCopyObject %in1 %explicit_perspective_alias +%explicit_perspective_centroid = OpExtInst %vec3 %glsl InterpolateAtCentroid %explicit_perspective_copy +%explicit_perspective_sample = OpExtInst %float %glsl InterpolateAtSample %explicit_perspective_scalar_copy %u1 +%explicit_perspective_offset = OpExtInst %float %glsl InterpolateAtOffset %explicit_perspective_scalar_copy %offset +%explicit_perspective_scale = OpFAdd %float %explicit_perspective_sample %explicit_perspective_offset +%explicit_perspective_result = OpVectorTimesScalar %vec3 %explicit_perspective_centroid %explicit_perspective_scale +OpReturnValue %explicit_perspective_result +OpFunctionEnd + +; No loads: helpers read copied block/vector/scalar pointers with all three InterpolateAt forms. +%explicit_linear_read = OpFunction %vec3 None %vec_fn +%explicit_linear_label = OpLabel +%explicit_linear_root = OpCopyObject %inblock %weights +%explicit_linear_root2 = OpCopyObject %inblock %explicit_linear_root +%explicit_linear_base = OpInBoundsAccessChain %in3 %explicit_linear_root2 %u1 +%explicit_linear_copy = OpCopyObject %in3 %explicit_linear_base +%explicit_linear_scalar = OpAccessChain %in1 %explicit_linear_copy %u1 +%explicit_linear_alias = OpAccessChain %in1 %explicit_linear_scalar +%explicit_linear_scalar_copy = OpCopyObject %in1 %explicit_linear_alias +%explicit_linear_centroid = OpExtInst %vec3 %glsl InterpolateAtCentroid %explicit_linear_copy +%explicit_linear_sample = OpExtInst %float %glsl InterpolateAtSample %explicit_linear_scalar_copy %u1 +%explicit_linear_offset = OpExtInst %float %glsl InterpolateAtOffset %explicit_linear_scalar_copy %offset +%explicit_linear_scale = OpFAdd %float %explicit_linear_sample %explicit_linear_offset +%explicit_linear_result = OpVectorTimesScalar %vec3 %explicit_linear_centroid %explicit_linear_scale +OpReturnValue %explicit_linear_result +OpFunctionEnd diff --git a/MoltenVKShaderConverter/Tests/pervertex-interface-linked.frag b/MoltenVKShaderConverter/Tests/pervertex-interface-linked.frag new file mode 100644 index 000000000..623b1db7e --- /dev/null +++ b/MoltenVKShaderConverter/Tests/pervertex-interface-linked.frag @@ -0,0 +1,23 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +#extension GL_EXT_fragment_shader_barycentric : require +layout(location = 0) pervertexEXT in Inputs { vec3 color; vec2 basis0; vec2 basis1; vec2 uv; float weight0; float weight1; } values[3]; +layout(location = 6) pervertexEXT in vec2 matrix0[3]; +layout(location = 7) pervertexEXT in vec2 matrix1[3]; +layout(location = 8) pervertexEXT in vec2 matrix2[3]; +layout(location = 9) pervertexEXT in vec2 matrix3[3]; +layout(location = 10) pervertexEXT in vec2 grid0[3]; +layout(location = 11) pervertexEXT in vec2 grid1[3]; +layout(location = 12) pervertexEXT in vec2 grid2[3]; +layout(location = 13) pervertexEXT in vec2 grid3[3]; +layout(location = 14, component = 0) pervertexEXT in vec2 packed[3]; +layout(location = 14, component = 2) in vec2 ordinary; +layout(location = 0) out vec4 color; +void main() { + int v = int(gl_FragCoord.x) % 3; + color = vec4(values[v].color, values[v].weight0 + values[v].weight1); + color.xy += values[v].basis0 + values[v].basis1 + values[v].uv; + color.xy += matrix0[v] + matrix1[v] + matrix2[v] + matrix3[v]; + color.xy += grid0[v] + grid1[v] + grid2[v] + grid3[v] + packed[v] + ordinary; +} diff --git a/MoltenVKShaderConverter/Tests/pervertex-interface.frag b/MoltenVKShaderConverter/Tests/pervertex-interface.frag new file mode 100644 index 000000000..8c18d8570 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/pervertex-interface.frag @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +#extension GL_EXT_fragment_shader_barycentric : require +struct Nested { vec2 uv; float weights[2]; }; +layout(location = 0) pervertexEXT in Inputs { vec3 color; mat2 basis; Nested nested; } values[3]; +layout(location = 6) pervertexEXT in mat2 matrices[3][2]; +layout(location = 10) pervertexEXT in vec2 grid[3][2][2]; +layout(location = 14, component = 0) pervertexEXT in vec2 packed[3]; +layout(location = 14, component = 2) in vec2 ordinary; +layout(location = 0) out vec4 color; +void main() { + int v = int(gl_FragCoord.x) % 3; + color = vec4(values[v].color, values[v].nested.weights[v % 2]); + color.xy += values[v].basis[v % 2] + values[v].nested.uv; + color.xy += matrices[v][v % 2][0] + grid[v][1][v % 2] + packed[v] + ordinary; +} diff --git a/MoltenVKShaderConverter/Tests/pervertex-interface.vert b/MoltenVKShaderConverter/Tests/pervertex-interface.vert new file mode 100644 index 000000000..886d5b965 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/pervertex-interface.vert @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +// Export the consumer's scalar/vector leaves: matrix/array capture export is unsupported. +layout(location = 0) out Inputs { vec3 color; vec2 basis0; vec2 basis1; vec2 uv; float weight0; float weight1; } values; +layout(location = 6) out vec2 matrix0; +layout(location = 7) out vec2 matrix1; +layout(location = 8) out vec2 matrix2; +layout(location = 9) out vec2 matrix3; +layout(location = 10) out vec2 grid0; +layout(location = 11) out vec2 grid1; +layout(location = 12) out vec2 grid2; +layout(location = 13) out vec2 grid3; +layout(location = 14, component = 0) out vec2 packed; +layout(location = 14, component = 2) out vec2 ordinary; +void main() { + gl_Position = vec4(0, 0, 0, 1); + values.color = vec3(1); + values.basis0 = vec2(1, 0); + values.basis1 = vec2(0, 1); + values.uv = vec2(1); + values.weight0 = 1; + values.weight1 = 2; + matrix0 = matrix2 = vec2(1, 0); + matrix1 = matrix3 = vec2(0, 1); + grid0 = vec2(0, 0); + grid1 = vec2(0, 1); + grid2 = vec2(1, 0); + grid3 = vec2(1, 1); + packed = vec2(1); + ordinary = vec2(2); +} diff --git a/MoltenVKShaderConverter/Tests/portable-barycentric.frag b/MoltenVKShaderConverter/Tests/portable-barycentric.frag new file mode 100644 index 000000000..72806f966 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/portable-barycentric.frag @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +#extension GL_EXT_fragment_shader_barycentric : require +#ifndef PERSPECTIVE +#define PERSPECTIVE 1 +#endif +#ifndef LINEAR +#define LINEAR 1 +#endif +#ifndef CAPTURED +#define CAPTURED 0 +#endif +#if CAPTURED +layout(location = 0) pervertexEXT in vec4 value[3]; +#endif +layout(location = 0) out vec4 color; +void main() { + color = vec4(0); +#if PERSPECTIVE + color += vec4(gl_BaryCoordEXT, 1); +#endif +#if LINEAR + color += vec4(gl_BaryCoordNoPerspEXT, 1); +#endif +#if CAPTURED + color += value[0] + value[1] + value[2]; +#endif +} diff --git a/MoltenVKShaderConverter/Tests/portable-barycentric.vert b/MoltenVKShaderConverter/Tests/portable-barycentric.vert new file mode 100644 index 000000000..efb7a8158 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/portable-barycentric.vert @@ -0,0 +1,18 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +layout(location = 0) in vec4 position; +layout(location = 0) out vec4 value; +out gl_PerVertex { + vec4 gl_Position; +#ifdef POINTS + float gl_PointSize; +#endif +}; +void main() { + gl_Position = position; + value = position; +#ifdef POINTS + gl_PointSize = 3.0; +#endif +} diff --git a/MoltenVKShaderConverter/Tests/reflection-alignment.glsl b/MoltenVKShaderConverter/Tests/reflection-alignment.glsl new file mode 100644 index 000000000..e55885cbe --- /dev/null +++ b/MoltenVKShaderConverter/Tests/reflection-alignment.glsl @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Jean-Philippe Meunier +// SPDX-License-Identifier: Apache-2.0 +#version 450 +struct Nested { float scalar; vec4 wide; vec2 tail[20]; }; +#if VERTEX +layout(location = 0) out +#else +layout(location = 0) in +#endif +Block { +#if HEAD + float head; +#endif + Nested nested[2]; + vec4 end; +} values; +#if !VERTEX +layout(location = 0) out vec4 color; +#endif +void main() { +#if VERTEX +#if HEAD + values.head = 1; +#endif + for (int n = 0; n < 2; ++n) { + values.nested[n].scalar = 1; + values.nested[n].wide = vec4(1); + for (int i = 0; i < 20; ++i) { values.nested[n].tail[i] = vec2(1); } + } + values.end = vec4(1); +#else + color = values.end; +#if HEAD + color += values.head; +#endif + for (int n = 0; n < 2; ++n) { + color += values.nested[n].scalar + values.nested[n].wide; + for (int i = 0; i < 20; ++i) { color.xy += values.nested[n].tail[i]; } + } +#endif +} diff --git a/MoltenVKShaderConverter/Tests/run-per-vertex-input-tests.sh b/MoltenVKShaderConverter/Tests/run-per-vertex-input-tests.sh new file mode 100644 index 000000000..796c25a7f --- /dev/null +++ b/MoltenVKShaderConverter/Tests/run-per-vertex-input-tests.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +cross_source=${1:?SPIRV-Cross source directory required} +cross_build=${2:?SPIRV-Cross library build directory required} +ordinary_spirv=${3:?Ordinary fragment SPIR-V fixture required} +output=${4:-$(mktemp -d "${TMPDIR:-/tmp}/mvk-converter-pervertex.XXXXXX")} +mkdir -p "$output" +compiler_dir="$root/MoltenVKShaderConverter/MoltenVKShaderConverter" +for fixture in simple complex unsupported; do + glslangValidator -V --target-env vulkan1.1 "$cross_source/tests-other/msl_capture_layout_$fixture.vert" -o "$output/capture-$fixture.spv" +done +glslangValidator -V --target-env vulkan1.1 "$cross_source/tests-other/msl_capture_layout_consumer.frag" -o "$output/capture-consumer.spv" +glslangValidator -V --target-env vulkan1.1 "$cross_source/tests-other/msl_tese_compute.tese" -o "$output/capture-tese.spv" +spirv-as --target-env vulkan1.1 "$cross_source/tests-other/msl_tese_primitive_builtin_block.spvasm" -o "$output/capture-tese-block.spv" +spirv-val --target-env vulkan1.1 "$output/capture-tese-block.spv" +xcrun clang++ -arch "${MVK_TEST_ARCH:-$(uname -m)}" -std=c++17 -O0 -g -DMVK_EXCLUDE_SPIRV_TOOLS=1 -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross \ + -I "$compiler_dir" -I "$root/Common" -I "$cross_source" -I "$root/External/cereal/include" \ + "$test_dir/PerVertexInputTests.cpp" "$compiler_dir/SPIRVToMSLConverter.cpp" \ + "$compiler_dir/SPIRVSupport.cpp" "$compiler_dir/FileSupport.mm" \ + "$cross_build/libspirv-cross-msl.a" "$cross_build/libspirv-cross-glsl.a" "$cross_build/libspirv-cross-core.a" \ + -framework Foundation -o "$output/per-vertex-input-test" +"$output/per-vertex-input-test" "$cross_source/tests-other/msl_per_vertex_input.spv" "$ordinary_spirv" "$output" +for shader in "$output"/*.metal; do + xcrun metal -x metal -std=macos-metal2.4 -fsyntax-only -fno-modules "$shader" +done +echo "PASS: generated MSL syntax; output: $output" diff --git a/MoltenVKShaderConverter/Tests/run-pervertex-pipeline-interface-tests.sh b/MoltenVKShaderConverter/Tests/run-pervertex-pipeline-interface-tests.sh new file mode 100644 index 000000000..1b5f07c1e --- /dev/null +++ b/MoltenVKShaderConverter/Tests/run-pervertex-pipeline-interface-tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +cross_source=${1:?SPIRV-Cross source directory required} +cross_build=${2:?SPIRV-Cross library build directory required} +output=${3:?Output directory required} +arch=${4:-$(uname -m)} +mkdir -p "$output" +compiler_dir="$root/MoltenVKShaderConverter/MoltenVKShaderConverter" +for stage in vert frag; do + glslangValidator -V --target-env vulkan1.1 "$test_dir/pervertex-interface.$stage" -o "$output/interface.$stage.spv" + spirv-val --target-env vulkan1.1 "$output/interface.$stage.spv" +done +glslangValidator -V --target-env vulkan1.1 "$test_dir/pervertex-interface-linked.frag" -o "$output/interface-linked.frag.spv" +spirv-val --target-env vulkan1.1 "$output/interface-linked.frag.spv" +# Validate the matching VS/FS block interface as well as each standalone module. +glslangValidator -V --target-env vulkan1.1 -l "$test_dir/pervertex-interface.vert" "$test_dir/pervertex-interface-linked.frag" -o "$output/linked.spv" +xcrun clang++ -arch "$arch" -std=c++17 -O0 -g -DMVK_EXCLUDE_SPIRV_TOOLS=1 -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross \ + -I "$compiler_dir" -I "$root/Common" -I "$cross_source" -I "$root/External/cereal/include" \ + "$test_dir/PerVertexPipelineInterfaceTests.cpp" "$compiler_dir/SPIRVToMSLConverter.cpp" \ + "$compiler_dir/SPIRVSupport.cpp" "$compiler_dir/FileSupport.mm" \ + "$cross_build/libspirv-cross-reflect.a" "$cross_build/libspirv-cross-msl.a" \ + "$cross_build/libspirv-cross-glsl.a" "$cross_build/libspirv-cross-core.a" \ + -framework Foundation -o "$output/pervertex-pipeline-interface-test" +"$output/pervertex-pipeline-interface-test" "$output" +"$output/pervertex-pipeline-interface-test" "$output" interface-linked.frag.spv diff --git a/MoltenVKShaderConverter/Tests/run-pipeline-cache-per-vertex-tests.sh b/MoltenVKShaderConverter/Tests/run-pipeline-cache-per-vertex-tests.sh new file mode 100644 index 000000000..f4bd89817 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/run-pipeline-cache-per-vertex-tests.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +build="$root/build/portable-pervertex" +output=${1:-$(mktemp -d "$build/cache-roundtrip.XXXXXX")} +mkdir -p "$output" +output=$(cd "$output" && pwd) +library=${MVK_CACHE_LIBRARY:-"$build/cmake/moltenvk/MoltenVK/libMoltenVK.1.4.3.dylib"} +arch=${MVK_CACHE_ARCH:-x86_64} +cross_source=${MVK_CACHE_CROSS_SOURCE:-"$build/spirv-cross-bcc63e30"} + +# Copy the real definitions, not a test reimplementation. Mutations affect only generated copies. +python3 - "$root" "$output" <<'PY' +import pathlib, re, sys +root, output = map(pathlib.Path, sys.argv[1:]) +source = (root / 'MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm').read_text() +header = (root / 'MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h').read_text() +start = '#pragma mark Cereal archive definitions' +end = 'template\nvoid serialize(Archive & archive, MVKShaderModuleKey& k)' +assert source.count(start) == source.count(end) == 1, 'Serializer extraction boundaries changed' +serializers = source.split(start, 1)[1].split(end, 1)[0] +variants = {'baseline': (serializers, header)} +for name, field in [('omit-multiview', 'opt.multiview'), ('omit-layered', 'opt.multiview_layered_rendering'), ('omit-cfg', 'cfg.perVertexInputBuffer'), ('omit-used-flag', 'scr.needsPerVertexInputBuffer'), ('omit-result-layout', 'scr.capturedVertexLayout')]: + changed, count = re.subn(r',\s*' + re.escape(field) + r'\b', '', serializers) + assert count == 1, f'Missing or repeated production field: {field}' + variants[name] = (changed, header) +changed, count = re.subn(r',\s*cfg\.exportCapturedVertexLayout\b', '', serializers) +assert count == 1, 'Missing or repeated production field: cfg.exportCapturedVertexLayout' +variants['omit-export-layout'] = (changed, header) +for name, field in [('omit-user-fields', 'layout.components'), ('omit-builtins', 'layout.builtins')]: + changed, count = re.subn(r',\s*' + re.escape(field) + r'\b', '', header) + assert count == 1, f'Missing or repeated production field: {field}' + variants[name] = (serializers, changed) +for name, (definitions, declarations) in variants.items(): + directory = output / name + directory.mkdir(exist_ok=True) + (directory / 'PipelineCacheSerializers.inc').write_text(definitions) + (directory / 'SPIRVToMSLConverter.h').write_text(declarations) +PY +shasum -a 256 "$root/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm" "$root/MoltenVKShaderConverter/MoltenVKShaderConverter/SPIRVToMSLConverter.h" "$library" > "$output/sources.sha256" +for variant in baseline omit-multiview omit-layered omit-cfg omit-export-layout omit-used-flag omit-result-layout omit-user-fields omit-builtins; do + directory="$output/$variant" + xcrun clang++ -arch "$arch" -mmacosx-version-min=11.0 -std=c++17 -O0 -g \ + -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross \ + -I "$directory" -I "$cross_source" -I "$root/External/cereal/include" \ + "$test_dir/PipelineCachePerVertexTests.cpp" "$library" \ + -Wl,-rpath,"$(dirname "$library")" -o "$directory/cache-test" + status=0 + "$directory/cache-test" > "$directory/result.log" 2>&1 || status=$? + if [[ "$variant" == baseline ]]; then + cat "$directory/result.log" + [[ "$status" == 0 ]] || exit "$status" + else + if [[ "$status" != 2 ]] || ! rg -q '^FAIL: (cfg|scr)' "$directory/result.log"; then + cat "$directory/result.log" + echo "FAIL: $variant did not produce the expected semantic failure (exit $status)" >&2 + exit 1 + fi + printf 'PASS: %s detected: ' "$variant" + cat "$directory/result.log" + fi +done +echo "PASS: production cfg/scr round-trip and all eight omission controls; evidence: $output" diff --git a/MoltenVKShaderConverter/Tests/run-portable-barycentric-tests.sh b/MoltenVKShaderConverter/Tests/run-portable-barycentric-tests.sh new file mode 100644 index 000000000..5000a8973 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/run-portable-barycentric-tests.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +cross_source=${1:?SPIRV-Cross source required} +cross_build=${2:?SPIRV-Cross library build required} +output=${3:?Output directory required} +mkdir -p "$output" +output=$(cd "$output" && pwd) +glslangValidator -V --target-env vulkan1.1 "$test_dir/portable-barycentric.vert" -o "$output/producer.spv" +glslangValidator -V --target-env vulkan1.1 -DPOINTS=1 "$test_dir/portable-barycentric.vert" -o "$output/point-producer.spv" +for mask in 1 2 3; do + for captured in 0 1; do + glslangValidator -V --target-env vulkan1.1 -DPERSPECTIVE=$((mask & 1)) -DLINEAR=$((mask & 2)) -DCAPTURED="$captured" "$test_dir/portable-barycentric.frag" -o "$output/fragment-$mask-$captured.spv" + done +done +spirv-as --target-env vulkan1.1 "$cross_source/tests-other/msl_barycentric_explicit_only.spvasm" -o "$output/explicit-only.spv" +spirv-as --target-env vulkan1.1 "$cross_source/tests-other/msl_barycentric_copied_pointer.spvasm" -o "$output/copied-pointer.spv" +spirv-as --target-env vulkan1.1 "$test_dir/barycentric-active-block.spvasm" -o "$output/active-block.spv" +for locations in 15 31; do + glslangValidator -V --target-env vulkan1.1 -DLOCATIONS="$locations" -DDENSE=1 "$root/MoltenVK/Tests/pervertex-capacity.vert" -o "$output/dense-$locations.spv" +done +for fixture in "$output"/*.spv; do spirv-val --target-env vulkan1.1 "$fixture"; done +# Exercise the production serializers without linking/loading the Metal runtime. +python3 - "$root" "$output" <<'PY' +from pathlib import Path +import sys +root, output = map(Path, sys.argv[1:]) +source = (root / 'MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm').read_text() +start = '#pragma mark Cereal archive definitions' +end = 'template\nvoid serialize(Archive & archive, MVKShaderModuleKey& k)' +assert source.count(start) == source.count(end) == 1 +(output / 'PipelineCacheSerializers.inc').write_text(source.split(start, 1)[1].split(end, 1)[0]) +# Compile the actual pipeline selector, so ignoring isUsed also fails the regression. +selector = source.split('bool MVKGraphicsPipeline::initPerVertexInputPipeline(', 1)[1] +selector = selector.split('for (const auto& input : inputs) {', 1)[1].split('if (!usesPerVertex && !usesPortableBarycentrics())', 1)[0] +(output / 'PipelineBarycentricSelection.inc').write_text('for (const auto& input : inputs) {' + selector) +PY +compiler_dir="$root/MoltenVKShaderConverter/MoltenVKShaderConverter" +xcrun clang++ -arch arm64 -std=c++17 -O0 -g -DMVK_EXCLUDE_SPIRV_TOOLS=1 -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross \ + -I "$output" -I "$compiler_dir" -I "$root/Common" -I "$cross_source" -I "$root/External/cereal/include" \ + -I "$root/External/Vulkan-Headers/include" -I "$root/MoltenVK/MoltenVK/Commands" \ + "$test_dir/PortableBarycentricTests.cpp" "$compiler_dir/SPIRVToMSLConverter.cpp" "$compiler_dir/SPIRVSupport.cpp" "$compiler_dir/FileSupport.mm" \ + "$cross_build/libspirv-cross-msl.a" "$cross_build/libspirv-cross-glsl.a" "$cross_build/libspirv-cross-reflect.a" "$cross_build/libspirv-cross-core.a" \ + -framework Foundation -o "$output/portable-barycentric-test" +"$output/portable-barycentric-test" "$output" +for shader in "$output"/*.metal; do xcrun metal -x metal -std=macos-metal2.4 -fsyntax-only -fno-modules "$shader"; done +echo "PASS: all generated Metal 2.4 sources; no GPU device or command submission" diff --git a/MoltenVKShaderConverter/Tests/run-shader-interface-reflection-tests.sh b/MoltenVKShaderConverter/Tests/run-shader-interface-reflection-tests.sh new file mode 100644 index 000000000..891413838 --- /dev/null +++ b/MoltenVKShaderConverter/Tests/run-shader-interface-reflection-tests.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright (c) 2026 Jean-Philippe Meunier +# SPDX-License-Identifier: Apache-2.0 +set -euo pipefail +test_dir=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$test_dir/../.." && pwd) +cross_source=${1:?SPIRV-Cross source required} +cross_build=${2:?SPIRV-Cross library build required} +output=${3:?Output directory required} +mode=${4:-all} +mkdir -p "$output" +output=$(cd "$output" && pwd) +# Reuse the validated F1 module; place aliases BEFORE the first member, between +# the two members, and in helpers that only use InterpolateAt*. +python3 - "$test_dir" "$output" <<'PY' +from pathlib import Path +import sys +test_dir, output = map(Path, sys.argv[1:]) +source = (test_dir / 'barycentric-active-block.spvasm').read_text() +replacements = { + '%perspective_ptr = OpAccessChain %in3 %weights %u0': '%perspective_root = OpAccessChain %inblock %weights\n%perspective_ptr = OpAccessChain %in3 %perspective_root %u0', + '%linear_ptr = OpAccessChain %in3 %weights %u1': '%linear_copy = OpCopyObject %inblock %weights\n%linear_root = OpAccessChain %inblock %linear_copy\n%linear_root2 = OpInBoundsAccessChain %inblock %linear_root\n%linear_ptr = OpAccessChain %in3 %linear_root2 %u1', + '%both_n = OpAccessChain %in3 %weights %u1': '%both_root = OpAccessChain %inblock %weights\n%both_n = OpAccessChain %in3 %both_root %u1', + '%ordinary_ptr = OpAccessChain %in4 %weights %u2': '%ordinary_root = OpInBoundsAccessChain %inblock %weights\n%ordinary_ptr = OpAccessChain %in4 %ordinary_root %u2', + '%unused_entry = OpLabel': '%unused_entry = OpLabel\n%unused_root = OpAccessChain %inblock %weights', + '%explicit_perspective_root = OpCopyObject %inblock %weights': '%explicit_perspective_root = OpAccessChain %inblock %weights', + '%explicit_linear_root2 = OpCopyObject %inblock %explicit_linear_root': '%explicit_linear_alias_root = OpInBoundsAccessChain %inblock %explicit_linear_root\n%explicit_linear_root2 = OpCopyObject %inblock %explicit_linear_alias_root', +} +for old, new in replacements.items(): + assert source.count(old) == 1, old + source = source.replace(old, new) +(output / 'root-alias.spvasm').write_text(source) +PY +spirv-as --target-env vulkan1.1 "$test_dir/barycentric-active-block.spvasm" -o "$output/active-block.spv" +spirv-as --target-env vulkan1.1 "$output/root-alias.spvasm" -o "$output/root-alias.spv" +for stage in vert frag; do + vertex=0 + if [[ "$stage" == vert ]]; then vertex=1; fi + for head in 0 1; do + glslangValidator -V --target-env vulkan1.1 -S "$stage" -DVERTEX="$vertex" -DHEAD="$head" "$test_dir/reflection-alignment.glsl" -o "$output/alignment-$head.$stage.spv" + done +done +for fixture in "$output"/*.spv; do spirv-val --target-env vulkan1.1 "$fixture"; done +xcrun clang++ -arch arm64 -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer -DSPIRV_CROSS_NAMESPACE_OVERRIDE=MVK_spirv_cross \ + -I "$output" -I "$root/MoltenVKShaderConverter/MoltenVKShaderConverter" -I "$cross_source" \ + -I "$root/MoltenVK/MoltenVK/Utility" -I "$root/MoltenVK/MoltenVK/API" -I "$root/MoltenVK/include" -I "$root/Common" \ + "$test_dir/ShaderInterfaceReflectionTests.cpp" "$cross_build/libspirv-cross-reflect.a" "$cross_build/libspirv-cross-glsl.a" "$cross_build/libspirv-cross-core.a" \ + -o "$output/reflection-tests" +if [[ "$mode" == all ]]; then + "$output/reflection-tests" "$output" activity + "$output/reflection-tests" "$output" alignment +else + "$output/reflection-tests" "$output" "$mode" +fi