arkavo-org / arkavo-org/VRMMetalKit
GLTFMetalKit: persistent triple-buffered morph vertex buffer pool
- Dominant language
- Swift
- Stars
- 6
- Forks
- 2
- Avg merge
- 18h 51m
- Merged PRs (30d)
- 26
Description
## Context
Reviewer feedback on PR #241 (Major #1):
> Each call to `drawCalls(animationIndex:time:)` that touches a morphed primitive allocates a fresh `MTLBuffer` via `device.makeBuffer(bytes:length:options:[])`. For canonical Khronos `AnimatedMorphCube` (24 vertices) this is a non-issue. For a typical face blendshape rig (8–30k vertices, 30–80 targets, 60 fps), this becomes a significant per-frame allocation and copy that will fragment the Metal heap and stall on the autorelease pool.
Cheap intermediate fix before the GPU compute kernel ports (issue #244). Tracked here so it's not lost.
## What's needed
- Cache `MTLBuffer`s per (primitiveIndex, frame-in-flight) so the rebuild path reuses storage instead of allocating each call.
- Triple-buffer to allow the GPU to consume frame N's morphed vertices while the CPU builds frame N+2's. Standard Metal in-flight pattern.
- Add a "weights unchanged since last frame" guard so static rest-pose morphed primitives skip the blend + upload entirely.
- Storage location: probably a new `GLTFMorphBufferPool` class owned by the asset (mutable state), or threaded explicitly through the `drawCalls(...)` call.
## Acceptance
- A synthetic stress test (e.g. 30k verts × 16 morph targets at 60 fps for 5 seconds) doesn't show heap-fragmentation warnings or autorelease-pool stalls in Instruments.
- Memory footprint is bounded (~3× per-primitive vertex buffer size, not unbounded growth).
- `AnimatedMorphCube` continues to render identically.
## Relationship to #244
Independent. The GPU compute kernel (#244) is a separate axis: it moves the *math* to GPU. This issue is about *buffer lifecycle* on the CPU/upload side. Doing both is the canonical face-blendshape path; either alone is an improvement.
## Estimate
~3 hours. The triple-buffer pattern is well-established in Metal sample code; the bulk is wiring the lifecycle correctly without breaking the value-type-ish `GLTFAsset` API.
Contributor guide
Research direction
Start at GLTFAsset's drawCalls(animationIndex:time:) morphed-primitive rebuild path and review PR #241's allocation behavior. Trace how asset state can own or receive a GLTFMorphBufferPool, then use the AnimatedMorphCube and the synthetic 30k-vertex stress case to verify bounded triple-buffered storage, skipped unchanged weights, and identical rendering.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- computer-graphics, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100