FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0023 — SimObject has no render teardown hook, so a SimObject cannot release its Ogre objects before the Ogre root is gone
ID 0023
Class API
Severity 2
Status ready
Models —
Found 2026-09-25, filed from fhsim_fishery FISH-0041 on the owner's decision
Decision needed — (Karl-Johan Reite decided 2026-09-25 to file this as ready: add the hook)

Evidence

The visualisation part of the SimObject interface is two pure virtuals and nothing else (include/fhsim/simobject/SimObjectInclude.h:305-321):

#ifdef FH_VISUALIZATION
virtual void RenderInit(Ogre::Root* ogreRoot, ISimObjectCreator* creator) = 0;
virtual void RenderUpdate(double T, const double* X) = 0;
#endif

IModelVisualizationHooks (src/engine/model/IModelVisualizationHooks.h:31-54) mirrors this: RenderInit and RenderUpdate, no release. ModelStructure::RenderInit (src/engine/model/ModelStructure.cpp:221-233) forwards to every SimObject.

At the end of a visualised run VisualizationObserver::OnSimulationEnd (src/visual/observer/VisualizationObserver.cpp:70-73) calls FhVisualization::Shutdown (src/visual/renderer/FhVisualization.cpp:291-355), which destroys the scene manager (:342) and, when it owns it, deletes the Ogre root (:351-352). The SimObjects are destroyed later, with the model. So the only place a SimObject can release the scene nodes, entities and manual objects it created in RenderInit is its destructor, and by then the scene manager it cached is freed memory.

Downstream this has already crashed twice and is now guarded case by case:

  • fhsim_fishery FISH-0041: adding the obvious release to Auv::~Auv segfaulted FhVis on exit. Auv (src/auv/Auv.cpp:30-51, used at :277) and SensorEchosounder (src/auv/SensorEchosounder.cpp:60-81, used at :250) now release only when SceneIsStillAlive(m_sceneMgr), i.e. Ogre still has the "main" scene manager. fhsim_fishery AGENTS.md makes that guard a house rule until this hook exists.
  • fhsim_environment ENV-0023 (resolved, 1507a3f and 5bf8d11): visual::ScalarPlane, ScalarVolume and FlowPlane segfaulted at shutdown in Release(); each now returns early when !OgreRootIsAlive() (src/environment/visual/OgreLifetime.h:29, used in ScalarVolume.cpp:112, ScalarPlane.cpp:59, FlowPlane.cpp:108).

Effect

Every visualised library must know that the Ogre root dies before its SimObjects and guard each destructor; one that does not segfaults every visualised run at shutdown. With the guard, geometry is never released by its owner at all, only swept up with the scene. The rule is not visible in the interface.

Possible fix

  1. Add to SimObject in SimObjectInclude.h, under FH_VISUALIZATION, next to RenderUpdate:

    virtual void RenderRelease() { }

    Empty default, not pure, so no existing SimObject has to change.

  2. Add RenderRelease() to IModelVisualizationHooks, implement it in ModelStructure by calling RenderRelease() on every SimObject (reverse creation order), and add an empty override to SimObjectIterator if it needs one.
  3. Call m_renderHooks->RenderRelease() in VisualizationObserver::OnSimulationEnd and VisualizationSDLObserver::OnSimulationEnd (and any other path that reaches FhVisualization::Shutdown, including its destructor and the FMU teardown) before m_visualization.Shutdown(), only when RenderInit ran.

Follow-ups in other repositories, once this is released:

  • fhsim_fishery: move the release code of Auv::~Auv and SensorEchosounder::~SensorEchosounder into RenderRelease() overrides, and drop the SceneIsStillAlive guards and the AGENTS.md house rule (FISH-0041).
  • fhsim_environment: have the owning SimObjects call ScalarVolume::Release(), ScalarPlane and FlowPlane release from RenderRelease(); the OgreRootIsAlive() guards from ENV-0023 can then stay as a safety net or go.

Test that would prove it

A visualisation-build test with a stub SimObject that records RenderInit, RenderRelease and its destructor, run through ModelStructure and a visualisation observer (headless if available): RenderRelease is called exactly once, after the last RenderUpdate, while Ogre::Root::getSingletonPtr() is non-null and hasSceneManager("main") is true, and before the destructor. A second case with a SimObject that does not override it shows the default is harmless.

Risk

Low for existing code: the default is empty. The ordering must hold on every shutdown path (window closed, user stop, FMU terminate, exception unwinding); a path that skips the call just falls back to today's behaviour, which is why downstream guards may stay until each path is covered. An FMU that reuses an existing Ogre root (m_rootOwned == false, FhVisualization.cpp:141-144) must still get the call, since the scene manager is destroyed there too.