Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,58 @@ cd m_tree/build
./binaries/m_tree_tests
```

## Memory Safety Testing

The project uses compiler sanitizers to detect memory errors, undefined behavior, and threading issues. These run automatically in CI on every push and pull request.

### CI Workflow

The sanitizer workflow (`.github/workflows/sanitizers.yml`) runs three sanitizers on Linux:

| Sanitizer | Detects |
|-----------|---------|
| **ASan** (AddressSanitizer) | Buffer overflows, use-after-free, memory leaks |
| **UBSan** (UndefinedBehaviorSanitizer) | Signed overflow, null pointer dereference, misaligned access |
| **TSan** (ThreadSanitizer) | Data races, deadlocks |

A separate macOS job uses `xcrun leaks` for leak detection on ARM.

### Running Sanitizers Locally

Build with sanitizer flags enabled:

```bash
cd m_tree/build

# AddressSanitizer (recommended first check)
cmake ../ -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build . --config Debug

# Run with enhanced options
ASAN_OPTIONS=detect_stack_use_after_return=1:check_initialization_order=1 \
./binaries/m_tree_tests

# UndefinedBehaviorSanitizer
cmake ../ -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-sanitize-recover=all -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"
cmake --build . --config Debug
UBSAN_OPTIONS=print_stacktrace=1 ./binaries/m_tree_tests
```

### macOS Leak Detection

On macOS, use the system `leaks` tool:

```bash
cd m_tree
xcrun leaks --atExit -- ./binaries/m_tree_tests
```

Look for "0 leaks for 0 total leaked bytes" in the output.

## Architecture

The library follows a pipeline pattern: TreeFunctions -> Tree -> Mesher -> Mesh
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5_4_0
5_5_0
2 changes: 1 addition & 1 deletion blender_manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ schema_version = "1.0.0"

id = "modular_tree"
name = "Modular Tree"
version = "5.4.0"
version = "5.5.0"
website = "https://github.com/GoodPie/modular_tree"
tagline = "Procedural node based 3D tree generation"
maintainer = "GoodPie <brandynbb96@gmail.com>"
Expand Down
4 changes: 2 additions & 2 deletions m_tree/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.5...3.27)
cmake_minimum_required(VERSION 3.15...3.31)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Expand Down
2 changes: 1 addition & 1 deletion m_tree/dependencies/pybind11
Submodule pybind11 updated 257 files
2 changes: 1 addition & 1 deletion m_tree/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def build():
if not os.path.exists(build_dir):
os.makedirs(build_dir)

subprocess.check_call(["cmake", "../", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5"], cwd=build_dir)
subprocess.check_call(["cmake", "../"], cwd=build_dir)
subprocess.check_call(["cmake", "--build", ".", "--config", "Release"], cwd=build_dir)

print([i for i in os.listdir(os.path.join(os.path.dirname(__file__), "binaries"))])
Expand Down
2 changes: 1 addition & 1 deletion m_tree/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"

[project]
name = "m_tree"
version = "5.4.0"
version = "5.5.0"
description = "Procedural 3D tree generation library"
requires-python = ">=3.11"

Expand Down
4 changes: 2 additions & 2 deletions m_tree/python_bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.5...3.27)
cmake_minimum_required(VERSION 3.15...3.31)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Expand Down
4 changes: 2 additions & 2 deletions m_tree/source/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.5...3.27)
cmake_minimum_required(VERSION 3.15...3.31)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -fPIC")
Expand Down
7 changes: 7 additions & 0 deletions m_tree/source/meshers/base_types/TreeMesher.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#pragma once
#include "source/mesh/Mesh.hpp"
#include "source/tree/Tree.hpp"
#include <concepts>

namespace Mtree
{

template <typename T>
concept Mesher = requires(T& mesher, Tree& tree) {
{ mesher.mesh_tree(tree) } -> std::same_as<Mesh>;
};

class TreeMesher
{
public:
Expand Down
33 changes: 21 additions & 12 deletions m_tree/source/meshers/manifold_mesher/ManifoldMesher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "source/utilities/NodeUtilities.hpp"
#include <algorithm>
#include <iostream>
#include <numbers>

using namespace Mtree;
using namespace Mtree::NodeUtilities;
Expand Down Expand Up @@ -88,7 +89,7 @@ CircleDesignator add_circle(const Vector3& node_position, const Node& node, floa

for (size_t i = 0; i < radial_n_points; i++)
{
float angle = (float)i / radial_n_points * 2 * M_PI;
float angle = (float)i / radial_n_points * 2 * std::numbers::pi_v<float>;
Vector3 point = cos(angle) * right + sin(angle) * up;
point = point * radius + circle_position;
int index = mesh.add_vertex(point);
Expand All @@ -105,7 +106,8 @@ CircleDesignator add_circle(const Vector3& node_position, const Node& node, floa
mesh.uvs.emplace_back((float)i / radial_n_points, uv_y);
}
mesh.uvs.emplace_back(1, uv_y);
return CircleDesignator{vertex_index, uv_index, radial_n_points};
return CircleDesignator{
.vertex_index = vertex_index, .uv_index = uv_index, .radial_n = radial_n_points};
}

bool is_index_in_branch_mask(const std::vector<IndexRange>& mask, const int index,
Expand Down Expand Up @@ -158,17 +160,22 @@ float get_branch_angle_around_parent(const Node& parent, const Node& branch)
Vector3 up = right.cross(parent.direction);
float cos_angle = projected_branch_dir.dot(right);
float sin_angle = projected_branch_dir.dot(up);
return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * M_PI, 2 * M_PI);
return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * std::numbers::pi_v<float>,
2 * std::numbers::pi_v<float>);
}

IndexRange get_branch_indices_on_circle(const int radial_n_points, const float circle_radius,
const float branch_radius, const float branch_angle)
{
float angle_delta = std::asin(std::clamp(branch_radius / circle_radius, -1.f, 1.f));
float increment = 2 * M_PI / radial_n_points;
int min_index = (int)(std::fmod(branch_angle - angle_delta + 2 * M_PI, 2 * M_PI) / increment);
float increment = 2 * std::numbers::pi_v<float> / radial_n_points;
int min_index = (int)(std::fmod(branch_angle - angle_delta + 2 * std::numbers::pi_v<float>,
2 * std::numbers::pi_v<float>) /
increment);
int max_index =
(int)(std::fmod(branch_angle + angle_delta + increment + 2 * M_PI, 2 * M_PI) / increment);
(int)(std::fmod(branch_angle + angle_delta + increment + 2 * std::numbers::pi_v<float>,
2 * std::numbers::pi_v<float>) /
increment);
return IndexRange{min_index, max_index};
}

Expand Down Expand Up @@ -280,14 +287,15 @@ float get_child_twist(const Node& child, const Node& parent)
Vector3 up = right.cross(child.direction);
float cos_angle = child.tangent.dot(right);
float sin_angle = child.tangent.dot(up);
return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * M_PI, 2 * M_PI);
return std::fmod(std::atan2(sin_angle, cos_angle) + 2 * std::numbers::pi_v<float>,
2 * std::numbers::pi_v<float>);
}

int add_child_base_uvs(float parent_uv_y, const Node& parent, const NodeChild& child,
const IndexRange child_range, const int child_radial_n,
const int parent_radial_n, Mesh& mesh)
{
float uv_growth = parent.length / (parent.radius + .001f) / (2 * M_PI);
float uv_growth = parent.length / (parent.radius + .001f) / (2 * std::numbers::pi_v<float>);
for (size_t i = 0; i < 2;
i++) // recreating outer uvs (but without continuous (no looping back to x=0)
{
Expand All @@ -307,7 +315,8 @@ int add_child_base_uvs(float parent_uv_y, const Node& parent, const NodeChild& c
float uv_circle_radius = std::min((float)child_radial_n / parent_radial_n, uv_growth / 2) * .6f;
for (size_t i = 0; i < child_radial_n; i++) // inner uvs
{
float angle = (float)i / (child_radial_n - 1) * 2 * M_PI + M_PI;
float angle = (float)i / (child_radial_n - 1) * 2 * std::numbers::pi_v<float> +
std::numbers::pi_v<float>;
Vector2 uv_position = Vector2{cos(angle), sin(angle)} * uv_circle_radius + uv_circle_center;
mesh.uvs.push_back(uv_position);
}
Expand Down Expand Up @@ -337,9 +346,9 @@ CircleDesignator add_child_circle(const Node& parent, const NodeChild& child,
get_child_index_order(parent_base, child_radial_n, child_range, child, parent, mesh);

float child_twist = get_child_twist(child.node, parent);
int offset =
(int)(child_twist / (2 * M_PI) * child_radial_n - child_radial_n / 4 + child_radial_n) %
child_radial_n;
int offset = (int)(child_twist / (2 * std::numbers::pi_v<float>)*child_radial_n -
child_radial_n / 4 + child_radial_n) %
child_radial_n;

CircleDesignator child_base{(int)mesh.vertices.size(), (int)mesh.uvs.size(), child_radial_n};
child_base.uv_index = add_child_base_uvs(uv_y, parent, child, child_range, child_radial_n,
Expand Down
7 changes: 4 additions & 3 deletions m_tree/source/meshers/manifold_mesher/ManifoldMesher.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include "../base_types/TreeMesher.hpp"
#include <string>
#include <tuple>

#ifndef M_PI
Expand All @@ -14,9 +15,9 @@ class ManifoldMesher : public TreeMesher
public:
struct AttributeNames
{
inline static std::string smooth_amount = "smooth_amount";
inline static std::string radius = "radius";
inline static std::string direction = "direction";
inline static const std::string smooth_amount = "smooth_amount";
inline static const std::string radius = "radius";
inline static const std::string direction = "direction";
// Pivot Painter 2.0 attributes
inline static std::string stem_id = "stem_id";
inline static std::string hierarchy_depth = "hierarchy_depth";
Expand Down
46 changes: 43 additions & 3 deletions m_tree/source/tree/GrowthInfo.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,50 @@
#pragma once
#include <Eigen/Core>
#include <variant>

namespace Mtree
{
class GrowthInfo
using Vector3 = Eigen::Vector3f;

struct BranchGrowthInfo
{
public:
virtual ~GrowthInfo() {}
float desired_length;
float origin_radius;
Vector3 position;
float current_length = 0;
float deviation_from_rest_pose = 0;
float cumulated_weight = 0;
float age = 0;
bool inactive = false;
};

struct BioNodeInfo
{
enum class NodeType
{
Meristem,
Branch,
Cut,
Ignored,
Dormant,
Flower
} type;
float branch_weight = 0;
Vector3 center_of_mass;
Vector3 absolute_position;
float vigor_ratio = 1;
float vigor = 0;
int age = 0;
float philotaxis_angle = 0;
bool is_lateral = false;

BioNodeInfo(NodeType type = NodeType::Ignored, int age = 0, float philotaxis_angle = 0,
bool is_lateral = false)
: type(type), age(age), philotaxis_angle(philotaxis_angle), is_lateral(is_lateral)
{
}
};

using GrowthInfo = std::variant<std::monostate, BranchGrowthInfo, BioNodeInfo>;

} // namespace Mtree
2 changes: 1 addition & 1 deletion m_tree/source/tree/Node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Node
float length;
float radius;
int creator_id = 0;
std::unique_ptr<GrowthInfo> growthInfo = nullptr;
GrowthInfo growthInfo;

bool is_leaf() const;

Expand Down
Loading
Loading