Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid recursion in csg tree #1086

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

avoid recursion in csg tree #1086

wants to merge 8 commits into from

Conversation

pca006132
Copy link
Collaborator

Fixes #989.

This refactors the code into a single loop that does dfs on the csg tree, and perform flattening on-the-fly.

There are still some failures, I guess this is due to shared nodes being evaluated in a weird way... Will try to fix it soon. But for now I think the logic is mostly there and should be ready for review.

For comparison:

// before
n = 20
nTri = 91814, time = 2.28608 sec
n = 30
nTri = 279334, time = 20.6878 sec

// after
n = 20
nTri = 91814, time = 1.18475 sec
n = 30
nTri = 279334, time = 5.67487 sec

src/csg_tree.cpp Outdated
->GetImpl());
} else {
std::vector<std::shared_ptr<CsgLeafNode>> tmp;
for (size_t j : set) {
tmp.push_back(
std::dynamic_pointer_cast<CsgLeafNode>(children_[start + j]));
std::dynamic_pointer_cast<CsgLeafNode>(children[start + j]));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this cast dynamic while the one above is static?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I should change these to static.

src/csg_tree.cpp Outdated
bool finalize;
OpType parent_op;
mat3x4 transform;
std::vector<std::shared_ptr<CsgLeafNode>> *parent_children;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the relationship between parent_children and left_children and right_children? And why is just the parent a pointer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent_children is the list of children of the parent (or ancestor), while left_children and right_children are the sets of children of the current node.

Will explain it better with some comment.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I might call parent_children siblings, if I'm understanding you correctly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, the issue is that it is not exactly sibling as they may be in different levels... I need to think about a better naming.

op_node(op_node) {}
};

std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode() const {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never fully understood what the previous version of this algorithm did either - perhaps some comments on the general steps this is recursing through?

Copy link
Owner

@elalish elalish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks like a good direction, though a bit out of my wheelhouse. I'll be curious to understand where the bug is...

@pca006132
Copy link
Collaborator Author

CondensedMatter16 hits #970 after this PR. I guess this is due to the code now performs optimization more aggressively, and there are more things that got feed into BatchUnion...

Copy link

codecov bot commented Dec 2, 2024

Codecov Report

Attention: Patch coverage is 90.62500% with 9 lines in your changes missing coverage. Please review.

Project coverage is 91.65%. Comparing base (11387fb) to head (c99eb63).
Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/csg_tree.cpp 90.62% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1086      +/-   ##
==========================================
- Coverage   91.72%   91.65%   -0.07%     
==========================================
  Files          30       30              
  Lines        5910     5897      -13     
==========================================
- Hits         5421     5405      -16     
- Misses        489      492       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pca006132
Copy link
Collaborator Author

This slows down the overall test slightly, from 26180ms to 26877ms, ~2.5%. This is due to CondensedMatter64 being slower, which is due to BatchUnioning a lot of meshes (1021) being slower than unioning them in batches...

If I tune the BatchUnion threshold from 1000 to 100, it is now taking 25641ms, ~2% faster. Not sure if this is general enough though.

@pca006132
Copy link
Collaborator Author

Will add documentation tmr, a bit tired today.

@pca006132
Copy link
Collaborator Author

Anyway, decided to write the documentation now :)

About the original bug: The idea is that the flattening takes O(n) time in the old implementation, which makes it problematic because in the worst case we can have O(n) flattening operations as well, which gives us O(n^2) time. As an example, consider the following:

Union(a, Union(b, Union(c, Union(d)))
=> Union(a, Union(b, Union(c, d)))       // 1 copy
=> Union(a, Union(b, c, d))              // 2 copy
=> Union(a, b, c, d)                     // 3 copy

The implementation here doesn't attempt to do flattening when we construct the node. In fact, it is impossible to avoid this quadratic behavior if we perform (all) flattening when we construct the nodes, because there will be O(n^2) nodes if we do that. The algorithm only does one pass over the tree, and do flattening on-the-fly. Each node is moved only once. This makes it O(n).

// `Subtract` is handled differently from `Add` and `Intersect`. It is treated
// as two `Add` nodes, `left_children` and `right_children`, that should be
// subtracted later. This allows flattening children `Add` nodes. For normal
// `Add` and `Intersect`, we only use `left_children`.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Could these be named positive_children and negative_children? Or children and inverted_children?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

}
auto impl = frame->op_node->impl_.GetGuard();
if (frame->finalize) {
if (frame->op_node->cache_ == nullptr) switch (frame->op_node->op_) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can cache_ be anything but nullptr here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I forgot to remove this check after adding the check above.

std::vector<std::shared_ptr<const Manifold::Impl>> impls;
for (auto &child : frame->left_children)
impls.push_back(child->GetImpl());
auto result = BatchBoolean(OpType::Intersect, impls);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is batch intersection so different than batch union?

node->Transform(transform)));
else
stack.push_back(std::make_shared<CsgStackFrame>(
false, op, transform, children,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow this logic: either we add the node to the set of children, or we add a frame to the stack that contains this node and all the children? Do the children belong to this node or not (when the function is called)?

frame->op_node->cache_->Transform(frame->transform)));
stack.pop_back();
} else {
auto add_children = [&](std::shared_ptr<CsgNode> &node, OpType op,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to be explicit about what's captured in a lambda - it helps me to understand the scope of the function.

frame->op_node.use_count() <= 2 &&
frame->op_node->impl_.UseCount() == 1;
if (skipFinalize) stack.pop_back();
auto transform =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const? And for short types, I prefer to avoid auto.

frame->op_node->op_ == frame->parent_op &&
frame->op_node.use_count() <= 2 &&
frame->op_node->impl_.UseCount() == 1;
if (skipFinalize) stack.pop_back();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just set frame->finalize = true; above - is that still true? Or does this skipping refer to its child or parent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frame is removed here, so we will skip the current frame. Anyway, I can make it explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

accidental quadratic runtime in csg tree
2 participants