Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2917c88
fix(Relationships): rebuild component index from vertex ids on load
BotellaA Oct 20, 2026
44942f1
fix(VertexIdentifier): repair unique vertices of loaded models
BotellaA Oct 20, 2026
35f1e56
fix(ComponentsStorage): repair loaded components
BotellaA Oct 20, 2026
2a02b90
fix(ModelIO): remove relations of components missing from loaded models
BotellaA Oct 20, 2026
ddf5172
fix(ModelIO): load components with a missing mesh file or unknown mes…
BotellaA Oct 20, 2026
547b4be
fix(ModelIO): save and load RegularGrid surface and block meshes
BotellaA Oct 20, 2026
b2f35b5
fix(ModelIO): check output streams and use UTF-8 paths for model files
BotellaA Oct 20, 2026
9579a3f
fix(Component): bound legacy component name size when loading
BotellaA Oct 20, 2026
9e565ce
fix(ModelIO): restore the Logger level when a model load or save fails
BotellaA Oct 20, 2026
288aecf
fix(ModelIO): turn std exceptions into OpenGeodeException when loading
BotellaA Oct 20, 2026
7f4e620
fix(ModelIO): check component existence without a qualified fold expr…
BotellaA Oct 20, 2026
e4eb903
fix(IO): use UTF-8 paths in the generic input and output helpers
BotellaA Oct 20, 2026
4fff975
fix(IO): only test non-ASCII archive names, not directories
BotellaA Oct 20, 2026
c749cd2
perf(VertexIdentifier): scan component vertices in parallel before re…
BotellaA Oct 20, 2026
7b8b0bf
Merge remote-tracking branch 'origin/next' into fix/model-io-robustness
BotellaA Oct 20, 2026
9ff215e
Merge remote-tracking branch 'origin/next' into fix/model-io-robustness
BotellaA Oct 21, 2026
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
7 changes: 4 additions & 3 deletions include/geode/basic/detail/geode_input_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ namespace geode::detail
const Timer timer;
auto input = geode_object_input_reader< Factory >( filename );
OpenGeodeBasicException::check_exception(
std::filesystem::exists( input->filename() ), nullptr,
OpenGeodeException::TYPE::data, "[", type, "] File \"", filename,
"\" does not exist" );
std::filesystem::exists(
std::filesystem::u8path( to_string( input->filename() ) ) ),
nullptr, OpenGeodeException::TYPE::data, "[", type, "] File \"",
filename, "\" does not exist" );
auto object = input->read( std::forward< Args >( args )... );
Logger::info(
type, " loaded from ", filename, " in ", timer.duration() );
Expand Down
2 changes: 1 addition & 1 deletion include/geode/basic/detail/geode_output_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ namespace geode::detail
const Timer timer;
auto output = geode_object_output_writer< Factory >( filename );
const auto directories = filepath_without_filename(
std::filesystem::path{ expand_predefined_folders( filename ) } );
std::filesystem::u8path( expand_predefined_folders( filename ) ) );
if( !directories.empty() )
{
std::filesystem::create_directories( directories );
Expand Down
32 changes: 32 additions & 0 deletions include/geode/basic/logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
* Logger::info( "My information is ", 42 );
* Logger::warning( "My warning is ", 42, " or more" );
*/
class opengeode_basic_api Logger

Check warning on line 42 in include/geode/basic/logger.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/basic/logger.hpp:42:31 [cppcoreguidelines-special-member-functions]

class 'Logger' defines a destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator
{
public:
enum struct LEVEL : std::uint8_t
Expand Down Expand Up @@ -142,6 +142,38 @@
private:
IMPLEMENTATION_MEMBER( impl_ );
};

namespace detail
{
/*!
* Set the Logger level for the lifetime of this object and restore
* the previous level on destruction, even if an exception is thrown.
* The Logger level is global: scopes running concurrently in several
* threads may restore each other's level.
*/
class ScopedLoggerLevel
{
public:
explicit ScopedLoggerLevel( Logger::LEVEL level )
: previous_level_{ Logger::level() }
{
Logger::set_level( level );
}

~ScopedLoggerLevel()
{
Logger::set_level( previous_level_ );
}

ScopedLoggerLevel( const ScopedLoggerLevel & ) = delete;
ScopedLoggerLevel &operator=( const ScopedLoggerLevel & ) = delete;
ScopedLoggerLevel( ScopedLoggerLevel && ) = delete;
ScopedLoggerLevel &operator=( ScopedLoggerLevel && ) = delete;

private:
Logger::LEVEL previous_level_;
};
} // namespace detail
} // namespace geode

#include <geode/basic/detail/enable_debug_logger.hpp>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
* Create several empty unique vertices
* @return Index of the first created unique vertex
*/
index_t create_unique_vertices( index_t nb );

Check warning on line 79 in include/geode/model/mixin/builder/vertex_identifier_builder.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/builder/vertex_identifier_builder.hpp:79:49 [readability-identifier-length]

parameter name 'nb' is too short, expected at least 3 characters

/*!
* Identify a component vertex to an existing unique vertex index.
Expand Down Expand Up @@ -117,6 +117,13 @@
*/
std::vector< index_t > delete_isolated_vertices();

/*!
* Remove component vertices referencing an unregistered component or
* an invalid vertex, and restore missing links from component vertices
* to their unique vertex. Used after loading a model.
*/
void repair_component_vertices();

private:
VertexIdentifier& vertex_identifier_;
};
Expand Down
126 changes: 117 additions & 9 deletions include/geode/model/mixin/core/detail/components_storage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,58 @@
#include <bitsery/ext/std_smart_ptr.h>

#include <geode/basic/growable.hpp>
#include <geode/basic/logger.hpp>

#include <geode/geometry/bitsery_archive.hpp>

#include <geode/mesh/core/bitsery_archive.hpp>
#include <geode/mesh/core/mesh_factory.hpp>
#include <geode/mesh/core/mesh_id.hpp>

#include <geode/model/mixin/core/bitsery_archive.hpp>
#include <geode/model/mixin/core/component_type.hpp>

namespace geode::detail
{
/*!
* Return the given mesh implementation if it is registered, otherwise
* the default implementation of the given mesh type (with a warning).
*/
[[nodiscard]] inline MeshImpl loadable_mesh_impl( const MeshImpl& impl,
const MeshType& fallback_type,
std::string_view file )
{
if( MeshFactory::has_creator( impl ) )
{
return impl;
}
const auto& fallback = MeshFactory::default_impl( fallback_type );
Logger::warning( "[ComponentsStorage] Unknown mesh implementation ",
impl.get(), " for file ", file, ", loading it as ",
fallback.get() );
return fallback;
}

/*!
* Create an empty mesh for a component whose mesh file is missing, using
* the component mesh implementation if it is registered, otherwise the
* default implementation of the given mesh type.
*/
template < typename Component >
[[nodiscard]] std::unique_ptr< typename Component::Mesh >
empty_mesh_of_missing_file(
const Component& component, const MeshType& fallback_type )
{
Logger::warning( "[ComponentsStorage] Missing mesh file of ",
component.component_id().string(), ", an empty mesh is used" );
if( MeshFactory::has_creator( component.mesh_type() ) )
{
return Component::Mesh::create( component.mesh_type() );
}
return Component::Mesh::create(
MeshFactory::default_impl( fallback_type ) );
}

template < typename Component >
class ComponentsStorage
{
Expand Down Expand Up @@ -98,15 +139,21 @@

void save_components( std::string_view filename ) const
{
std::ofstream file{ to_string( filename ), std::ofstream::binary };
std::ofstream file{ std::filesystem::u8path(
to_string( filename ) ),
std::ofstream::binary };
OpenGeodeModelException::check_exception( file.is_open(), nullptr,
OpenGeodeException::TYPE::internal,
"[ComponentsStorage::save_components] Cannot open file: ",
filename );
TContext context{};
BitseryExtensions::register_serialize_pcontext(
std::get< 0 >( context ) );
Serializer archive{ context, file };
archive.object( *this );
archive.adapter().flush();
OpenGeodeModelException::check_exception(
std::get< 1 >( context ).isValid(), nullptr,
file.good() && std::get< 1 >( context ).isValid(), nullptr,
OpenGeodeException::TYPE::internal,
"[ComponentsStorage::save_components] Error while writing "
"file: ",
Expand All @@ -118,13 +165,26 @@
components_.erase( components_.find( component_id ) );
}

void load_components( std::string_view filename )
/*!
* Load the components stored in the given file.
* @param[in] required If false, a missing file is expected (e.g. for
* files written before this component type existed) and silently
* ignored. Otherwise a warning is emitted.
*/
void load_components( std::string_view filename, bool required = true )
{
if( !std::filesystem::exists( to_string( filename ) ) )
const auto path = std::filesystem::u8path( to_string( filename ) );
if( !std::filesystem::exists( path ) )
{
if( required )
{
Logger::warning( "[ComponentsStorage::load_components] "
"Missing file: ",
filename, ", no component loaded from it" );
}
return;
}
std::ifstream file{ to_string( filename ), std::ifstream::binary };
std::ifstream file{ path, std::ifstream::binary };
TContext context{};
BitseryExtensions::register_deserialize_pcontext(
std::get< 0 >( context ) );
Expand All @@ -139,21 +199,23 @@
"[ComponentsStorage::load_components] Error while reading "
"file: ",
filename );
repair_loaded_components( filename );
}

[[nodiscard]] absl::flat_hash_map< std::string, std::string >
file_mapping( std::string_view directory ) const
{
absl::flat_hash_map< std::string, std::string > mapping;
for( const auto& file :
std::filesystem::directory_iterator( to_string( directory ) ) )
for( const auto& file : std::filesystem::directory_iterator(
std::filesystem::u8path( to_string( directory ) ) ) )
{
auto path = file.path();
auto filename = path.replace_extension( "" ).string();
auto filename = path.replace_extension( "" ).u8string();
if( filename.size() > 36 )

Check warning on line 214 in include/geode/model/mixin/core/detail/components_storage.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/components_storage.hpp:214:39 [cppcoreguidelines-avoid-magic-numbers]

36 is a magic number; consider replacing it with a named constant
{
auto uuid = filename.substr( filename.size() - 36 );

Check warning on line 216 in include/geode/model/mixin/core/detail/components_storage.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/components_storage.hpp:216:68 [cppcoreguidelines-avoid-magic-numbers]

36 is a magic number; consider replacing it with a named constant
mapping.emplace( std::move( uuid ), file.path().string() );
mapping.emplace(
std::move( uuid ), file.path().u8string() );
}
}
return mapping;
Expand All @@ -179,10 +241,56 @@
} } } );
}

void repair_loaded_components( std::string_view filename )

Check warning on line 244 in include/geode/model/mixin/core/detail/components_storage.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/components_storage.hpp:244:14 [readability-function-cognitive-complexity]

function 'repair_loaded_components' has cognitive complexity of 12 (threshold 10)
{
bool has_wrong_component{ false };
for( const auto& [component_id, component] : components_ )
{
if( !component || component->id() != component_id )
{
has_wrong_component = true;
break;
}
}
if( !has_wrong_component )
{
return;
}
ComponentsStore repaired;
repaired.reserve( components_.size() );
for( auto& [component_id, component] : components_ )
{
if( !component )
{
Logger::warning( "[ComponentsStorage::load_components] "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if it should be a simple warning, or an exception raising?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Kept as a warning on purpose: this runs on load, and our policy for corrupted files is to repair what can be repaired and warn, rather than reject the whole model. Here the invalid component is dropped, so the loaded model stays consistent.

"Removing null component ",
component_id.string(), " from file: ", filename );
continue;
}
auto id = component->id();

Check warning on line 270 in include/geode/model/mixin/core/detail/components_storage.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/components_storage.hpp:270:22 [readability-identifier-length]

variable name 'id' is too short, expected at least 3 characters
if( id != component_id )
{
Logger::warning( "[ComponentsStorage::load_components] "
"Component stored with id ",
component_id.string(), " has id ", id.string(),
" in file: ", filename );
}
const auto [_, inserted] =
repaired.emplace( id, std::move( component ) );
if( !inserted )
{
Logger::warning( "[ComponentsStorage::load_components] "
"Removing duplicated component ",
id.string(), " from file: ", filename );
}
}
components_ = std::move( repaired );
}

[[nodiscard]] Component& find_component(
const uuid& component_id ) const
{
const auto it = components_.find( component_id );

Check warning on line 293 in include/geode/model/mixin/core/detail/components_storage.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/components_storage.hpp:293:24 [readability-identifier-length]

variable name 'it' is too short, expected at least 3 characters
OpenGeodeModelException::check_exception( it != components_.end(),
component_id, OpenGeodeException::TYPE::data,
"[ComponentsStorage::component] Component with id ",
Expand Down
12 changes: 9 additions & 3 deletions include/geode/model/mixin/core/detail/relationships_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
#include <geode/model/mixin/core/detail/uuid_to_index.hpp>
#include <geode/model/representation/core/mapping.hpp>

namespace geode

Check warning on line 42 in include/geode/model/mixin/core/detail/relationships_impl.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/relationships_impl.hpp:42:1 [modernize-concat-nested-namespaces]

nested namespaces can be concatenated
{
namespace detail
{
Expand All @@ -50,7 +50,7 @@
class opengeode_model_api RelationshipsImpl
{
public:
RelationshipsImpl( BITSERY );

Check warning on line 53 in include/geode/model/mixin/core/detail/relationships_impl.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/relationships_impl.hpp:53:13 [google-explicit-constructor]

single-argument constructors must be marked explicit to avoid unintentional implicit conversions

[[nodiscard]] index_t nb_components_with_relations() const;

Expand Down Expand Up @@ -78,7 +78,7 @@
void remove_components( absl::Span< const uuid > component_ids );

index_t add_relation_edge(
const ComponentID& from, const ComponentID& to );

Check warning on line 81 in include/geode/model/mixin/core/detail/relationships_impl.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/relationships_impl.hpp:81:61 [readability-identifier-length]

parameter name 'to' is too short, expected at least 3 characters

void remove_relation(
const uuid& component_id1, const uuid& component_id2 );
Expand Down Expand Up @@ -106,7 +106,13 @@

[[nodiscard]] const Graph& graph() const;

void delete_isolated_vertices();
/*!
* Rebuild the uuid to graph vertex map from the ids stored on the
* graph vertices, after loading a file.
* Vertices without a valid or with a duplicated id are removed with
* their relations, as well as isolated vertices.
*/
void repair_after_load();

private:
friend class bitsery::Access;
Expand All @@ -123,15 +129,15 @@
archive.object( impl.uuid2index_ );
archive.ext(
impl.ids_, bitsery::ext::StdSmartPtr{} );
impl.delete_isolated_vertices();
impl.repair_after_load();
},
[]( Archive& archive, RelationshipsImpl& impl ) {
archive.ext(
impl.graph_, bitsery::ext::StdSmartPtr{} );
archive.object( impl.uuid2index_ );
archive.ext(
impl.ids_, bitsery::ext::StdSmartPtr{} );
impl.delete_isolated_vertices();
impl.repair_after_load();
} } } );
}

Expand Down
10 changes: 10 additions & 0 deletions include/geode/model/mixin/core/detail/uuid_to_index.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,22 @@
uuid2index_[id] = index;
}

void clear()
{
uuid2index_.clear();
}

void reserve( index_t capacity )
{
uuid2index_.reserve( capacity );
}

void update( absl::Span< const index_t > old2new )
{
for( auto& it : uuid2index_ )
{
const auto new_index = old2new[it.second];
OpenGeodeModelException::check_assertion(

Check failure on line 92 in include/geode/model/mixin/core/detail/uuid_to_index.hpp

View workflow job for this annotation

GitHub Actions / test / tidy

include/geode/model/mixin/core/detail/uuid_to_index.hpp:92:21 [clang-diagnostic-error]

use of undeclared identifier 'OpenGeodeModelException'
new_index != NO_ID,
"[UuidToIndex::update] no uuid should be removed" );
it.second = new_index;
Expand Down
7 changes: 7 additions & 0 deletions include/geode/model/mixin/core/vertex_identifier.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ namespace geode
*/
std::vector< index_t > delete_isolated_vertices( BuilderKey /*key*/ );

/*!
* Remove component vertices referencing an unregistered component or
* an invalid vertex, and restore missing links from component vertices
* to their unique vertex. Used after loading a model.
*/
void repair_component_vertices( BuilderKey /*key*/ );

protected:
VertexIdentifier( VertexIdentifier&& other ) noexcept;
VertexIdentifier& operator=( VertexIdentifier&& other ) noexcept;
Expand Down
33 changes: 33 additions & 0 deletions include/geode/model/representation/builder/detail/filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@

#pragma once

#include <geode/basic/logger.hpp>
#include <geode/basic/range.hpp>
#include <geode/basic/uuid.hpp>

#include <geode/model/common.hpp>
#include <geode/model/mixin/core/component_type.hpp>
Expand Down Expand Up @@ -64,6 +66,27 @@ namespace geode
}
};

template < typename Mixin, typename Model >
[[nodiscard]] bool has_mixin_component(
const Model& model, const uuid& component_id )
{
return model.Mixin::has_component( component_id );
}

template < typename >
struct ComponentsExistenceChecker;

template < typename... Components >
struct ComponentsExistenceChecker< std::tuple< Components... > >
{
template < typename Model >
bool apply( const Model& model, const uuid& component_id ) const
{
return (
has_mixin_component< Components >( model, component_id )
|| ... );
}
};
} // namespace internal

namespace detail
Expand All @@ -74,6 +97,9 @@ namespace geode
typename Model::Builder builder{ model };
const internal::ComponentsTypesChecker< typename Model::Components >
checker;
const internal::ComponentsExistenceChecker<
typename Model::Components >
existence_checker;
std::vector< uuid > components_to_remove;
for( const auto vertex :
Range{ model.nb_components_with_relations() } )
Expand All @@ -84,6 +110,13 @@ namespace geode
{
components_to_remove.push_back( component_id.id );
}
else if( !existence_checker.apply( model, component_id.id ) )
{
Logger::warning( "[filter_unsupported_components] "
"Removing relations of missing ",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same rationale: the relations pointing to missing components are removed during load, so the model is repaired and usable; a warning reports it instead of failing the whole load.

component_id.string() );
components_to_remove.push_back( component_id.id );
}
}
for( const auto& component : components_to_remove )
{
Expand Down
Loading
Loading