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

Simplify the CsgOpNodes as we build them, rather than in GetChildren #368

Merged
merged 15 commits into from
Mar 16, 2023

Conversation

ochafik
Copy link
Contributor

@ochafik ochafik commented Mar 14, 2023

Early simplification (while building boolean op nodes) makes it easier to flatten the tree and avoids... stack overflows in GetChildren.

large_scene_test.cpp in this PR will just segfault without the flattening changes (and could potentially form the basis for some large scene benchmark so I thought it worth to check it in).

Found this crash while testing openscad/openscad#4533 (segfaulted on issue2342.scad)

@ochafik ochafik changed the title Flatten the CsgOpNodes as we build them, rather than in GetChildren Simplify the CsgOpNodes as we build them, rather than in GetChildren Mar 14, 2023
Copy link
Collaborator

@pca006132 pca006132 left a comment

Choose a reason for hiding this comment

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

Thanks. I thought that I have already fixed this issue previously but it seems that it is not.

@@ -22,13 +22,16 @@ enum class CsgNodeType { Union, Intersection, Difference, Leaf };

class CsgLeafNode;

class CsgNode {
class CsgNode : public std::enable_shared_from_this<CsgNode> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks! I never know such a thing exists, I wanted to do something similar and have to do it the dumb way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it's a nice get out of jail free card 🤣


auto handleOperand = [&](const std::shared_ptr<CsgNode> &operand) {
if (auto opNode = std::dynamic_pointer_cast<CsgOpNode>(operand)) {
if (opNode->IsOp(op) && opNode.use_count() == 1 &&
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can also have some sort of max recursion level parameter, and force flattening the tree when the recursion level is reached, regardless of whether or not the child is shared? So we can avoid stack overflow in any situation.

Copy link
Contributor Author

@ochafik ochafik Mar 14, 2023

Choose a reason for hiding this comment

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

So tbh I'm not quite sure in which circumstances the nodes and/or their impl are shared. I've relaxed the test anyway as I didn't realize the operand.use_count()==1 broke the intended fix (there's copies of shared pointers around, and not just because I mistakenly passed it by value in that method - now fixed). Checking that count seems a bit brittle.

I've left the other check (on impl.UseCount()) to keep some mysterious tests happy, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re/ your suggestion to force-flatten in extreme sharing cases, maybe we could explore as follow up? Not sure if geometry from OpenSCAD would trigger that case though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure, I don't think it will happen normally.

@codecov
Copy link

codecov bot commented Mar 14, 2023

Codecov Report

Patch coverage: 95.00% and project coverage change: -3.92 ⚠️

Comparison is base (dca9cce) 89.39% compared to head (409dd3f) 85.48%.

❗ Current head 409dd3f differs from pull request most recent head 6fcf61b. Consider uploading reports for the commit 6fcf61b to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #368      +/-   ##
==========================================
- Coverage   89.39%   85.48%   -3.92%     
==========================================
  Files          33       36       +3     
  Lines        3979     4409     +430     
==========================================
+ Hits         3557     3769     +212     
- Misses        422      640     +218     
Impacted Files Coverage Δ
src/manifold/src/csg_tree.h 75.00% <0.00%> (-25.00%) ⬇️
src/manifold/src/csg_tree.cpp 92.30% <97.36%> (+0.67%) ⬆️
src/manifold/src/manifold.cpp 92.14% <100.00%> (-0.07%) ⬇️

... and 16 files with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

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.

@pca006132, you know this code better than I, so I'll leave approval to you.

time ./extras/largeSceneTest 50 )
*/
int main(int argc, char **argv) {
int n = 20; // Crashes at n=50
Copy link
Owner

Choose a reason for hiding this comment

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

Does it still crash at n=50 with this change, or was that only before?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was only before, edited this out, thanks!

@@ -419,34 +457,16 @@ std::vector<std::shared_ptr<CsgNode>> &CsgOpNode::GetChildren(
bool finalize) const {
auto impl = impl_.GetGuard();
auto &children_ = impl->children_;
if (children_.empty() || (impl->simplified_ && !finalize) || impl->flattened_)
return children_;
impl->simplified_ = true;
Copy link
Owner

Choose a reason for hiding this comment

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

Happy to see this simplification, but what was the difference between simplified and flattened?

Copy link
Collaborator

Choose a reason for hiding this comment

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

simplified did not perform flattening if the nodes are shared, iirc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Flattening (also aliased here to finalization) was forcing leaf nodes, while simplification was flattening 1 level of op nodes nesting (adopting the children of same op children, and in the case of difference, of union op children past the first child).

Have now renamed finalize & flattened_ to force[d]ToLeafNodes[_] to disambiguate, and simplified this method further.

Copy link
Owner

Choose a reason for hiding this comment

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

Perfect, thank you!

@pca006132
Copy link
Collaborator

This looks fine for me, but can you do a simple benchmark comparing with the current master? Wondering if there is any performance improvement/degradation for our test cases (manifold_test and the python examples, which are extracted from your gist of benchmarks)

auto handleOperand = [&](const std::shared_ptr<CsgNode> &operand) {
if (auto opNode = std::dynamic_pointer_cast<CsgOpNode>(operand)) {
if ((opNode->IsOp(op) ||
(op == OpType::Subtract && opNode->IsOp(OpType::Add))) &&
Copy link
Contributor Author

@ochafik ochafik Mar 14, 2023

Choose a reason for hiding this comment

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

Now doing (a-(b+c) -> (a-b-c) here to play better w/ CsgOpNode::ToLeafNode (which turns it back to (a-batch(b+c))

@ochafik
Copy link
Contributor Author

ochafik commented Mar 14, 2023

can you do a simple benchmark comparing with the current master?

@pca006132 performance looks identical for these tests and examples, here's the script I used to benchmark (uses hyperfine)

@pca006132
Copy link
Collaborator

I was testing this branch and I found it breaks my existing python code. The output stl is different from the current master. I will try to make a minimal example for it.

@pca006132
Copy link
Collaborator

  auto a = Manifold::Cylinder(10, 10);
  auto b = Manifold::Cube({10, 10, 10});
  auto c = Manifold::Cylinder(1, 1);

  float volume1 = (a - c).GetProperties().volume;
  float volume2 = (a + b - c).GetProperties().volume;
  EXPECT_GT(volume2, volume1);

The assertion failed:

Expected: (volume2) > (volume1), actual: 2342.13 vs 3122.84

while this passed with the current master:

3341.423096 > 3122.840088

@ochafik
Copy link
Contributor Author

ochafik commented Mar 16, 2023

@pca006132 thanks for the repro! Pushed a fix and some tests based on your snippet

@pca006132
Copy link
Collaborator

Interestingly it still break my other code... will try to reproduce it later today.

Copy link
Collaborator

@pca006132 pca006132 left a comment

Choose a reason for hiding this comment

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

Minimal example that you can add to the test case:

  auto a = (Manifold::Cube({1, 1, 1}) + Manifold::Cube({1, 1, 1})).Translate({1, 0, 0});
  auto b = (Manifold::Cube({1, 1, 1}) + Manifold::Cube({1, 1, 1}));

  EXPECT_FLOAT_EQ((a + b).GetProperties().volume, 2);

Currently this evaluates to 1. Note that if you write it as

  EXPECT_FLOAT_EQ(((Manifold::Cube({1, 1, 1}) + Manifold::Cube({1, 1, 1})).Translate({1, 0, 0}) + (Manifold::Cube({1, 1, 1}) + Manifold::Cube({1, 1, 1}))).GetProperties().volume, 2);

it will not trigger the bug, as the use count is somehow set to 2, perhaps due to how C++ deal with destructors.


if (IsOp(op) && (impl_.UseCount() == 1)) {
auto impl = impl_.GetGuard();
std::copy(impl->children_.begin(), impl->children_.end(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This copy doesn't apply transform from the parent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hadn't realized the transform was around, thanks!

src/manifold/src/csg_tree.cpp Outdated Show resolved Hide resolved
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.

Nice job working together on this!

// Define solids which volumes are easy to compute w/ bit arithmetics:
// m1, m2, m4 are unique, non intersecting "bits" (of volume 1, 2, 4)
// m3 = m1 + m2
// m7 = m1 + m2 + m3
Copy link
Owner

Choose a reason for hiding this comment

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

Excellent tests, thank you! Technically these should go in boolean_test.cpp now that I've separated them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks!! Moved them 👌

@elalish elalish merged commit e17eb03 into elalish:master Mar 16, 2023
@ochafik
Copy link
Contributor Author

ochafik commented Mar 16, 2023

Thanks a lot guys, appreciate the help & quick turnaround! (aaaaand the amazing library of course!!)

ochafik added a commit to ochafik/openscad that referenced this pull request Mar 16, 2023
@elalish elalish mentioned this pull request Nov 3, 2023
cartesian-theatrics pushed a commit to SovereignShop/manifold that referenced this pull request Mar 11, 2024
…lalish#368)

* Flatten the CsgOpNodes as we build them, rather than in GetChildren

* Don't inline reused nodes in CsgNode::Boolean

* Reinstate difference -> union rewrite

* No need for opportunistic flattening anymore

* Fix typo

* Remove CsgOpNode::Impl::simplified_

* Don't overskip referenced nodes in simplification

* Avoid needless copy of shared pointers

* Disambiguate CsgOpNode::Impl::flatten_ -> forcedToLeafNodes_ and align GetChildren arg naming

* Simplified CsgOpNode::GetChildren

* Simpler (a-(b+c)) -> (a-b-c) transform in CsgOpNode::Boolean to work better w/ CsgOpNode::ToLeafNode

* Simpler build hints for large_scene_test.cpp

* [flatten] Refine rules about tree flattening + added test case

* [manifold] Propagate transforms when flattening op trees

* [flatten] move tests from manifold_test to boolean_test
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.

3 participants