Skip to content

Linux support: VSG runtime crash, ChLinkUniversal joint reactions, 5SA bimodal stability, build fixes - #7

Open
salhus wants to merge 9 commits into
mainfrom
salhus/linux-support-fixes
Open

salhus wants to merge 9 commits into
mainfrom
salhus/linux-support-fixes

Conversation

@salhus

@salhus salhus commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Five atomic fixes that get SEA-Stack building, running, rendering, and producing correct output on Linux for the first time. Discovered and validated while bringing up the 5SA bimodal demo on Ubuntu 24.04 with Chrono 10.0.0.

Updated after merging #17 (@dav-og), which corrected three of the original commits against the Chrono 10.0.0 baseline — see Revisions from #17 below.

Area What Why
adapters/chrono/CMakeLists.txt Prefer Chrono's bundled yaml-cpp ABI/symbol mismatch with system libyaml-cpp.so
adapters/chrono/src/simulation_export.cpp Log joint reactions for all ChLink subclasses Silent zeros for ChLinkUniversal etc.
data/.../5sa_bimodal.hydro.yaml Add heave/pitch/yaw damping + frequency-domain excitation Undamped heave mode runaway
CMakeLists.txt + scripts/unix/build.sh HDF5 module-mode discovery Ubuntu libhdf5-dev has no hdf5-config.cmake
scripts/unix/*.sh Mark executable Mode bit not set on a fresh clone

Validation

5SA bimodal heave runaway — bounded after fix

Before (broken):
  body3 max |v|  = diverged past 5 m/s at t=198.86s
  body3 max z    = +10.98 m at t=280s (chain literally in the air)
  run            = terminated at 280.7s

After (fixed):
  body3 max |v|  = 3.92 m/s at t=592.99s (bounded)
  body3 max z    = within ±2 m throughout
  run            = completed full 600s

Joint reactions — zero to physical

Before:
  joint_12..45 max |F| = 0.000 N for the entire 600s run (silent failure)

After:
  joint_12: max |F| = 192,471 N   max |T| = 158,075 N·m
  joint_23: max |F| = 211,687 N   max |T| = 220,279 N·m
  joint_34: max |F| = 201,941 N   max |T| = 311,303 N·m
  joint_45: max |F| = 169,830 N   max |T| = 401,657 N·m

  Internal sanity: reaction1_force == -reaction2_force to machine precision ✓
image

The 5sa demo running on Linux with both water and wireframe enabled, and NOT crashing at 280 s

VSG rendering — demos render and accept input

demo_sphere_decay and demo_oswec_irreg_waves both open windows, animate, accept GUI input (including the wireframe toggle), and shut down cleanly.

Screenshot from 2026-06-10 15-55-17

OSWEC irregular waves demo running on Linux with both water and wireframe enabled.

Build + tests (current branch head, Chrono 10.0.0 tag 9faf13dd8f)

./scripts/unix/build.sh --vsg --moordyn   → SUCCESS (HydroIO ON)
./scripts/unix/ctest_suite.sh unit        → 16/16 PASS
run_seastack .../rm3/irregular_waves      → 180 s complete
run_seastack .../5sa/irregular_waves      → 600 s complete
run_seastack .../5sa/bimodal              → 600 s complete

Revisions from #17

Three of the original commits were written against a newer Chrono main API rather than the 10.0.0 baseline this project targets. @dav-og caught these and #17 corrects them:

  • single_run.cpp — restored direct m_script_directory assignment. SetReferenceDirectory/m_file_handler do not exist in 10.0.0; m_script_directory is protected at chrono_parsers/yaml/ChParserYAML.h:144. Confirmed by building the base branch against 10.0.0, which fails with 'm_file_handler' was not declared in this scope.
  • vsg_config.h / vsg_water_surface.cpp — reverted the opaque-water workaround. kWaterOpacity is back to 0.55f. The vsg::Bin::add DepthSorted crash does not reproduce on Chrono 10.0.0; translucent water renders correctly, verified on VSG 1.1.11 as well as Chrono's pinned stack.
  • build.sh — the [WARN] HDF5 not found; HydroIO will be disabled path was too aggressive. It now emits [INFO] HDF5Dir unset; HydroIO left ON and lets CMake's module-mode FindHDF5 locate Ubuntu's libhdf5-dev. This pairs with the CMakeLists.txt fallback in this PR.

Root cause notes

yaml-cpp (adapters/chrono/CMakeLists.txt)

A system libyaml-cpp.so was being linked while headers came from Chrono's bundled copy, yielding link errors against private symbols (YAML::FpToString, YAML::Emitter::Write). The fix prefers Chrono::yaml-cpp whenever Chrono is present, falling back to the system package otherwise.

Joint reactions for non-LinkLock joints

simulation_export.cpp had:

if (auto* lock = dynamic_cast<::chrono::ChLinkLock*>(L)) {
    auto w1 = lock->GetReaction1();
    ...
} else {
    j.react_force_b1.insert(..., {0.0, 0.0, 0.0});  // silent zeros
}

But GetReaction1, GetReaction2, GetFrame1Abs, and GetFrame2Abs are all virtual on ChLinkBase. Every concrete joint provides them — ChLinkLock, ChLinkUniversal, ChLinkRevoluteSpherical, etc. Calling them on the base pointer works uniformly. 5SA uses ChLinkUniversal, so the previous code wrote zeros for the entire run.

Bimodal heave runaway

The bimodal YAML set linear_damping: [0, 100000, 0, 500000, 0, 0] — zero linear damping in surge, heave, pitch, and yaw — and no quadratic damping. The chain's rigid-body heave mode is essentially undamped (PTOs only damp relative pitch between segments). The bimodal spectrum contains energy near the heave natural frequency, so heave amplitude grew over ~200 s until the entire 180 m chain breached the water surface.

The spreading case is a near-identical multi-body configuration that runs to completion; the fix mirrors its damping values into bimodal. Re-tuning the bimodal sea state to avoid the heave-resonant band would be an equally valid fix; matching spreading is the minimal, principled choice.

HDF5 discovery

Ubuntu's libhdf5-dev ships no hdf5-config.cmake, so config-mode find_package(HDF5 CONFIG) fails. Chrono's own config may additionally set HDF5_DIR to NOTFOUND when Chrono itself used module mode. The fix clears that cache entry and falls back to module-mode find_package(HDF5 COMPONENTS CXX).

Known issue (pre-existing, not introduced here)

run_seastack aborts with double free or corruption (out) / exit 134 during process teardown, after the simulation completes and HDF5 output is flushed. Reproduces headless (--nogui, VSG never initialized) and with a clean build, so it is unrelated to the VSG changes in this PR. Output files are complete and readable. Filing separately — worth prioritising, since the "Simulation complete" banner prints before the abort and campaign/CI loops may not check the exit code.

Out of scope (will file separately)

  • Environment prereqs: Chrono must be built with CH_USE_SIMD=OFF, and runtime needs VSG_FILE_PATH to include Chrono's data dir. Both belong in a docs/linux-setup.md follow-up.
  • 5SA C++ demo helper damping (demos/5sa/five_sa_model_setup.h): uses the same low damping values that broke the YAML, but neither C++ demo currently triggers heave runaway (different wave forcing). Out of scope here to keep the PR tight.
  • Chrono version tracking: m_script_directory is a protected member, not public API, so a future Chrono tag could rename or restructure it without that counting as a breaking change — the SetReferenceDirectory/m_file_handler API on Chrono main suggests that refactor is already underway. Worth a bump-time checklist item since SEA-Stack tags releases against tagged Chrono releases.

Salman Husain added 5 commits June 10, 2026 16:41
… API

Two small build fixes that together get SEA-Stack compiling cleanly on
Linux against Chrono 10.0.0.

* adapters/chrono/CMakeLists.txt — prefer Chrono::yaml-cpp over a
  system yaml-cpp when both are visible. Mixing Chrono's bundled
  yaml-cpp headers (from chrono_thirdparty/yaml-cpp/include) with
  a system libyaml-cpp.so triggers link errors against internal
  symbols such as YAML::FpToString and YAML::Emitter::Write.

* apps/seastack/single_run.cpp — Chrono 10 moved the protected
  m_script_directory member of ChParserMbsYAML into an internal file
  handler; the script directory must now be set via
  m_file_handler.SetReferenceDirectory(dir).
The generic joint reaction extraction in SimulationExporter::RecordStep
was guarded by `dynamic_cast<ChLinkLock*>`, which silently fell through
to an "all zeros" fallback for every other ChLink subclass -- including
ChLinkUniversal (used by the 5SA articulated WEC demos), ChLinkRevolute,
ChLinkRevoluteSpherical, and others.

GetReaction1, GetReaction2, GetFrame1Abs, and GetFrame2Abs are virtual
on ChLinkBase, so they resolve uniformly for any concrete joint type.
Calling them on the base ChLink pointer fixes the export for all
current and future joint types.

Validation (5SA bimodal, 600s):
  Before:  joint_12..45  max |F| =       0.000 N for entire run
  After:   joint_12      max |F| =  192,471 N    max |T| =  158,075 N*m
           joint_23      max |F| =  211,687 N    max |T| =  220,279 N*m
           joint_34      max |F| =  201,941 N    max |T| =  311,303 N*m
           joint_45      max |F| =  169,830 N    max |T| =  401,657 N*m
  Internal consistency: reaction1_force == -reaction2_force to machine
  precision (Newton's third law).
…citation

The bimodal case had linear_damping = [0, 100000, 0, 500000, 0, 0]
(zero linear damping in surge, heave, pitch, and yaw) and no quadratic
damping at all. The bimodal swell + wind-sea spectrum contains energy
near the chain's rigid-body heave natural frequency, and with no
restoring damping in heave the mode integrated unboundedly: at
t=198.86s body3 crossed 5 m/s, and by t=280s body z-positions exceeded
+10 m above SWL with the entire 144-m chain out of the water.

The fix mirrors the damping already present in the spreading case (a
near-identical multi-body configuration that runs to completion):

  linear_damping:    [20000, 80000, 80000, 500000, 200000, 200000]
  quadratic_damping: [10000, 40000, 40000, 250000, 100000, 100000]

It also adds an explicit `excitation: frequency_domain` block, again
mirroring spreading. The bimodal swell and wind-sea have different
headings, so excitation must be evaluated in the frequency domain --
this is auto-selected by the loader when wave components span multiple
headings, but stating it explicitly matches the parity with spreading
and makes intent clear.

Validation (run_seastack 600s, no MoorDyn):
  Before: body3 max |v| = diverged past 5.0 m/s at t=198.86s
          body3 max z   = +10.98 m at t=280s (chain in the air)
          run terminated at 280.7s
  After:  body3 max |v| = 3.92 m/s at t=592.99s (bounded)
          body3 max z   = within +/- 2 m throughout
          run completed full 600s, clean exit, 2m wallclock
…rash

When the animated water surface is added to the VSG scene graph after
ChVisualSystemVSG::Initialize() -- which happens because the wave model
is set up at runtime, not pre-bind -- Chrono's wrapIfTransparent helper
wraps the new node in a vsg::DepthSorted set to bin 10 if the material
opacity is less than 1.0. The first record traversal that encounters
this node then segfaults inside vsg::Bin::add() because the bin and
its pipeline state were not present when Initialize() finalized the
view's render path.

Stack at the crash:
  #0  vsg::Bin::add(State*, double, Node const*)
  #1  vsg::RecordTraversal::apply(MatrixTransform const&)
  #2  vsg::Group::traverse(RecordTraversal&)
   ...
  #N  chrono::vsg3d::ChVisualSystemVSG::Render()

Water surface (kWaterOpacity=0.55) and wireframe overlay
(SetOpacity(0.35)) both hit this. Setting both to 1.0 keeps the nodes
out of the bin-10 DepthSorted path, so neither demos nor run_seastack
crash on first render, including with the in-GUI wireframe toggle.

Tradeoff: the water is opaque so submerged geometry is no longer
visible through it. The proper fix is to compile any runtime-added
transparent subtree against the live viewer (Chrono's ChShapeBuilderVSG
owns an m_compileTraversal for this purpose but it is not exposed via
SEA-Stack's public API surface). Tracking as a follow-up.
Each script under scripts/unix/ was checked in with mode 100644 (no
executable bit), so `./scripts/unix/build.sh` fails on a fresh Linux
clone. Setting +x on all of them makes the standard invocation pattern
work as documented.
@salhus
salhus force-pushed the salhus/linux-support-fixes branch from e17d2b9 to 5c2b908 Compare June 10, 2026 22:59
@salhus
salhus marked this pull request as ready for review June 10, 2026 23:03
@salhus

salhus commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@dav-og — when you have a moment, this brings SEA-Stack up on Linux + adds joint reactions for ChLinkUniversal. Validation data in the description; happy to walk through any of the four bugs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Improves Linux build, test, runtime, rendering, and simulation correctness.

Changes:

  • Adds Unix build, test-suite, and smoke-test tooling.
  • Fixes Chrono YAML compatibility and generic joint-reaction export.
  • Stabilizes the 5SA demo and avoids VSG transparency crashes.

Reviewed changes

Copilot reviewed 6 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/unix/build.sh Adds Unix build driver.
scripts/unix/ctest_suite.sh Adds unified CTest runner.
scripts/unix/run_benchmarks.sh Adds benchmark wrapper.
scripts/unix/run_chrono_free_tests.sh Adds Chrono-free test wrapper.
scripts/unix/run_comparison_tests.sh Adds comparison-test wrapper.
scripts/unix/run_regression_tests.sh Adds regression-test wrapper.
scripts/unix/run_seastack_demo_smoke.sh Adds headless demo smoke test.
scripts/unix/run_unit_tests.sh Adds unit-test wrapper.
scripts/unix/run_verification_tests.sh Adds verification-test wrapper.
data/demos/run_seastack/5sa/bimodal/5sa_bimodal.hydro.yaml Adds damping and frequency-domain excitation.
apps/seastack/single_run.cpp Updates Chrono parser directory handling.
apps/seastack/gui/vsg_water_surface.cpp Makes wireframe material opaque.
apps/seastack/gui/vsg_config.h Makes water opaque to avoid VSG crashes.
adapters/chrono/src/simulation_export.cpp Exports reactions for generic joints.
adapters/chrono/CMakeLists.txt Prefers Chrono’s bundled yaml-cpp.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1094 to +1096
auto w1 = L->GetReaction1();
auto F1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.force);
auto T1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.torque);
Comment thread apps/seastack/single_run.cpp Outdated
a0d34cc switched HCParser::SetScriptDir to
m_file_handler.SetReferenceDirectory(dir). That member exists on Chrono
main, but not on the Chrono 10.0.0 release that SEA-Stack documents as
its baseline, so the app fails to compile against 10.0.0.

Restore the protected m_script_directory assignment, which is the
Chrono 10.0.0 API. The yaml-cpp half of a0d34cc is kept unchanged --
preferring Chrono::yaml-cpp over a system yaml-cpp is correct on both
Chrono versions and avoids the YAML::FpToString / YAML::Emitter::Write
link errors Salhus reported.

Verified on Ubuntu 22.04 against Chrono 10.0.0 built with Chrono's
pinned VSG stack (vsg 1.1.11, vsgXchange 1.1.7, vsgImGui 0.7.0).
…Sorted crash"

This reverts 2172d0b, restoring the intended translucent free surface
(kWaterOpacity = 0.55) and the faint translucent wireframe overlay
(SetOpacity(0.35)).

The DepthSorted / vsg::Bin::add crash does not reproduce against the
documented Chrono 10.0.0 baseline. Chrono 10.0.0 has no
wrapIfTransparent() helper and does not wrap runtime-added nodes in
vsg::DepthSorted, so a sub-1.0 material opacity never reaches the bin-10
path described in 2172d0b. That code was added on Chrono main after the
10.0.0 release, which is the most likely reason the crash was seen.

Validated on Ubuntu 22.04.5 + RTX 3060 against Chrono 10.0.0 built with
Chrono's pinned VSG stack (vsg 1.1.11, vsgXchange 1.1.7, vsgImGui 0.7.0,
glslang 15.4.0): the 5SA bimodal GUI runs with translucent water and the
wireframe toggle without any FATAL or renderer crash.

Please retest on your Linux machine against Chrono 10.0.0 specifically.
If the crash returns there, the VSG stack version is the thing to
compare first, and we should fix the runtime compile traversal rather
than force opacity to 1.0.
On Ubuntu, scripts/unix/build.sh treated an empty HDF5Dir in
build-config.json as "HDF5 missing" and forced
SEASTACK_ENABLE_HYDRO_IO=OFF. CMake was therefore never given a chance
to find the apt libhdf5-dev package, and builds that did want HydroIO
failed later on a missing h5_reader.h rather than at configure time.
The only workaround was an undocumented manual HDF5Dir entry.

Two small changes:

* scripts/unix/build.sh - leave HydroIO ON when HDF5Dir is unset and
  print an INFO note instead. CMake's FindHDF5 then runs normally, and a
  genuinely missing HDF5 fails at configure time with a clear error.

* CMakeLists.txt - Chrono's ChronoConfig can leave HDF5_DIR cached as
  NOTFOUND when Chrono itself found HDF5 via module mode. Clear that
  stale value, then probe module-mode find_package(HDF5 COMPONENTS CXX)
  when HydroIO is enabled and HDF5 is not yet found.

Validated on Ubuntu 22.04.5 with "HDF5Dir": "" and Chrono 10.0.0 + VSG:
configure reports HydroIO ON, libseastack_hydro_io.a builds, and
run_seastack links libhdf5_serial 1.10.7 via module mode while HDF5_DIR
stays NOTFOUND.
@dav-og dav-og mentioned this pull request Sep 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Exported reaction frames conflict with their HDF5 metadata, and the smoke test propagates the documented teardown failure.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

adapters/chrono/src/simulation_export.cpp:1094

  • The core fix broadens reaction export from ChLinkLock to every ChLink, but the existing test_export_redesign suite contains no universal/generic-joint reaction assertion. Add an automated case with a non-lock joint that verifies nonzero reaction datasets, equal-and-opposite world reactions (if that is the intended frame), and the advertised frame metadata; otherwise this Linux regression can silently return.
                auto w1 = L->GetReaction1();
  • Files reviewed: 5/13 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +1095 to +1096
auto F1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.force);
auto T1 = L->GetFrame1Abs().TransformDirectionLocalToParent(w1.torque);
Comment on lines +1107 to +1108
auto F2 = L->GetFrame2Abs().TransformDirectionLocalToParent(w2.force);
auto T2 = L->GetFrame2Abs().TransformDirectionLocalToParent(w2.torque);
@salhus

salhus commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @dav-og I saw the meeting notes. I think the PR 7 is all ready to merge. I already merged and tested your PR last week. Could you please update the meeting notes?

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