perf(av-live-body): use LowLevelMesh for verts

Each frame the Python sender pushes ~125 KB of vertices per person.
The previous MeshRenderer rebuilt the MeshResource from scratch
every frame, costing a copy + driver upload for each entity.

LowLevelMesh (RealityKit, macOS 15+) keeps a persistent GPU vertex
buffer that we mutate in place via withUnsafeMutableBytes. The
faces buffer is written once at entity creation, and we tag each
ModelEntity with a PidComponent so updateMeshVertices can look up
its LowLevelMesh without touching the entities-by-pid dictionary
from the render path.

Package.swift bumped to swift-tools 6.0 / macOS 15 for the
LowLevelMesh.parts.replaceAll API ; the target stays in Swift 5
language mode so the existing closures don't need Sendable
annotations.
This commit is contained in:
L'électron rare
2026-05-13 11:23:35 +02:00
parent 7378b879fa
commit 500abb937f
2 changed files with 85 additions and 9 deletions
+5 -2
View File
@@ -1,15 +1,18 @@
// swift-tools-version:5.9
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "AVLiveBody",
platforms: [.macOS(.v14)],
platforms: [.macOS(.v15)],
targets: [
.executableTarget(
name: "AVLiveBody",
path: "Sources/AVLiveBody",
resources: [
.copy("Resources/smplx_faces.bin"),
],
swiftSettings: [
.swiftLanguageMode(.v5),
]
)
]
@@ -47,28 +47,45 @@ final class MeshRenderer: ObservableObject {
let receivedPids = Set(persons.map { $0.pid })
for (pid, _) in personEntities where !receivedPids.contains(pid) {
personEntities.removeValue(forKey: pid)
lowLevelMeshes.removeValue(forKey: pid)
}
for p in persons {
let entity = personEntities[p.pid] ?? makeEntity(pid: p.pid)
let entity: ModelEntity
if let existing = personEntities[p.pid] {
entity = existing
} else {
entity = makeEntity(pid: p.pid)
entity.components.set(PidComponent(pid: p.pid))
}
updateMeshVertices(entity, vertices: p.vertices)
entity.transform.translation = p.translation
personEntities[p.pid] = entity
}
}
private var lowLevelMeshes: [Int: LowLevelMesh] = [:]
private func makeEntity(pid: Int) -> ModelEntity {
let material = SimpleMaterial(color: colorForPid(pid),
isMetallic: false)
let entity = ModelEntity()
let initial = Array(repeating: SIMD3<Float>(0, 0, 0), count: 10475)
entity.model = ModelComponent(
mesh: makeMesh(vertices: initial),
materials: [material]
)
if let mesh = createLowLevelMesh(vertices: initial) {
lowLevelMeshes[pid] = mesh
if let resource = try? MeshResource(from: mesh) {
entity.model = ModelComponent(mesh: resource,
materials: [material])
}
} else {
entity.model = ModelComponent(
mesh: fallbackMesh(vertices: initial),
materials: [material]
)
}
return entity
}
private func makeMesh(vertices: [SIMD3<Float>]) -> MeshResource {
private func fallbackMesh(vertices: [SIMD3<Float>]) -> MeshResource {
var desc = MeshDescriptor(name: "smplx")
desc.positions = MeshBuffer(vertices)
desc.primitives = .triangles(faces)
@@ -78,9 +95,59 @@ final class MeshRenderer: ObservableObject {
return MeshResource.generateBox(size: 0.1)
}
/// Update les positions des vertices in-place via LowLevelMesh
/// (macOS 14+) ; fallback rebuild du MeshResource si indisponible.
private func updateMeshVertices(_ entity: ModelEntity,
vertices: [SIMD3<Float>]) {
entity.model?.mesh = makeMesh(vertices: vertices)
let pid = entity.components[PidComponent.self]?.pid ?? -1
if let mesh = lowLevelMeshes[pid] {
mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawPtr in
let dst = rawPtr.bindMemory(to: SIMD3<Float>.self)
let n = min(dst.count, vertices.count)
for i in 0..<n { dst[i] = vertices[i] }
}
return
}
entity.model?.mesh = fallbackMesh(vertices: vertices)
}
private func createLowLevelMesh(vertices: [SIMD3<Float>]) -> LowLevelMesh? {
let vertexAttr = LowLevelMesh.Attribute(
semantic: .position, format: .float3, offset: 0)
let vertexLayout = LowLevelMesh.Layout(
bufferIndex: 0,
bufferStride: MemoryLayout<SIMD3<Float>>.stride)
let desc = LowLevelMesh.Descriptor(
vertexCapacity: vertices.count,
vertexAttributes: [vertexAttr],
vertexLayouts: [vertexLayout],
indexCapacity: faces.count,
indexType: .uint32
)
guard let mesh = try? LowLevelMesh(descriptor: desc) else {
return nil
}
mesh.withUnsafeMutableBytes(bufferIndex: 0) { ptr in
let dst = ptr.bindMemory(to: SIMD3<Float>.self)
for (i, v) in vertices.enumerated() where i < dst.count {
dst[i] = v
}
}
mesh.withUnsafeMutableIndices { ptr in
let dst = ptr.bindMemory(to: UInt32.self)
for (i, f) in faces.enumerated() where i < dst.count {
dst[i] = f
}
}
let bounds = BoundingBox(min: SIMD3(-2, -2, -2),
max: SIMD3(2, 2, 2))
mesh.parts.replaceAll([
.init(indexCount: faces.count,
topology: .triangle,
materialIndex: 0,
bounds: bounds)
])
return mesh
}
private func colorForPid(_ pid: Int) -> NSColor {
@@ -99,3 +166,9 @@ struct SMPLXPersonData {
let translation: SIMD3<Float>
let vertices: [SIMD3<Float>]
}
/// Component permettant de retrouver le pid d'une ModelEntity sans
/// passer par un dictionnaire externe.
struct PidComponent: Component {
let pid: Int
}