Repository navigation
Add experimental PerVertexKHR capture and replay (test entry point only) - #2858
Draft
carbonimax wants to merge 15 commits into
Draft
carbonimax wants to merge 15 commits into
carbonimax wants to merge 15 commits into
Conversation
The render state only kept the Metal primitive type, which cannot tell a triangle fan from a triangle list. It now also keeps the Vulkan topology, static from the pipeline or dynamic from vkCmdSetPrimitiveTopology, and the encoder state reads the effective one through getPrimitiveTopology(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The draw paths decided whether to convert a triangle fan to triangles from the topology the pipeline was created with, ignoring a primitive topology set with vkCmdSetPrimitiveTopology. A pipeline created with another triangle topology drew a dynamic triangle fan as a triangle list, and a pipeline created as a triangle fan drew a dynamic triangle list or strip through the fan conversion. The four draw paths now read the effective topology from the encoder state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without layered rendering, each Metal pass renders one enabled view. The first view of a pass was computed by walking view groups and decrementing the pass index inside each group, which skipped the gaps between groups: for view mask 0b101, pass 0 started at view 3 instead of view 0. The first view of pass N is now the lowest bit left after clearing N enabled views. mvkGetNextViewMaskGroup shifted a signed 1 by the running end position. With view 31 enabled the loop reached a shift by 32, which is undefined. The shifts are now unsigned and the loop stops once the mask is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a portable implementation of the PerVertexKHR fragment inputs and the BaryCoordKHR and BaryCoordNoPerspKHR builtins of VK_KHR_fragment_shader_barycentric for vertex and fragment pipelines on GPUs with MSL 2.4. A draw whose fragment shader reads PerVertexKHR inputs is captured by a vertex pass into a private scratch buffer, then replayed by a raster pass that gives each fragment the values of the vertices of its primitive. The capture and replay is a separate path of the physical device, which only an explicit test entry point, not part of the shipping targets, selects. Without it, VK_KHR_fragment_shader_barycentric and VK_NV_fragment_shader_barycentric keep their native path: they are advertised on GPUs with Metal barycentric coordinates, BaryCoordKHR and BaryCoordNoPerspKHR read Metal barycentric coordinates, and PerVertexKHR inputs are not supported. The same flag selects the native vertex_value path of Apple10 GPUs, the commitment of memoryless attachments and the deferral of immediate Metal command buffer prefill. In a test-enabled build, some implementation paths outside the claimed scope remain reachable: indirect draws, mesh shaders, the native path for Apple10 GPUs and multiview with more than one view. They are not qualified or claimed as supported. Tessellation admission has a separate test-only compile guard. Some changes apply whether or not the extension is enabled: Metal command buffer completion handlers go through one device-level path, device loss is published only after submitted encodings and handlers complete, vkQueueSubmit prepares every batch before it submits any, compatible tessellation pipelines keep their levels in float32, and the pipeline cache UUID and archive layout change. This builds on the preceding commits, which track the effective primitive topology and fix multiview pass grouping. It adds record-time topology tracking, which sizes the capture scratch buffer, and keeps the pipeline topology accessor that this sizing uses. The pinned SPIRV-Cross revision is the MSL capture and replay series this change depends on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Converter tests (MoltenVKShaderConverter/Tests) check the SPIR-V reflection, interface matching, pipeline cache serialization and MSL generation of the capture and replay shaders on the CPU. Vulkan tests (MoltenVK/Tests) build an experimental library with the test entry point that selects the capture and replay path, then run host-side checks (prefill, helper preflight, scratch sizing, draw sizes, primitive restart, topology, capture variants, memoryless attachments), a dynamic primitive topology oracle, the refusal of memoryless attachments split across capture and replay, and the CTS cases of the claimed scope through a loader shim, with their case lists and expected statuses. Six of the host-side checks run on the CPU only; the primitive restart and memoryless checks submit Metal work to the GPU with their GPU argument. The memoryless storage check also covers the native path, where an enabled barycentric extension keeps memoryless storage. PerVertexKHR-README.md describes the scope, the activation, the commands and the expected results, including the origin of the expected E failures and the limit of the F check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Query pools are retained when their queries are activated, and released once the completion handler has finished them. Two paths of the command buffer completion handling in this change never finish them: a failed continuation leaves no Metal command buffer, and a prefilled command buffer that is reset or freed before submission is abandoned and never enqueued. An abandoned command buffer can hold ordinary commands as well as captured draws. Release the pools on those paths too. A continuation can stop waiting during a device loss before Metal completes its work. The release stays safe because device loss is published only after submitted encodings and completion handlers complete, so valid Vulkan usage keeps each pool alive while its commands are pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test scripts create their default output directory under TMPDIR instead of a fixed path, and two code comments are reworded. No functional change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The converter test still expected the 156 bytes of padding of an earlier SPIRVToMSLConversionConfiguration layout. The production archive check in MVKPipeline.mm now uses 154, which matches the current layout. Test change only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cache immutable replay tables per command encoding pool, topology and provoking mode. Admit single-instance draws and complete instanced lists only; preserve per-draw tables for restart and incomplete lists. Retain superseded buffers until pool destruction so pending command buffers remain valid, and fall back if allocation fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fely Validate replay table lengths before CPU population and use memcpy for the float-to-integer bit representation. This preserves the existing error path and avoids strict-aliasing undefined behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the upstream depth clip configuration when compiling vertex capture. The pinned SPIRV-Cross replay API explicitly rejects depth clip emulation; preserving this requirement refuses unsupported replay pipelines instead of silently dropping the transform. Ordinary vertex pipelines retain the upstream configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover rejection of unsupported depth clip replay and preservation of the upstream depth clip result flag in pipeline archives. Document shared table admission, lifetime and the sanitizer test command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
carbonimax
force-pushed
the
pervertex-experimental
branch
from
October 9, 2026 12:20
4dcbc35 to
645bcf2
Compare
Each PerVertexKHR capture ends the render encoder, runs its compute capture and restarts the render pass, which costs one encoder switch per captured draw. With MVK_CONFIG_PERVERTEX_PREPASS=1, when a subpass opens, its indexed PerVertexKHR captures are encoded ahead of time in a single compute encoder, replaying the state commands they depend on and restoring the encoder state afterwards, push descriptors included. The prepass stops at barriers, subpass changes, copies, queries, secondary command buffers, and non-indexed, tessellation and indirect draws. Without the variable, or with any other value, encoding is unchanged. PerVertexPrepassStateTests.mm checks the state save and restore on the CPU; the GPU path has no test yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scratch buffers of captured draws are allocated per draw and each one commits the residency set on its own. Two opt-in levers reduce that overhead; both are off by default and encoding is unchanged without them: - MVK_CONFIG_PERVERTEX_BATCH_RESIDENCY=1: the scratch allocations of a Metal command buffer join the residency set together, with one commit at submission and one at completion; - MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1: scratch buffers of direct draws come from free lists keyed by storage mode and power-of-two size, stay resident and are recycled on their last reference. The pool keeps its high-water mark until the device is destroyed. PerVertexScratchPoolTests.mm checks size classes, storage modes, return on the last reference and single residency per created buffer on the CPU, from the allocation calls of a fake device. The test device of PerVertexHelperPreflightTests.mm gains the pool accessor, without a pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vkCmdDrawMeshTasksIndirectEXT is refused, so a client has to read the grid back on the CPU, draining the GPU queue, before it can call vkCmdDrawMeshTasksEXT. With MVK_CONFIG_MESH_INDIRECT_GPU=1, a call with drawCount <= 1 records an MVKCmdDrawMeshTasksIndirect command and Metal reads the three words of VkDrawMeshTasksIndirectCommandEXT itself through drawMeshThreadgroupsWithIndirectBuffer. drawCount above 1 stays refused, and without the variable the call is refused as before. The command has nothing to capture, so the PerVertexKHR prepass continues past it. Only native mesh pipelines are handled. Known gap: the direct draw refuses a grid at or above the Metal limit at record time; an indirect grid cannot be checked then, and Metal draws nothing for it without reporting an error. No test covers this path yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, experimental; capture and replay are selected only by a test entry point. This change implements portable
PerVertexKHRfragment inputs andBaryCoordKHR/BaryCoordNoPerspKHRusing captured vertex outputs. It does not claim extension conformance. The shipping barycentric extension path and advertisement remain native.The branch has been rebased onto MoltenVK main
67b2682699d7903606b97a0392061d85d27d49e6. The additional changes are limited to reusable replay tables, replay scratch checks, safe copying of tessellation-level bits, and tests for the rebase. No new runtime configuration option is introduced by those changes. Three later commits add opt-in performance changes; they are off by default and described in Opt-in performance changes below.New replay-table optimization
The command encoding pool caches immutable occurrence, primitive-index and corner tables by topology and provoking mode. For single-instance draws, a larger table contains each smaller draw's table as a prefix. Complete instanced point/line/triangle lists can use the same representation. Primitive restart, instanced strips/fans and incomplete instanced lists keep their per-draw tables.
Tables grow geometrically. Superseded buffers remain resident until pool destruction because pending command buffers may still reference them. Metal buffer-size limits are checked, and allocation failure falls back to per-draw population. This removes repeated CPU population for admitted draws; no application frame-rate or timing claim is made here. The tradeoff is retained host-visible table memory until the pool is destroyed.
Replay scratch lengths are now checked before CPU population. The test tessellation path copies a float's bits with
memcpyinstead of type punning. These are safety changes, not claimed performance gains.Opt-in performance changes
The first two of the last three commits reduce encoder and allocation work around captured draws; the third one removes a CPU readback on the experimental mesh path. These changes are disabled when the variables are unset; use
=1to opt in. None of them changes the captured ABI, the replay shaders or the SPIRV-Cross pin.MVK_CONFIG_PERVERTEX_PREPASS=1). Each capture otherwise ends the render encoder, runs its compute capture and restarts the render pass. With the variable, when an inline, single-view subpass opens, its indexed captured draws are encoded ahead of time in one compute encoder. The state commands they depend on are replayed there, and the encoder state, push descriptors included, is restored before the render pass starts. The window stops at barriers, subpass changes, copies, queries, secondary command buffers, non-indexed PerVertex captures, PerVertex TES captures and conventional indirect draws. Native mesh draws have no capture and can be crossed.MVK_CONFIG_PERVERTEX_SCRATCH_POOL=1,MVK_CONFIG_PERVERTEX_BATCH_RESIDENCY=1, independent). The pool serves the scratch buffers of direct draws from free lists keyed by storage mode and power-of-two size; pooled buffers stay resident and return to the pool on their last reference. The tradeoff is that the pool keeps its high-water mark until device destruction. Batched residency adds the scratch allocations of a Metal command buffer to the residency set together, with one commit at submission and one at completion, instead of one commit per buffer.MVK_CONFIG_MESH_INDIRECT_GPU=1).vkCmdDrawMeshTasksIndirectEXTwithdrawCount == 1records a command that passes the indirect buffer todrawMeshThreadgroupsWithIndirectBuffer, so the client does not have to read the grid back on the CPU.drawCount == 0is a no-op, not a command.drawCount > 1stays refused, as does every call without the variable. This only concerns native mesh pipelines of the experimental mesh path. Known gap: the direct draw refuses a grid at or above the Metal limit at record time, but an indirect grid cannot be checked then, and Metal draws nothing for it without reporting an error.New tests:
PerVertexPrepassStateTests.mmchecks the prepass state save and restore against the built library, in place and with a grown push-descriptor set.PerVertexScratchPoolTests.mmchecks size classes, storage modes, return on the last reference and single residency per created buffer, from the allocation calls of a fake device. Both run on the CPU. The GPU paths of the prepass, the pool and batched residency are covered only by running the existing GPU suites with the three variables set (table below), which does not prove each path was taken. The indirect mesh command has no test. No timing or frame-rate claim is made for these commits: there is no public, reproducible benchmark for them yet.Rebase details
The rebase retains main's depth-clip result field and serialization, pipeline-cache matching fixes, and vertex depth-clip configuration. The pinned replay compiler explicitly rejects depth-clip emulation; unsupported replay pipelines are refused rather than silently losing the transform. Capture/replay fields are retained alongside main's fields. Archive coverage was checked against the actual serializers.
The first three commits still originate from #2851 (primitive topology / triangle fans) and #2852 (multiview pass grouping). They should be reviewed there and will drop out when those changes merge. This branch retains the pipeline topology accessor needed for capture sizing.
The experimental mesh implementation remains separate from #2842. It is a narrow test-only path, not a mesh-shader deliverable of this pull request. The previous textual overlap survey with other proposals is historical; it is not a current semantic compatibility claim.
SPIRV-Cross dependency and building
This requires KhronosGroup/SPIRV-Cross#2699, pinned at
81739edf516c2bef043ac61f510b4bd78e126a22inExternalRevisions/SPIRV-Cross_repo_revision. That revision already contains main's pinaa217aeb6c9f0ace7a0ab233b28807edf45eb165and its depth-clip APIs. The additional replay-table optimization does not change the captured ABI or require a further SPIRV-Cross change.The series includes the changes also proposed in #2690, #2691, #2695, #2696, #2697 and #2698. Those are code overlaps in the pinned series, not six separately fetched dependencies. Its temporary mesh
LocalSizeIdchange overlaps #2686; #2699 describes the intended removal before merging. Nativevertex_valuebehavior on newer GPUs still needs separate review and testing.Until the series is merged, fetch the pull-request ref before fetching dependencies from a clean checkout:
The standard dependency fetch does not fetch pull-request refs. A clean build or CI may therefore fail to resolve the pin until the dependency is merged or the ref is explicitly fetched. A successful local build does not resolve that CI limitation.
Activation and scope
MoltenVK/Tests/PerVertexTES/EnableTestDevice.mmis excluded from shipping CMake/Xcode targets. A test build links it, and the test application callsmvkEnableTESFixtureDevice(VkPhysicalDevice)before device creation. This selects the portable capture/replay path on devices with Metal barycentric support.MoltenVK/Tests/PerVertexKHR-README.mddocuments the runners and scope.The portable tests cover vertex/fragment SPIR-V pipelines, typed scalar/vector/composite inputs, barycentrics, points/lines/triangles including fans, dynamic topology within a topology class, and direct/indexed draws with one sample and the first provoking vertex. Code also exists for paths outside that tested scope: indirect draws, tessellation, mesh, several views, multisampling, multiple instances and newer native per-vertex hardware. They are not all gated or qualified in the same way. Tessellation admission has a separate test compile guard; mesh has a separate test device entry point.
The implementation still refuses valid usages including unsupported depth-clip replay, some memoryless attachment splits, line polygon mode, dynamic rasterizer discard, multiple-view capture, unsupported resolve modes, and interfaces beyond the replay buffer/varying budgets. Whether every such refusal is allowed by Vulkan remains an open limitation.
Default behavior requiring review
Capture/replay remains opt-in, but the original proposal also changes general runtime behavior:
MTLSharedEvent.The synthetic tests below exercise several of these paths, but do not qualify every default-path change or all Vulkan applications.
Current local validation
Tested on Apple M4 Pro, macOS 27.2. Release test libraries were built for both x86_64 and arm64 with
-O2, assertions retained, deployment target macOS 11, and private Metal APIs off. The test adapter was linked only into these validation libraries. SPIRV-Cross was built from the unmodified pin above.The opt-in commits were checked on the same machine, with the same Release
-O2configuration for x86_64 and arm64 and the unmodified SPIRV-Cross pin above:Batched residency has no dedicated test; pooled direct-draw allocations bypass it.
The five ordinary-list failures are retained expectations: two restart-disabled triangle-fan cases and three mesh cases affected by the experimental shim. They are not presented as passing tests. There were no CTS process crashes. These results qualify the tested synthetic scope, not conformance, arbitrary applications, native Intel hardware, iOS, newer native per-vertex hardware, or public performance improvements. Earlier M1/M2 reports belong to prior revisions and are not fresh qualification of this rebase.
To reproduce the portable checks, fetch the pinned dependencies, build a test library using
MoltenVK/Tests/PerVertexTES/build-library.sh <output> OFF ON, and use the commands and CTS lists inPerVertexKHR-README.md. Release-O2can be selected by reconfiguring its generated driver withCMAKE_BUILD_TYPE=ReleaseandCMAKE_{C,CXX,OBJC,OBJCXX}_FLAGS_RELEASE=-O2before rebuilding. The CTS binary is VK-GL-CTS 1.4.6.2. This testing does not require an external application.Follow-up work
Capture scheduling and scratch-pool/residency changes are now included as opt-in commits (see above); they still need GPU tests that prove each path is taken, and reproducible public evidence before any default change is proposed. Deduplicated indexed capture and device-specific threadgroup tuning remain absent. No logging or application-specific tuning is added here.
AI assistance
The original contribution was developed with Claude Code assistance, as described in the prior revision. This local rebase, selection, additional tests and draft update were prepared with Codex assistance. The three opt-in commits were developed and checked with Claude Code assistance. Existing license headers and authors are preserved.