Skip to content

Commit 2a096f3

Browse files
yutingyemeta-codesync[bot]
authored andcommitted
Fix Windows SIMD renderer stack overflow (#1570)
Summary: Pull Request resolved: #1570 `pymomentum.renderer`'s lighting coverage has been skipped on Windows because the CMake/SIMD build crashes with a stack overflow when running the Python renderer tests. A previous attempt tried to fix this by forcing noinline boundaries and, in later versions, moving renderer calls onto a larger Windows thread stack. That still did not make the failing CI path reliable: the renderer remained dependent on MSVC stack layout for a deep Python -> pybind11 -> rasterizer call chain with large DrJit packet temporaries. The heavy stack pressure is in the outer packetized triangle setup in `rasterizeMeshImp`, where MSVC has to materialize several DrJit packet matrix/vector temporaries (`Matrix3fP`, `Matrix3dP`, `Vector3fP`, `IntP`) before dispatching to the per-pixel rasterizer. This work is useful, but it is not the core pixel-loop SIMD path. This change keeps the existing SIMD pixel loop, but for MSVC SIMD builds it processes the per-triangle setup scalarly: - transform/project one triangle at a time - compute the interpolation matrix, inverse, bounds, normals, and texture attributes scalarly - call the existing `rasterizeOneTriangle` implementation so the pixel loop still uses DrJit SIMD Non-MSVC builds keep the existing packetized triangle setup. A `MOMENTUM_RASTERIZER_FORCE_SCALAR_OUTER` compile-time knob is included so the fallback path can be built and tested on non-Windows machines. With the Windows-specific renderer issue addressed, this also removes the Windows skips from the renderer texture-coordinate tests and stops excluding `test_lighting` from the Windows GitHub pixi and weekly workflows. Reviewed By: cstollmeta Differential Revision: D111848283 fbshipit-source-id: 3fe6075708a6115ec9ed7dac39b9b640e9a29c48
1 parent 2da144e commit 2a096f3

4 files changed

Lines changed: 249 additions & 21 deletions

File tree

‎.github/workflows/weekly.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,5 @@ jobs:
153153
pixi run -e py312-cuda129 python -c "
154154
import os, subprocess, sys
155155
os.environ['MOMENTUM_MODELS_PATH'] = 'momentum/'
156-
sys.exit(subprocess.call(['pytest', 'pymomentum/test/', '-k', 'not ((TestFBXIO and (test_save_motions_with_joint_params or test_save_motions_with_model_params)) or test_lighting)']))
156+
sys.exit(subprocess.call(['pytest', 'pymomentum/test/', '-k', 'not (TestFBXIO and (test_save_motions_with_joint_params or test_save_motions_with_model_params))']))
157157
"

‎momentum/rasterizer/rasterizer_triangles.cpp‎

Lines changed: 247 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
#include <drjit/matrix.h>
2121
#include <drjit/util.h>
2222

23+
#include <algorithm>
2324
#include <array>
25+
#include <cmath>
2426
#include <cstddef>
2527
#include <span>
2628
#include <sstream>
@@ -31,6 +33,13 @@ namespace momentum::rasterizer {
3133

3234
namespace {
3335

36+
#if (defined(_MSC_VER) && defined(MOMENTUM_ENABLE_SIMD) && MOMENTUM_ENABLE_SIMD) || \
37+
defined(MOMENTUM_RASTERIZER_FORCE_SCALAR_OUTER)
38+
#define MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP 1
39+
#else
40+
#define MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP 0
41+
#endif
42+
3443
// This is the vertex interpolation matrix, where we map from
3544
// homgeneous coordinates to interpolate texture coordinates, etc.
3645
// https://redirect.cs.umbc.edu/~olano/papers/2dh-tri/2dh-tri.pdf
@@ -58,11 +67,46 @@ inline Matrix3fP createVertexMatrix(
5867
return result;
5968
}
6069

70+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
71+
inline Matrix3f createVertexMatrix(
72+
const std::array<Vector3f, 3>& p_tri_window,
73+
const Camera& camera) {
74+
Matrix3f result;
75+
for (int i = 0; i < 3; ++i) {
76+
result(i, 0) = p_tri_window[i].x() / camera.fx() * p_tri_window[i].z();
77+
result(i, 1) = p_tri_window[i].y() / camera.fy() * p_tri_window[i].z();
78+
result(i, 2) = p_tri_window[i].z();
79+
}
80+
return result;
81+
}
82+
#endif
83+
6184
inline Vector3fP reflect(const Vector3fP& v, const Vector3fP& n) {
6285
const FloatP dotProd = drjit::dot(v, n);
6386
return (2.0f * dotProd) * n - v;
6487
}
6588

89+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
90+
inline Eigen::Vector3f transformModelToEye(
91+
const Camera& camera,
92+
const Eigen::Matrix4f& modelMatrix,
93+
const Eigen::Vector3f& p_model) {
94+
const Eigen::Vector4f p_model_h(p_model.x(), p_model.y(), p_model.z(), 1.0f);
95+
const Eigen::Vector4f p_world_h = modelMatrix * p_model_h;
96+
const Eigen::Vector3f p_world = p_world_h.head<3>() / p_world_h.w();
97+
return camera.eyeFromWorld() * p_world;
98+
}
99+
100+
inline Eigen::Matrix3f createNormalMatrix(
101+
const Camera& camera,
102+
const Eigen::Matrix4f& modelMatrix) {
103+
return (camera.eyeFromWorld() * Eigen::Transform<float, 3, Eigen::Affine>(modelMatrix))
104+
.linear()
105+
.inverse()
106+
.transpose();
107+
}
108+
#endif
109+
66110
template <int N>
67111
Matrix3f extractTriangleAttributes(
68112
const Eigen::Ref<const Eigen::VectorXf>& vec,
@@ -110,6 +154,20 @@ inline auto isAllFinite(const Vector3dP& v) {
110154
return drjit::isfinite(v.x()) && drjit::isfinite(v.y()) && drjit::isfinite(v.z());
111155
}
112156

157+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
158+
inline bool isAllFinite(const Vector3d& v) {
159+
return std::isfinite(v.x()) && std::isfinite(v.y()) && std::isfinite(v.z());
160+
}
161+
162+
inline int32_t floorClippedToImage(float value, int32_t maxValue) {
163+
return static_cast<int32_t>(std::floor(std::clamp(value, 0.0f, static_cast<float>(maxValue))));
164+
}
165+
166+
inline int32_t ceilClippedToImage(float value, int32_t maxValue) {
167+
return static_cast<int32_t>(std::ceil(std::clamp(value, 0.0f, static_cast<float>(maxValue))));
168+
}
169+
#endif
170+
113171
inline auto allBehindNearClip(const std::array<Vector3fP, 4>& pts, float nearClip) {
114172
return pts[0].z() < nearClip && pts[1].z() < nearClip && pts[2].z() < nearClip &&
115173
pts[3].z() < nearClip;
@@ -484,6 +542,32 @@ inline Matrix3fP computeTriangleEyeNormals(
484542
return n_tri_eye;
485543
}
486544

545+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
546+
inline Matrix3f computeTriangleEyeNormals(
547+
const Eigen::Ref<const Eigen::VectorXf>& normals_world,
548+
const Eigen::Matrix3f& normalMatrix,
549+
const Matrix3f& p_tri_eye,
550+
const Eigen::Vector3i& triangle) {
551+
Matrix3f n_tri_eye;
552+
if (normals_world.size() == 0) {
553+
const Vector3f n_eye = drjit::normalize(
554+
drjit::cross(
555+
extractColumn(p_tri_eye, 1) - extractColumn(p_tri_eye, 0),
556+
extractColumn(p_tri_eye, 2) - extractColumn(p_tri_eye, 0)));
557+
n_tri_eye = toUniformMat(n_eye);
558+
} else {
559+
for (int i = 0; i < 3; ++i) {
560+
const Eigen::Vector3f n_world = normals_world.segment<3>(3 * triangle[i]);
561+
const Eigen::Vector3f n_eye = (normalMatrix * n_world).normalized();
562+
for (int j = 0; j < 3; ++j) {
563+
n_tri_eye(j, i) = n_eye[j];
564+
}
565+
}
566+
}
567+
return n_tri_eye;
568+
}
569+
#endif
570+
487571
// Support both signed and unsigned triangles for backward compatibility:
488572
template <typename TriangleT>
489573
Matrix3f getTextureAttributes(
@@ -502,6 +586,134 @@ Matrix3f getTextureAttributes(
502586
return extractTriangleAttributes<2>(textureCoords, textureTriangle);
503587
}
504588

589+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
590+
// Support both signed and unsigned triangles for backward compatibility:
591+
template <typename TriangleT>
592+
void rasterizeMeshImpScalarTriangleLoop(
593+
const Eigen::Ref<const Eigen::VectorXf>& positions_world,
594+
const Eigen::Ref<const Eigen::VectorXf>& normals_world,
595+
const Eigen::Ref<const Eigen::Matrix<TriangleT, Eigen::Dynamic, 1>>& triangles,
596+
const Eigen::Ref<const Eigen::VectorXf>& textureCoords,
597+
const Eigen::Ref<const Eigen::Matrix<TriangleT, Eigen::Dynamic, 1>>& textureTriangles,
598+
const Eigen::Ref<const Eigen::VectorXf>& perVertexDiffuseColor,
599+
const Camera& camera,
600+
const SimdCamera& cameraSimd,
601+
int32_t imageWidth,
602+
int32_t imageHeight,
603+
bool usePerVertexColor,
604+
bool useTextureCoords,
605+
float* zBufferPtr,
606+
float* rgbBufferPtr,
607+
float* surfaceNormalsBufferPtr,
608+
int* vertexIndexBufferPtr,
609+
int* triangleIndexBufferPtr,
610+
index_t zBufferRowStride,
611+
const std::vector<Light>& lights_eye,
612+
const PhongMaterial& material,
613+
const Eigen::Matrix4f& modelMatrix,
614+
bool backfaceCulling,
615+
float nearClip,
616+
float depthOffset) {
617+
const auto nTriangles = triangles.size() / 3;
618+
const Eigen::Matrix3f normalMatrix = createNormalMatrix(camera, modelMatrix);
619+
const Matrix3f diffuseColor = toUniformMat(material.diffuseColor);
620+
621+
for (index_t iTriangle = 0; iTriangle < nTriangles; ++iTriangle) {
622+
const Eigen::Vector3i triangle =
623+
triangles.template segment<3>(3 * iTriangle).template cast<int32_t>();
624+
625+
std::array<Vector3f, 3> p_tri_window;
626+
Matrix3f p_tri_eye;
627+
628+
bool validTriangle = true;
629+
for (int i = 0; i < 3; ++i) {
630+
const Eigen::Vector3f p_model = positions_world.segment<3>(3 * triangle[i]);
631+
const Eigen::Vector3f p_eye = transformModelToEye(camera, modelMatrix, p_model);
632+
const auto [p_window, validProj] = camera.intrinsicsModel()->project(p_eye);
633+
634+
p_tri_window[i] = toEnokiVec(p_window);
635+
for (int j = 0; j < 3; ++j) {
636+
p_tri_eye(j, i) = p_eye[j];
637+
}
638+
validTriangle = validTriangle && validProj;
639+
}
640+
641+
if (backfaceCulling) {
642+
const Eigen::Vector2f edge1(
643+
p_tri_window[1].x() - p_tri_window[0].x(), p_tri_window[1].y() - p_tri_window[0].y());
644+
const Eigen::Vector2f edge2(
645+
p_tri_window[2].x() - p_tri_window[0].x(), p_tri_window[2].y() - p_tri_window[0].y());
646+
647+
const float signedArea = edge1.y() * edge2.x() - edge1.x() * edge2.y();
648+
validTriangle = validTriangle && signedArea > 0;
649+
}
650+
if (!validTriangle) {
651+
continue;
652+
}
653+
654+
const Matrix3f vertexMatrix = createVertexMatrix(p_tri_window, camera);
655+
const Matrix3d vertexMatrixInverse = drjit::inverse(Matrix3d(vertexMatrix));
656+
657+
const Vector3d recipWInterp(
658+
vertexMatrixInverse(0, 0) + vertexMatrixInverse(0, 1) + vertexMatrixInverse(0, 2),
659+
vertexMatrixInverse(1, 0) + vertexMatrixInverse(1, 1) + vertexMatrixInverse(1, 2),
660+
vertexMatrixInverse(2, 0) + vertexMatrixInverse(2, 1) + vertexMatrixInverse(2, 2));
661+
if (!isAllFinite(recipWInterp)) {
662+
continue;
663+
}
664+
665+
if (p_tri_window[0].z() < nearClip && p_tri_window[1].z() < nearClip &&
666+
p_tri_window[2].z() < nearClip) {
667+
continue;
668+
}
669+
670+
const int32_t startX = floorClippedToImage(
671+
std::min({p_tri_window[0].x(), p_tri_window[1].x(), p_tri_window[2].x()}), imageWidth - 1);
672+
const int32_t endX = ceilClippedToImage(
673+
std::max({p_tri_window[0].x(), p_tri_window[1].x(), p_tri_window[2].x()}), imageWidth - 1);
674+
const int32_t startY = floorClippedToImage(
675+
std::min({p_tri_window[0].y(), p_tri_window[1].y(), p_tri_window[2].y()}), imageHeight - 1);
676+
const int32_t endY = ceilClippedToImage(
677+
std::max({p_tri_window[0].y(), p_tri_window[1].y(), p_tri_window[2].y()}), imageHeight - 1);
678+
679+
const Matrix3f n_tri_eye =
680+
computeTriangleEyeNormals(normals_world, normalMatrix, p_tri_eye, triangle);
681+
682+
rasterizeOneTriangle(
683+
static_cast<int32_t>(iTriangle),
684+
triangle,
685+
cameraSimd,
686+
startX,
687+
endX,
688+
startY,
689+
endY,
690+
Vector3f(recipWInterp),
691+
drjit::transpose(Matrix3f(vertexMatrixInverse)),
692+
p_tri_eye,
693+
n_tri_eye,
694+
getTextureAttributes(
695+
textureCoords,
696+
textureTriangles,
697+
useTextureCoords,
698+
static_cast<int>(iTriangle),
699+
triangle),
700+
usePerVertexColor ? extractTriangleAttributes<3>(perVertexDiffuseColor, triangle)
701+
: diffuseColor,
702+
material,
703+
lights_eye,
704+
nearClip,
705+
zBufferRowStride,
706+
depthOffset,
707+
zBufferPtr,
708+
rgbBufferPtr,
709+
surfaceNormalsBufferPtr,
710+
vertexIndexBufferPtr,
711+
triangleIndexBufferPtr,
712+
false);
713+
}
714+
}
715+
#endif
716+
505717
// Support both signed and unsigned triangles for backward compatibility:
506718
template <typename TriangleT>
507719
void rasterizeMeshImp(
@@ -561,7 +773,39 @@ void rasterizeMeshImp(
561773
float* surfaceNormalsBufferPtr = dataOrNull(surfaceNormalsBuffer);
562774
int* vertexIndexBufferPtr = dataOrNull(vertexIndexBuffer);
563775
int* triangleIndexBufferPtr = dataOrNull(triangleIndexBuffer);
564-
776+
const index_t zBufferRowStride = getRowStride(zBuffer);
777+
778+
#if MOMENTUM_RASTERIZER_USE_SCALAR_TRIANGLE_SETUP
779+
// MSVC's codegen for the DrJit packetized triangle setup has very large stack
780+
// frames in the Python renderer call chain. Keep pixel SIMD, but process the
781+
// per-triangle setup scalarly on Windows so the renderer fits the default
782+
// Python thread stack.
783+
rasterizeMeshImpScalarTriangleLoop(
784+
positions_world,
785+
normals_world,
786+
triangles,
787+
textureCoords,
788+
textureTriangles,
789+
perVertexDiffuseColor,
790+
camera,
791+
cameraSimd,
792+
imageWidth,
793+
imageHeight,
794+
usePerVertexColor,
795+
useTextureCoords,
796+
zBufferPtr,
797+
rgbBufferPtr,
798+
surfaceNormalsBufferPtr,
799+
vertexIndexBufferPtr,
800+
triangleIndexBufferPtr,
801+
zBufferRowStride,
802+
lights_eye,
803+
material,
804+
modelMatrix,
805+
backfaceCulling,
806+
nearClip,
807+
depthOffset);
808+
#else
565809
for (auto [triangleIndices, triangleMask] : intPacketRange(nTriangles)) {
566810
auto triangles_cur = drjit::gather<Vector3iP>(triangles.data(), triangleIndices, triangleMask);
567811
std::array<Vector3fP, 3> p_tri_window;
@@ -673,7 +917,7 @@ void rasterizeMeshImp(
673917
material,
674918
lights_eye,
675919
nearClip,
676-
getRowStride(zBuffer),
920+
zBufferRowStride,
677921
depthOffset,
678922
zBufferPtr,
679923
rgbBufferPtr,
@@ -683,6 +927,7 @@ void rasterizeMeshImp(
683927
false);
684928
}
685929
}
930+
#endif
686931
}
687932

688933
void validateSplatInputs(

‎pixi.toml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,7 @@ build_py = { cmd = "pip install . -v", env = { CMAKE_ARGS = """
515515
-DBUILD_SHARED_LIBS=OFF
516516
""", CMAKE_GENERATOR_INSTANCE = "", CMAKE_GENERATOR_PLATFORM = "", CMAKE_GENERATOR_TOOLSET = "", MOMENTUM_ENABLE_FBX_SAVING = "OFF", MOMENTUM_ENABLE_SIMD = "ON", TORCH_CUDA_ARCH_LIST = "5.0;6.0;6.1;7.0;7.5;8.0;8.6;8.9;9.0+PTX" } }
517517
# TODO: Remove -k option, once saving tests are disabled programmatically when Autodesk FBX SDK is unavailable (FBX loading always works via OpenFBX)
518-
# TODO: Remove test_lighting skip once stack overflow issue on Windows is resolved (currently causes crash with CMake build)
519-
test_py = { cmd = "pytest pymomentum/test/ -k 'not ((TestFBXIO and (test_save_motions_with_joint_params or test_save_motions_with_model_params)) or test_lighting)'", env = { MOMENTUM_MODELS_PATH = "momentum/" }, depends-on = [
518+
test_py = { cmd = "pytest pymomentum/test/ -k 'not (TestFBXIO and (test_save_motions_with_joint_params or test_save_motions_with_model_params))'", env = { MOMENTUM_MODELS_PATH = "momentum/" }, depends-on = [
520519
"build_py", "install",
521520
] }
522521

‎pymomentum/test/test_renderer.py‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
# pyre-strict
77

8-
import sys
98
import unittest
109

1110
import numpy as np
@@ -382,11 +381,6 @@ def test_rasterize_mesh_texcoords_match_vertices(self) -> None:
382381
render without texture coordinates, depth values should be correct,
383382
and the rgb_buffer should contain visible (lit) pixels.
384383
"""
385-
# The SIMD rasterizer uses large stack frames (drjit SIMD types +
386-
# deep inlining) that overflow the default 1 MB Windows thread stack.
387-
if sys.platform == "win32":
388-
return
389-
390384
camera = pym_camera.Camera(
391385
pym_camera.PinholeIntrinsicsModel(image_width=64, image_height=64)
392386
)
@@ -438,11 +432,6 @@ def test_rasterize_mesh_texcoords_fewer_than_vertices(self) -> None:
438432
pixel count should match a baseline render without texture
439433
coordinates, and the geometry is unaffected by the texture mapping.
440434
"""
441-
# The SIMD rasterizer uses large stack frames (drjit SIMD types +
442-
# deep inlining) that overflow the default 1 MB Windows thread stack.
443-
if sys.platform == "win32":
444-
return
445-
446435
camera = pym_camera.Camera(
447436
pym_camera.PinholeIntrinsicsModel(image_width=64, image_height=64)
448437
)
@@ -507,11 +496,6 @@ def test_rasterize_mesh_texcoords_more_than_vertices(self) -> None:
507496
- The geometry (z-buffer) is unaffected by the choice of texture
508497
triangle indices.
509498
"""
510-
# The SIMD rasterizer uses large stack frames (drjit SIMD types +
511-
# deep inlining) that overflow the default 1 MB Windows thread stack.
512-
if sys.platform == "win32":
513-
return
514-
515499
camera = pym_camera.Camera(
516500
pym_camera.PinholeIntrinsicsModel(image_width=64, image_height=64)
517501
)

0 commit comments

Comments
 (0)