Skip to content
Open
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
135 changes: 117 additions & 18 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,97 @@ namespace sofapython3
using sofa::core::objectmodel::Event;
using sofa::core::objectmodel::BaseObject;

Controller_Trampoline::Controller_Trampoline() = default;

Controller_Trampoline::~Controller_Trampoline()
{
// Clean up Python objects while holding the GIL
if (m_cacheInitialized)
{
PythonEnvironment::gil acquire {"~Controller_Trampoline"};
m_methodCache.clear();
m_onEventMethod = py::object();
m_pySelf = py::object();
}
}

void Controller_Trampoline::initializePythonCache()
{
if (m_cacheInitialized)
return;

// Must be called with GIL held
m_pySelf = py::cast(this);

// Cache the fallback "onEvent" method if it exists
if (py::hasattr(m_pySelf, "onEvent"))
{
py::object fct = m_pySelf.attr("onEvent");
if (PyCallable_Check(fct.ptr()))
{
m_hasOnEvent = true;
m_onEventMethod = fct;
}
}

m_cacheInitialized = true;
}

py::object Controller_Trampoline::getCachedMethod(const std::string& methodName)
{
// Must be called with GIL held and cache initialized

// Check if we've already looked up this method
auto it = m_methodCache.find(methodName);
if (it != m_methodCache.end())
{
return it->second;
}

// First time looking up this method - check if it exists
py::object method;
if (py::hasattr(m_pySelf, methodName.c_str()))
{
py::object fct = m_pySelf.attr(methodName.c_str());
if (PyCallable_Check(fct.ptr()))
{
method = fct;
}
}

// Cache the result (even if empty, to avoid repeated hasattr checks)
m_methodCache[methodName] = method;
return method;
}

bool Controller_Trampoline::callCachedMethod(const py::object& method, Event* event)
{
// Must be called with GIL held
if (f_printLog.getValue())
{
std::string eventStr = py::str(PythonFactory::toPython(event));
msg_info() << "on" << event->getClassName() << " " << eventStr;
}

py::object result = method(PythonFactory::toPython(event));
if (result.is_none())
return false;

return py::cast<bool>(result);
}

std::string Controller_Trampoline::getClassName() const
{
PythonEnvironment::gil acquire {"getClassName"};
// Get the actual class name from python.
return py::str(py::cast(this).get_type().attr("__name__"));

// Use cached self if available, otherwise cast
if (m_cacheInitialized && m_pySelf)
{
return py::str(py::type::of(m_pySelf).attr("__name__"));
}

// Fallback for when cache isn't initialized yet
return py::str(py::type::of(py::cast(this)).attr("__name__"));
}

void Controller_Trampoline::draw(const sofa::core::visual::VisualParams* params)
Expand All @@ -55,6 +141,8 @@ void Controller_Trampoline::draw(const sofa::core::visual::VisualParams* params)
void Controller_Trampoline::init()
{
PythonEnvironment::executePython(this, [this](){
// Initialize the Python object cache on first init
initializePythonCache();
PYBIND11_OVERLOAD(void, Controller, init, );
});
}
Expand Down Expand Up @@ -92,25 +180,36 @@ bool Controller_Trampoline::callScriptMethod(

void Controller_Trampoline::handleEvent(Event* event)
{
PythonEnvironment::executePython(this, [this,event](){
py::object self = py::cast(this);
std::string name = std::string("on")+event->getClassName();
/// Is there a method with this name in the class ?
if( py::hasattr(self, name.c_str()) )
PythonEnvironment::executePython(this, [this, event](){
// Ensure cache is initialized (in case init() wasn't called or
// handleEvent is called before init)
if (!m_cacheInitialized)
{
initializePythonCache();
}

// Build the event-specific method name (e.g., "onAnimateBeginEvent")
std::string methodName = std::string("on") + event->getClassName();

// Try to get the cached method for this specific event type
py::object method = getCachedMethod(methodName);

if (method)
{
py::object fct = self.attr(name.c_str());
if (PyCallable_Check(fct.ptr())) {
bool isHandled = callScriptMethod(self, event, name);
if(isHandled)
event->setHandled();
return;
}
// Found a specific handler for this event type
bool isHandled = callCachedMethod(method, event);
if (isHandled)
event->setHandled();
return;
}

/// Is the fallback method available.
bool isHandled = callScriptMethod(self, event, "onEvent");
if(isHandled)
event->setHandled();
// Fall back to the generic "onEvent" method if available
if (m_hasOnEvent)
{
bool isHandled = callCachedMethod(m_onEventMethod, event);
if (isHandled)
event->setHandled();
}
});
}

Expand Down
31 changes: 31 additions & 0 deletions bindings/Sofa/src/SofaPython3/Sofa/Core/Binding_Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

#include <pybind11/pybind11.h>
#include <sofa/core/behavior/BaseController.h>
#include <unordered_map>
#include <string>

namespace sofapython3 {

Expand All @@ -41,6 +43,9 @@ class Controller_Trampoline : public Controller
public:
SOFA_CLASS(Controller_Trampoline, Controller);

Controller_Trampoline();
~Controller_Trampoline() override;

void init() override;
void reinit() override;
void draw(const sofa::core::visual::VisualParams* params) override;
Expand All @@ -50,8 +55,34 @@ class Controller_Trampoline : public Controller
std::string getClassName() const override;

private:
/// Initializes the Python object cache (m_pySelf and method cache)
void initializePythonCache();

/// Returns a cached method if it exists, or an empty object if not
pybind11::object getCachedMethod(const std::string& methodName);

/// Calls a cached Python method with the given event
bool callCachedMethod(const pybind11::object& method, sofa::core::objectmodel::Event* event);

/// Legacy method for uncached calls (fallback)
bool callScriptMethod(const pybind11::object& self, sofa::core::objectmodel::Event* event,
const std::string& methodName);

/// Cached Python self reference (avoids repeated py::cast(this))
pybind11::object m_pySelf;

/// Cache of Python method objects, keyed by method name
/// Stores the method object if it exists, or an empty object if checked and not found
std::unordered_map<std::string, pybind11::object> m_methodCache;

/// Flag indicating whether the fallback "onEvent" method exists
bool m_hasOnEvent = false;

/// Cached reference to the fallback "onEvent" method
pybind11::object m_onEventMethod;

/// Flag indicating whether the cache has been initialized
bool m_cacheInitialized = false;
};

void moduleAddController(pybind11::module &m);
Expand Down
45 changes: 45 additions & 0 deletions examples/benchmarks/emptyMultipleControllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Sofa

g_nb_controllers = 10
g_nb_steps = 10000

class EmptyController(Sofa.Core.Controller):

def __init__(self, *args, **kwargs):
Sofa.Core.Controller.__init__(self, *args, **kwargs)

# Default Events *********************************************
def onAnimateBeginEvent(self, event): # called at each begin of animation step
# print(f"{self.name.value} : onAnimateBeginEvent")
pass

def createScene(root):
root.dt = 0.01
root.bbox = [[-1, -1, -1], [1, 1, 1]]
root.addObject('DefaultVisualManagerLoop')
root.addObject('DefaultAnimationLoop')


for i in range(g_nb_controllers):
root.addObject(EmptyController(name=f"MyEmptyController{i}"))


def main():
root = Sofa.Core.Node("root")
createScene(root)
Sofa.Simulation.initRoot(root)

# Import the time library
import time
start = time.time()
for iteration in range(g_nb_steps):
Sofa.Simulation.animate(root, root.dt.value)
end = time.time()

print(f"Scene with {g_nb_controllers} controllers and {g_nb_steps} steps took {end - start} seconds.")

print("End of simulation.")


if __name__ == '__main__':
main()
Loading