From d6c31d0f2da3904d93f6c62483c9c23e5e8cc1df Mon Sep 17 00:00:00 2001 From: Mohcine Chraibi Date: Tue, 11 Aug 2026 11:08:42 +0200 Subject: [PATCH 1/2] Serialize HDF5 access and stop tearing down the library The bundled HDF5 is built without thread safety (Threadsafety: OFF in libhdf5.settings), yet geometry and trajectory loading can read the same .h5 concurrently from different threads. Worse, CloseFile(), OpenFile()'s error path and DetectFormat() called H5close(), which shuts down the HDF5 library process-wide and crashes any reader that is mid-operation. Observed as a SIGSEGV at address 0x20 when one thread ran H5close() while another was still inside H5open(): Hdf5DataPlugin!H5P__access_class -> H5P_create_id -> H5T_init -> H5_init_library -> H5open Hdf5DataPlugin!FHdf5SimulationReader::OpenFile ProjectMobius!FProcessSimulationDataRunnable::LoadAndDeserializeHDF5File - Guard every public reader method with a process-wide recursive critical section. - Remove all H5close() calls; the library now lives for the process. H5Fclose() still closes individual files. --- .../Private/Hdf5SimulationReader.cpp | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/Hdf5DataPlugin/Private/Hdf5SimulationReader.cpp b/UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/Hdf5DataPlugin/Private/Hdf5SimulationReader.cpp index 5acbaece0..96c3333a3 100644 --- a/UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/Hdf5DataPlugin/Private/Hdf5SimulationReader.cpp +++ b/UnrealFolder/ProjectMobius/Plugins/Hdf5DataPlugin/Source/Hdf5DataPlugin/Private/Hdf5SimulationReader.cpp @@ -23,9 +23,26 @@ #include "Hdf5SimulationReader.h" #include "Misc/Paths.h" +#include "Misc/ScopeLock.h" DEFINE_LOG_CATEGORY_STATIC(LogHdf5SimulationReader, Log, All); +namespace +{ + /** + * The bundled HDF5 library is built without thread safety (Threadsafety: OFF in + * libhdf5.settings), so no two threads may execute HDF5 code concurrently. + * Geometry and trajectory loading both read the same .h5 from different threads, + * so every public reader method serializes on this process-wide lock. + * The lock is recursive on all UE platforms, so public methods may call each other. + */ + FCriticalSection& GetHdf5LibraryLock() + { + static FCriticalSection Hdf5LibraryLock; + return Hdf5LibraryLock; + } +} + FHdf5SimulationReader::FHdf5SimulationReader() { } @@ -37,6 +54,8 @@ FHdf5SimulationReader::~FHdf5SimulationReader() bool FHdf5SimulationReader::OpenFile(const FString& FilePath) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + // Close any previously open file CloseFile(); @@ -58,7 +77,8 @@ bool FHdf5SimulationReader::OpenFile(const FString& FilePath) if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("Failed to open HDF5 file: %s"), *FilePath); - H5close(); + // Do not call H5close() here: it tears down the HDF5 library process-wide, + // crashing any other reader that is mid-operation. return false; } @@ -111,10 +131,13 @@ bool FHdf5SimulationReader::OpenFile(const FString& FilePath) void FHdf5SimulationReader::CloseFile() { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId >= 0) { H5Fclose(FileId); - H5close(); + // Deliberately no H5close(): that shuts down the HDF5 library for the whole + // process, not just this file, and other readers may still be active. FileId = -1; DetectedFormat = EHdf5FormatType::Unknown; TimestepCount = 0; @@ -276,6 +299,8 @@ bool FHdf5SimulationReader::ReadStringAttribute(hid_t GroupId, const char* AttrN bool FHdf5SimulationReader::ReadMetadata(FHdf5SimulationMetadata& OutMetadata) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -320,6 +345,8 @@ bool FHdf5SimulationReader::ReadMetadata(FHdf5SimulationMetadata& OutMetadata) bool FHdf5SimulationReader::ReadEntities(TArray& OutEntities) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -417,6 +444,8 @@ bool FHdf5SimulationReader::ReadEntities(TArray& OutEntities) bool FHdf5SimulationReader::ReadTimesteps(TArray& OutTimesteps) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -453,6 +482,8 @@ bool FHdf5SimulationReader::ReadTimesteps(TArray& OutTimesteps) bool FHdf5SimulationReader::ReadSamplesPerTimestep(TArray& OutSamplesPerTimestep) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -496,6 +527,8 @@ bool FHdf5SimulationReader::ReadSamplesPerTimestep(TArray& OutSamplesPerT bool FHdf5SimulationReader::ReadAllSamples(TArray& OutSamples, bool* OutHasRotationField, bool* OutHasSpeedField) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -608,6 +641,8 @@ bool FHdf5SimulationReader::ReadAllSamples(TArray& OutSamples, bool FHdf5SimulationReader::ReadSamplesForTimestepRange(int32 StartTimestep, int32 EndTimestep, TArray& OutSamples) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -718,6 +753,8 @@ EHdf5FormatType FHdf5SimulationReader::DetectFormat(const FString& FilePath) return EHdf5FormatType::Unknown; } + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + FTCHARToUTF8 FilePathUtf8(*FilePath); // Check if it's a valid HDF5 file @@ -731,7 +768,6 @@ EHdf5FormatType FHdf5SimulationReader::DetectFormat(const FString& FilePath) hid_t TempFileId = H5Fopen(FilePathUtf8.Get(), H5F_ACC_RDONLY, H5P_DEFAULT); if (TempFileId < 0) { - H5close(); return EHdf5FormatType::Unknown; } @@ -750,7 +786,6 @@ EHdf5FormatType FHdf5SimulationReader::DetectFormat(const FString& FilePath) } H5Fclose(TempFileId); - H5close(); return Result; } @@ -759,6 +794,8 @@ EHdf5FormatType FHdf5SimulationReader::DetectFormat(const FString& FilePath) bool FHdf5SimulationReader::ReadJuelichMetadata(FHdf5JuelichMetadata& OutMetadata) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -845,6 +882,8 @@ bool FHdf5SimulationReader::ReadJuelichMetadata(FHdf5JuelichMetadata& OutMetadat bool FHdf5SimulationReader::ReadJuelichTrajectories(TArray& OutRecords) { + FScopeLock Hdf5Guard(&GetHdf5LibraryLock()); + if (FileId < 0) { UE_LOG(LogHdf5SimulationReader, Error, TEXT("No file is open")); @@ -924,6 +963,8 @@ bool FHdf5SimulationReader::ReadJuelichTrajectories(TArray Date: Tue, 11 Aug 2026 11:11:12 +0200 Subject: [PATCH 2/2] Load embedded geometry when an HDF5 agent file is selected HDF5 simulation files carry the scene geometry in the root wkt_geometry attribute, but selecting one as pedestrian data only loaded the trajectories; the geometry required picking the same file a second time in the Geometry slot (#11). When a selected .h5 contains wkt_geometry, adopt it as the geometry source too, so a single selection loads both. The mesh widget now listens to OnMeshFileChanged so its path display reflects the adopted file. Help texts updated to include .h5 among the supported types. --- .../Interfaces/ProjectMobiusInterface.cpp | 42 +++++++++++++++++++ .../UI/LoadSave/LoadAgentDataWidget.cpp | 2 +- .../Private/UI/LoadSave/LoadMeshWidget.cpp | 24 ++++++++++- .../Public/UI/LoadSave/LoadMeshWidget.h | 9 ++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/UnrealFolder/ProjectMobius/Source/MobiusCore/Private/Interfaces/ProjectMobiusInterface.cpp b/UnrealFolder/ProjectMobius/Source/MobiusCore/Private/Interfaces/ProjectMobiusInterface.cpp index ff01a6a7b..ea026f066 100644 --- a/UnrealFolder/ProjectMobius/Source/MobiusCore/Private/Interfaces/ProjectMobiusInterface.cpp +++ b/UnrealFolder/ProjectMobius/Source/MobiusCore/Private/Interfaces/ProjectMobiusInterface.cpp @@ -25,9 +25,48 @@ #include "Interfaces/ProjectMobiusInterface.h" #include "Engine/GameInstance.h" #include "GameInstances/ProjectMobiusGameInstance.h" +#include "Hdf5SimulationReader.h" +#include "Misc/Paths.h" // Add default functionality here for any IProjectMobiusInterface functions that are not pure virtual. +namespace +{ + /** + * HDF5 simulation files can carry the scene geometry alongside the trajectories in the + * root "wkt_geometry" attribute. When such a file is selected as the pedestrian data + * source, adopt it as the geometry source too so a single selection loads both. + * @param GameInst - Mobius game instance to update + * @param DataPath - Full path of the newly selected pedestrian data file + */ + void AdoptEmbeddedGeometrySource(UProjectMobiusGameInstance* GameInst, const FString& DataPath) + { + if (!GameInst || !DataPath.EndsWith(TEXT(".h5"), ESearchCase::IgnoreCase)) + { + return; + } + + FHdf5SimulationReader Reader; + if (!Reader.OpenFile(DataPath)) + { + return; + } + + FString WktGeometry; + const bool bHasGeometry = Reader.ReadWktGeometry(WktGeometry) && !WktGeometry.IsEmpty(); + Reader.CloseFile(); + + if (!bHasGeometry) + { + UE_LOG(LogTemp, Log, TEXT("No embedded geometry in %s, geometry selection left unchanged"), *DataPath); + return; + } + + GameInst->SetSimulationMeshFilePath(DataPath); + GameInst->SetSimulationMeshFileName(FPaths::GetCleanFilename(DataPath)); + } +} + UProjectMobiusGameInstance* IProjectMobiusInterface::GetMobiusGameInstance(UWorld* World) { if(!World) @@ -111,6 +150,9 @@ void IProjectMobiusInterface::UpdateMobiusGameInstancePedestrianData(UWorld* Wor // Set the pedestrian data file name MobiusGameInst->SetPedestrianDataFileName(FPaths::GetCleanFilename(CompleteDataPath)); + + // HDF5 files can embed the geometry, load it from the same file + AdoptEmbeddedGeometrySource(MobiusGameInst, CompleteDataPath); } void IProjectMobiusInterface::GetMobiusGameInstanceMeshDataFile(UWorld* World, FString& OutCompleteDataPath, diff --git a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadAgentDataWidget.cpp b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadAgentDataWidget.cpp index b10b0056b..9fb947f80 100644 --- a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadAgentDataWidget.cpp +++ b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadAgentDataWidget.cpp @@ -132,7 +132,7 @@ void ULoadAgentDataWidget::DialogClosed(const FString& AgentFilePath, const FStr Feedback->ReportError( FText::FromString("Invalid Agent Data File"), FText::FromString("Unsupported agent data file type selected."), - FText::FromString("Supported types: .json"), + FText::FromString("Supported types: .json, .h5"), FText::FromString("Load Agent Data")); } UE_LOG(LogTemp, Warning, TEXT("The file dialog was canceled or an error occurred")); diff --git a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadMeshWidget.cpp b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadMeshWidget.cpp index ce069e21d..b3218b7a6 100644 --- a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadMeshWidget.cpp +++ b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Private/UI/LoadSave/LoadMeshWidget.cpp @@ -25,11 +25,33 @@ #include "UI/LoadSave/LoadMeshWidget.h" #include "Subsystems/NativeFileDialogSubsystem.h" #include "Subsystems/MobiusUserFeedbackSubsystem.h" +#include "GameInstances/ProjectMobiusGameInstance.h" void ULoadMeshWidget::NativeConstruct() { Super::NativeConstruct(); + if (UProjectMobiusGameInstance* MobiusGameInst = IProjectMobiusInterface::GetMobiusGameInstance(GetWorld())) + { + MobiusGameInst->OnMeshFileChanged.AddUniqueDynamic(this, &ULoadMeshWidget::OnGameInstanceMeshFileChanged); + } +} + +void ULoadMeshWidget::NativeDestruct() +{ + if (UProjectMobiusGameInstance* MobiusGameInst = IProjectMobiusInterface::GetMobiusGameInstance(GetWorld())) + { + MobiusGameInst->OnMeshFileChanged.RemoveDynamic(this, &ULoadMeshWidget::OnGameInstanceMeshFileChanged); + } + + Super::NativeDestruct(); +} + +void ULoadMeshWidget::OnGameInstanceMeshFileChanged() +{ + // Pull the new path out of the game instance and refresh the text block + GetMobiusGameInstanceData(); + UpdateFileTextBlockTexts(); } void ULoadMeshWidget::OnSelectFileButtonClicked() @@ -132,7 +154,7 @@ void ULoadMeshWidget::DialogClosed(const FString& AgentFilePath, const FString& Feedback->ReportError( FText::FromString("Invalid Mesh File"), FText::FromString("Unsupported mesh file type selected."), - FText::FromString("Supported types: .fbx, .obj, .udatasmith, .ifc, .wkt"), + FText::FromString("Supported types: .fbx, .obj, .udatasmith, .ifc, .wkt, .h5"), FText::FromString("Load Mesh")); } UE_LOG(LogTemp, Warning, TEXT("The file dialog was canceled or an error occurred")); diff --git a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Public/UI/LoadSave/LoadMeshWidget.h b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Public/UI/LoadSave/LoadMeshWidget.h index e00fafa83..7c9e35d83 100644 --- a/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Public/UI/LoadSave/LoadMeshWidget.h +++ b/UnrealFolder/ProjectMobius/Source/MobiusWidgets/Public/UI/LoadSave/LoadMeshWidget.h @@ -41,6 +41,8 @@ class MOBIUSWIDGETS_API ULoadMeshWidget : public ULoadDataParentWidget #pragma region PUBLIC_METHODS // Constructor virtual void NativeConstruct() override; + + virtual void NativeDestruct() override; /** * Method to call when the SelectFileButton is clicked @@ -66,6 +68,13 @@ class MOBIUSWIDGETS_API ULoadMeshWidget : public ULoadDataParentWidget /** Handler for file dialog errors. Displays error popup to user. */ UFUNCTION() void OnDialogError(const FString& ErrorTitle, const FString& ErrorMessage); + + /** + * Refresh the displayed path when the geometry file is changed elsewhere, e.g. when an + * HDF5 pedestrian data file supplies its own embedded geometry. + */ + UFUNCTION() + void OnGameInstanceMeshFileChanged(); #pragma endregion PUBLIC_METHODS #pragma endregion METHODS