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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user