Skip to content

Commit 13b41dc

Browse files
cdtwiggmeta-codesync[bot]
authored andcommitted
Add batched CPU and CUDA mesh rasterization (#1697)
Summary: Pull Request resolved: #1697 Add batched mesh rasterization for animation sequences that share topology, camera, and model transform. CUDA renders the full batch with one two-dimensional grid for each initialization, rasterization, and resolve stage, and uses smaller per-triangle blocks for the common small-triangle workload. CPU batches use `dispenso::parallel_for` after releasing the Python GIL. Extend `pymomentum.torch.renderer.rasterize_mesh` to accept positions shaped `[batch, vertices, 3]`, return padded `[batch, height, width]` outputs, and preserve the existing unbatched API. Add a benchmark covering CPU, native CUDA batching, sequential CUDA calls, and stream-parallel calls. For a 3,658-triangle character at 256x256 and batch size 128, native CUDA batching reduced total render time from 5.47 ms to 0.89 ms. Dispenso CPU batching reached 12.3k frames/s compared with 1.4k frames/s for a single-frame loop. Reviewed By: yutingye Differential Revision: D125535570 fbshipit-source-id: a563740a213f734ae6e39848379391b0c27bacfb
1 parent e4305a2 commit 13b41dc

12 files changed

Lines changed: 1042 additions & 65 deletions

‎momentum/rasterizer/cuda_rasterizer.cpp‎

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,42 @@ detail::CudaCamera makeCudaCamera(const Camera& camera, const Eigen::Matrix4f& m
7979

8080
} // namespace
8181

82-
void rasterizeMeshCuda(
82+
void rasterizeMeshesCuda(
8383
CudaRasterizerInputs inputs,
84+
size_t batchSize,
8485
const Camera& camera,
8586
const Eigen::Matrix4f& modelMatrix,
8687
float nearClip,
87-
CudaRasterizerBuffers buffers,
88+
CudaBatchedRasterizerBuffers buffers,
8889
bool backfaceCulling,
8990
float depthOffset,
9091
cudaStream_t stream) {
9192
const auto* positionsModel = inputs.positionsModel;
9293
const size_t numVertices = inputs.numVertices;
9394
const auto* triangles = inputs.triangles;
9495
const size_t numTriangles = inputs.numTriangles;
96+
MT_THROW_IF(batchSize == 0 || batchSize > 65535, "CUDA rasterizer batch size is out of range");
9597
MT_THROW_IF(nearClip <= 0.0f, "Near clip must be positive");
9698
MT_THROW_IF(
9799
camera.imageWidth() <= 0 || camera.imageHeight() <= 0, "Camera image must be non-empty");
98100
MT_THROW_IF(buffers.rowStride < camera.imageWidth(), "CUDA rasterizer row stride is too small");
99-
MT_THROW_IF(buffers.visibilityBuffer == nullptr, "CUDA visibility buffer is null");
100-
MT_THROW_IF(buffers.zBuffer == nullptr, "CUDA z buffer is null");
101-
MT_THROW_IF(buffers.triangleIndexBuffer == nullptr, "CUDA triangle-index buffer is null");
102101
const auto imageHeight = static_cast<size_t>(camera.imageHeight());
103102
const auto rowStride = static_cast<size_t>(buffers.rowStride);
104103
MT_THROW_IF(
105104
rowStride > std::numeric_limits<size_t>::max() / imageHeight,
106105
"CUDA rasterizer output size overflows");
107-
const size_t requiredBufferSize = imageHeight * rowStride;
106+
const size_t imageRowsSize = imageHeight * rowStride;
107+
MT_THROW_IF(
108+
buffers.imageStride < 0 || static_cast<size_t>(buffers.imageStride) < imageRowsSize,
109+
"CUDA rasterizer image stride is too small");
110+
const auto imageStride = static_cast<size_t>(buffers.imageStride);
111+
MT_THROW_IF(
112+
batchSize - 1 > (std::numeric_limits<size_t>::max() - imageRowsSize) / imageStride,
113+
"CUDA rasterizer output size overflows");
114+
const size_t requiredBufferSize = (batchSize - 1) * imageStride + imageRowsSize;
115+
MT_THROW_IF(buffers.visibilityBuffer == nullptr, "CUDA visibility buffer is null");
116+
MT_THROW_IF(buffers.zBuffer == nullptr, "CUDA z buffer is null");
117+
MT_THROW_IF(buffers.triangleIndexBuffer == nullptr, "CUDA triangle-index buffer is null");
108118
MT_THROW_IF(
109119
buffers.visibilityBufferSize < requiredBufferSize,
110120
"CUDA visibility buffer has {} elements; at least {} are required",
@@ -124,7 +134,12 @@ void rasterizeMeshCuda(
124134
numVertices > std::numeric_limits<size_t>::max() / 3 ||
125135
numTriangles > std::numeric_limits<size_t>::max() / 3,
126136
"CUDA mesh input size overflows");
127-
const size_t requiredPositionElements = 3 * numVertices;
137+
const size_t positionElementsPerMesh = 3 * numVertices;
138+
MT_THROW_IF(
139+
positionElementsPerMesh > 0 &&
140+
batchSize > std::numeric_limits<size_t>::max() / positionElementsPerMesh,
141+
"CUDA mesh input size overflows");
142+
const size_t requiredPositionElements = batchSize * positionElementsPerMesh;
128143
const size_t requiredTriangleElements = 3 * numTriangles;
129144
MT_THROW_IF(numVertices > 0 && positionsModel == nullptr, "CUDA positions buffer is null");
130145
MT_THROW_IF(numTriangles > 0 && triangles == nullptr, "CUDA triangle buffer is null");
@@ -166,8 +181,9 @@ void rasterizeMeshCuda(
166181
device);
167182

168183
throwIfCudaError(
169-
detail::launchCudaRasterizeMesh(
184+
detail::launchCudaRasterizeMeshes(
170185
positionsModel,
186+
batchSize,
171187
numVertices,
172188
triangles,
173189
numTriangles,
@@ -176,11 +192,48 @@ void rasterizeMeshCuda(
176192
depthOffset,
177193
backfaceCulling,
178194
buffers.rowStride,
195+
buffers.imageStride,
179196
buffers.visibilityBuffer,
180197
buffers.zBuffer,
181198
buffers.triangleIndexBuffer,
182199
stream),
183-
"CUDA rasterizer launch");
200+
"Batched CUDA rasterizer launch");
201+
}
202+
203+
void rasterizeMeshCuda(
204+
CudaRasterizerInputs inputs,
205+
const Camera& camera,
206+
const Eigen::Matrix4f& modelMatrix,
207+
float nearClip,
208+
CudaRasterizerBuffers buffers,
209+
bool backfaceCulling,
210+
float depthOffset,
211+
cudaStream_t stream) {
212+
const int64_t imageHeight = camera.imageHeight();
213+
MT_THROW_IF(imageHeight <= 0 || buffers.rowStride < 0, "CUDA rasterizer output size is invalid");
214+
MT_THROW_IF(
215+
buffers.rowStride > std::numeric_limits<int64_t>::max() / imageHeight,
216+
"CUDA rasterizer output size overflows");
217+
const int64_t imageStride = imageHeight * buffers.rowStride;
218+
rasterizeMeshesCuda(
219+
inputs,
220+
1,
221+
camera,
222+
modelMatrix,
223+
nearClip,
224+
CudaBatchedRasterizerBuffers{
225+
.visibilityBuffer = buffers.visibilityBuffer,
226+
.visibilityBufferSize = buffers.visibilityBufferSize,
227+
.zBuffer = buffers.zBuffer,
228+
.zBufferSize = buffers.zBufferSize,
229+
.triangleIndexBuffer = buffers.triangleIndexBuffer,
230+
.triangleIndexBufferSize = buffers.triangleIndexBufferSize,
231+
.rowStride = buffers.rowStride,
232+
.imageStride = imageStride,
233+
},
234+
backfaceCulling,
235+
depthOffset,
236+
stream);
184237
}
185238

186239
} // namespace momentum::rasterizer

‎momentum/rasterizer/cuda_rasterizer.cu‎

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace momentum::rasterizer::detail {
1313
namespace {
1414

15+
constexpr int32_t kRasterThreadsPerBlock = 32;
16+
1517
struct TriangleSetup {
1618
float u[3];
1719
float v[3];
@@ -112,6 +114,7 @@ __global__ void initializeVisibilityKernel(
112114
int32_t imageWidth,
113115
int32_t imageHeight,
114116
int64_t rowStride,
117+
int64_t imageStride,
115118
const float* zBuffer,
116119
uint64_t* visibilityBuffer) {
117120
const int64_t pixel = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
@@ -120,9 +123,10 @@ __global__ void initializeVisibilityKernel(
120123
return;
121124
}
122125

126+
const int64_t batchOffset = static_cast<int64_t>(blockIdx.y) * imageStride;
123127
const int32_t y = static_cast<int32_t>(pixel / imageWidth);
124128
const int32_t x = static_cast<int32_t>(pixel - static_cast<int64_t>(y) * imageWidth);
125-
const int64_t offset = static_cast<int64_t>(y) * rowStride + x;
129+
const int64_t offset = batchOffset + static_cast<int64_t>(y) * rowStride + x;
126130
const float existingDepth = zBuffer[offset];
127131
visibilityBuffer[offset] =
128132
isnan(existingDepth) ? 0 : static_cast<uint64_t>(floatToOrderedUint(existingDepth)) << 32;
@@ -138,8 +142,12 @@ __global__ void rasterizeVisibilityKernel(
138142
float depthOffset,
139143
bool backfaceCulling,
140144
int64_t rowStride,
145+
int64_t imageStride,
141146
uint64_t* visibilityBuffer) {
147+
const size_t batchIndex = blockIdx.y;
142148
const size_t triangleIndex = blockIdx.x;
149+
positionsModel += 3 * batchIndex * numVertices;
150+
visibilityBuffer += static_cast<int64_t>(batchIndex) * imageStride;
143151
if (triangleIndex >= numTriangles) {
144152
return;
145153
}
@@ -228,6 +236,7 @@ __global__ void resolveVisibilityKernel(
228236
int32_t imageWidth,
229237
int32_t imageHeight,
230238
int64_t rowStride,
239+
int64_t imageStride,
231240
const uint64_t* visibilityBuffer,
232241
float* zBuffer,
233242
int32_t* triangleIndexBuffer) {
@@ -237,9 +246,10 @@ __global__ void resolveVisibilityKernel(
237246
return;
238247
}
239248

249+
const int64_t batchOffset = static_cast<int64_t>(blockIdx.y) * imageStride;
240250
const int32_t y = static_cast<int32_t>(pixel / imageWidth);
241251
const int32_t x = static_cast<int32_t>(pixel - static_cast<int64_t>(y) * imageWidth);
242-
const int64_t offset = static_cast<int64_t>(y) * rowStride + x;
252+
const int64_t offset = batchOffset + static_cast<int64_t>(y) * rowStride + x;
243253
const uint64_t key = visibilityBuffer[offset];
244254
const uint32_t triangleKey = static_cast<uint32_t>(key);
245255
if (triangleKey == 0) {
@@ -252,8 +262,9 @@ __global__ void resolveVisibilityKernel(
252262

253263
} // namespace
254264

255-
cudaError_t launchCudaRasterizeMesh(
265+
cudaError_t launchCudaRasterizeMeshes(
256266
const float* positionsModel,
267+
size_t batchSize,
257268
size_t numVertices,
258269
const int32_t* triangles,
259270
size_t numTriangles,
@@ -262,24 +273,30 @@ cudaError_t launchCudaRasterizeMesh(
262273
float depthOffset,
263274
bool backfaceCulling,
264275
int64_t rowStride,
276+
int64_t imageStride,
265277
uint64_t* visibilityBuffer,
266278
float* zBuffer,
267279
int32_t* triangleIndexBuffer,
268280
cudaStream_t stream) {
269281
const int64_t numPixels = static_cast<int64_t>(camera.imageWidth) * camera.imageHeight;
270282
const int32_t pixelBlocks = static_cast<int32_t>(
271283
(numPixels + kCudaRasterizerThreadsPerBlock - 1) / kCudaRasterizerThreadsPerBlock);
272-
273-
initializeVisibilityKernel<<<pixelBlocks, kCudaRasterizerThreadsPerBlock, 0, stream>>>(
274-
camera.imageWidth, camera.imageHeight, rowStride, zBuffer, visibilityBuffer);
284+
const uint32_t batchGridSize = static_cast<uint32_t>(batchSize);
285+
286+
initializeVisibilityKernel<<<
287+
dim3(static_cast<uint32_t>(pixelBlocks), batchGridSize),
288+
kCudaRasterizerThreadsPerBlock,
289+
0,
290+
stream>>>(
291+
camera.imageWidth, camera.imageHeight, rowStride, imageStride, zBuffer, visibilityBuffer);
275292
if (const cudaError_t error = cudaGetLastError(); error != cudaSuccess) {
276293
return error;
277294
}
278295
279296
if (numTriangles > 0) {
280297
rasterizeVisibilityKernel<<<
281-
static_cast<uint32_t>(numTriangles),
282-
kCudaRasterizerThreadsPerBlock,
298+
dim3(static_cast<uint32_t>(numTriangles), batchGridSize),
299+
kRasterThreadsPerBlock,
283300
0,
284301
stream>>>(
285302
positionsModel,
@@ -291,20 +308,58 @@ cudaError_t launchCudaRasterizeMesh(
291308
depthOffset,
292309
backfaceCulling,
293310
rowStride,
311+
imageStride,
294312
visibilityBuffer);
295313
if (const cudaError_t error = cudaGetLastError(); error != cudaSuccess) {
296314
return error;
297315
}
298316
}
299317
300-
resolveVisibilityKernel<<<pixelBlocks, kCudaRasterizerThreadsPerBlock, 0, stream>>>(
318+
resolveVisibilityKernel<<<
319+
dim3(static_cast<uint32_t>(pixelBlocks), batchGridSize),
320+
kCudaRasterizerThreadsPerBlock,
321+
0,
322+
stream>>>(
301323
camera.imageWidth,
302324
camera.imageHeight,
303325
rowStride,
326+
imageStride,
304327
visibilityBuffer,
305328
zBuffer,
306329
triangleIndexBuffer);
307330
return cudaGetLastError();
308331
}
309332
333+
cudaError_t launchCudaRasterizeMesh(
334+
const float* positionsModel,
335+
size_t numVertices,
336+
const int32_t* triangles,
337+
size_t numTriangles,
338+
const CudaCamera& camera,
339+
float nearClip,
340+
float depthOffset,
341+
bool backfaceCulling,
342+
int64_t rowStride,
343+
uint64_t* visibilityBuffer,
344+
float* zBuffer,
345+
int32_t* triangleIndexBuffer,
346+
cudaStream_t stream) {
347+
return launchCudaRasterizeMeshes(
348+
positionsModel,
349+
1,
350+
numVertices,
351+
triangles,
352+
numTriangles,
353+
camera,
354+
nearClip,
355+
depthOffset,
356+
backfaceCulling,
357+
rowStride,
358+
static_cast<int64_t>(camera.imageHeight) * rowStride,
359+
visibilityBuffer,
360+
zBuffer,
361+
triangleIndexBuffer,
362+
stream);
363+
}
364+
310365
} // namespace momentum::rasterizer::detail

‎momentum/rasterizer/cuda_rasterizer.h‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ struct CudaRasterizerBuffers {
4545
int64_t rowStride = 0;
4646
};
4747

48+
/// Device buffers used by batched CUDA rasterization.
49+
///
50+
/// Each image uses rowStride elements per row and imageStride elements between batch entries. The
51+
/// corresponding size fields are element counts, not byte counts, and must cover the final image's
52+
/// rows from each buffer pointer. Existing zBuffer and triangleIndexBuffer values are preserved
53+
/// where no new triangle wins the depth test.
54+
struct CudaBatchedRasterizerBuffers {
55+
uint64_t* visibilityBuffer = nullptr;
56+
size_t visibilityBufferSize = 0;
57+
float* zBuffer = nullptr;
58+
size_t zBufferSize = 0;
59+
int32_t* triangleIndexBuffer = nullptr;
60+
size_t triangleIndexBufferSize = 0;
61+
int64_t rowStride = 0;
62+
int64_t imageStride = 0;
63+
};
64+
4865
/// Rasterize a triangle mesh into CUDA-resident depth and triangle-index buffers.
4966
///
5067
/// The input arrays must be contiguous CUDA buffers with shapes [numVertices, 3] and
@@ -70,4 +87,31 @@ void rasterizeMeshCuda(
7087
float depthOffset = 0.0f,
7188
cudaStream_t stream = nullptr);
7289

90+
/// Rasterize a batch of triangle meshes with shared topology and camera.
91+
///
92+
/// inputs.positionsModel must be a contiguous CUDA buffer with shape
93+
/// [batchSize, inputs.numVertices, 3]. The triangle buffer, camera, and model matrix are shared by
94+
/// every batch entry. Work for the entire batch is launched asynchronously in three kernels on
95+
/// stream.
96+
///
97+
/// @param inputs CUDA input buffers, capacities, and mesh dimensions.
98+
/// @param batchSize Number of meshes; must be positive and at most 65535.
99+
/// @param camera Camera used for every batch entry.
100+
/// @param modelMatrix Additional model transform shared by every batch entry.
101+
/// @param nearClip Per-pixel near clipping distance; must be positive.
102+
/// @param buffers Batched CUDA output and scratch buffers.
103+
/// @param backfaceCulling Whether to discard back-facing triangles.
104+
/// @param depthOffset Value added to depth before the depth test and write.
105+
/// @param stream CUDA stream used for all launches.
106+
void rasterizeMeshesCuda(
107+
CudaRasterizerInputs inputs,
108+
size_t batchSize,
109+
const Camera& camera,
110+
const Eigen::Matrix4f& modelMatrix,
111+
float nearClip,
112+
CudaBatchedRasterizerBuffers buffers,
113+
bool backfaceCulling = true,
114+
float depthOffset = 0.0f,
115+
cudaStream_t stream = nullptr);
116+
73117
} // namespace momentum::rasterizer

‎momentum/rasterizer/cuda_rasterizer_kernels.cuh‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ namespace momentum::rasterizer::detail {
1616

1717
inline constexpr int32_t kCudaRasterizerThreadsPerBlock = 256;
1818

19+
/// Camera intrinsics models implemented by the CUDA projection path.
1920
enum class CudaCameraModel : int32_t {
2021
Pinhole,
2122
OpenCV,
2223
OpenCVFisheye,
2324
};
2425

26+
/// Trivially copyable camera parameters passed to CUDA kernels.
2527
struct CudaCamera {
2628
CudaCameraModel model = CudaCameraModel::Pinhole;
2729
int32_t imageWidth = 0;
@@ -36,6 +38,7 @@ struct CudaCamera {
3638
float worldToEye[12] = {};
3739
};
3840

41+
/// Launch the three CUDA kernels for one mesh.
3942
cudaError_t launchCudaRasterizeMesh(
4043
const float* positionsModel,
4144
size_t numVertices,
@@ -51,4 +54,22 @@ cudaError_t launchCudaRasterizeMesh(
5154
int32_t* triangleIndexBuffer,
5255
cudaStream_t stream);
5356

57+
/// Launch the three CUDA kernels for a batch with shared topology and camera.
58+
cudaError_t launchCudaRasterizeMeshes(
59+
const float* positionsModel,
60+
size_t batchSize,
61+
size_t numVertices,
62+
const int32_t* triangles,
63+
size_t numTriangles,
64+
const CudaCamera& camera,
65+
float nearClip,
66+
float depthOffset,
67+
bool backfaceCulling,
68+
int64_t rowStride,
69+
int64_t imageStride,
70+
uint64_t* visibilityBuffer,
71+
float* zBuffer,
72+
int32_t* triangleIndexBuffer,
73+
cudaStream_t stream);
74+
5475
} // namespace momentum::rasterizer::detail

0 commit comments

Comments
 (0)