From e92c59bc9bcd69576b44425042c0168e6d3c3cfa Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Thu, 31 Aug 2023 16:28:03 +0200 Subject: [PATCH] Geometry Nodes: refactor simulation storage and how simulation nodes access it Goals of the refactor: * Internal support for baking individual simulation zones (not exposed in the UI yet). * More well-defined access to simulation data in geometry nodes. Especially, it should be more obvious where data is modified. A similar approach should also work for the Bake node. Previously, there were a bunch of simulation specific properties in `GeoNodesModifierData` and then the simulation input and output nodes would have to figure out what to do with that data. Now, there is a new `GeoNodesSimulationParams` which controls the behavior of simulation zones. Contrary to before, different simulation zones can now be handled independently, even if that is not really used yet. `GeoNodesSimulationParams` has to be subclassed by a user of the geometry nodes API. The subclass controls what each simulation input and output node does. This some of the logic that was part of the node before, into the modifier. The way we store simulation data is "transposed". Previously, we stored zone data per frame, but now we store frame data per zone. This allows different zones to be more independent. Consequently, the way the simulation cache is accessed changed. I kept things simpler for now, avoiding many of the methods we had before, and directly accessing the data more often which is often simple enough. This change also makes it theoretically possible to store baked data for separate zones independently. A downside of this is, that existing baked data can't be read anymore. We don't really have compatibility guarantees for this format yet, so it's ok. Users will have to bake again. The bake folder for the modifier now contains an extra subfolder for every zone. Drawing the cached/baked frames in the timeline is less straight forward now. Currently, it just draws the state of one of the zones, which usually is identical to that of all other zones. This will change in the future though, and then the timeline drawing also needs some new UI work. Pull Request: https://projects.blender.org/blender/blender/pulls/111623 --- .../blenkernel/BKE_bake_items_paths.hh | 43 ++ .../blenkernel/BKE_simulation_state.hh | 133 ++--- .../BKE_simulation_state_serialize.hh | 18 +- source/blender/blenkernel/CMakeLists.txt | 2 + .../blenkernel/intern/bake_items_paths.cc | 76 +++ .../blenkernel/intern/simulation_state.cc | 257 ++------- .../intern/simulation_state_serialize.cc | 107 ---- source/blender/blenlib/BLI_sub_frame.hh | 5 - .../editors/object/object_bake_simulation.cc | 182 ++++--- .../editors/space_action/action_draw.cc | 32 +- source/blender/modifiers/intern/MOD_nodes.cc | 499 +++++++++++++----- .../nodes/NOD_geometry_nodes_lazy_function.hh | 113 +++- .../nodes/geometry/node_geometry_util.hh | 10 +- .../nodes/node_geo_simulation_input.cc | 78 +-- .../nodes/node_geo_simulation_output.cc | 204 ++++--- .../intern/geometry_nodes_lazy_function.cc | 76 ++- 16 files changed, 1013 insertions(+), 822 deletions(-) create mode 100644 source/blender/blenkernel/BKE_bake_items_paths.hh create mode 100644 source/blender/blenkernel/intern/bake_items_paths.cc diff --git a/source/blender/blenkernel/BKE_bake_items_paths.hh b/source/blender/blenkernel/BKE_bake_items_paths.hh new file mode 100644 index 00000000000..c1b1d664aa1 --- /dev/null +++ b/source/blender/blenkernel/BKE_bake_items_paths.hh @@ -0,0 +1,43 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include +#include + +#include "BLI_string_ref.hh" +#include "BLI_sub_frame.hh" +#include "BLI_vector.hh" + +namespace blender::bke::bake_paths { + +struct MetaFile { + SubFrame frame; + std::string path; +}; + +struct BakePath { + /** Path to the directory containing the meta data per frame. */ + std::string meta_dir; + /** + * Path to the directory that contains the binary data. Could be shared between multiple bakes + * to reduce memory consumption. + */ + std::string bdata_dir; + /** + * Folder that is allowed to be deleted when the bake is deleted and it doesn't contain anything + * else. Typically, this contains the meta and bdata directories. + */ + std::optional bake_dir; + + static BakePath from_single_root(StringRefNull root_dir); +}; + +std::string frame_to_file_name(const SubFrame &frame); +std::optional file_name_to_frame(const StringRefNull file_name); + +Vector find_sorted_meta_files(const StringRefNull meta_dir); + +} // namespace blender::bke::bake_paths diff --git a/source/blender/blenkernel/BKE_simulation_state.hh b/source/blender/blenkernel/BKE_simulation_state.hh index 7867f6e9e0d..a87b4051aaa 100644 --- a/source/blender/blenkernel/BKE_simulation_state.hh +++ b/source/blender/blenkernel/BKE_simulation_state.hh @@ -4,6 +4,7 @@ #pragma once +#include "BKE_bake_items_paths.hh" #include "BKE_bake_items_serialize.hh" #include "BKE_geometry_set.hh" @@ -11,65 +12,11 @@ #include "BLI_sub_frame.hh" struct bNodeTree; +struct NodesModifierData; +struct Main; namespace blender::bke::sim { -class ModifierSimulationCache; - -/** - * Storage of values for a single simulation input and output node pair. - * Used as a cache to allow random access in time, and as an intermediate form before data is - * baked. - */ -class SimulationZoneState { - public: - Map> item_by_identifier; -}; - -/** Identifies a simulation zone (input and output node pair) used by a modifier. */ -struct SimulationZoneID { - /** ID of the #bNestedNodeRef that references the output node of the zone. */ - int32_t nested_node_id; - - uint64_t hash() const - { - return this->nested_node_id; - } - - friend bool operator==(const SimulationZoneID &a, const SimulationZoneID &b) - { - return a.nested_node_id == b.nested_node_id; - } -}; - -/** - * Stores a single frame of simulation states for all simulation zones in a modifier's node - * hierarchy. - */ -class ModifierSimulationState { - private: - mutable bool bake_loaded_; - - public: - ModifierSimulationCache *owner_; - mutable std::mutex mutex_; - Map> zone_states_; - /** File path to folder containing baked meta-data. */ - std::optional meta_path_; - /** File path to folder containing baked data. */ - std::optional bdata_dir_; - - SimulationZoneState *get_zone_state(const SimulationZoneID &zone_id); - const SimulationZoneState *get_zone_state(const SimulationZoneID &zone_id) const; - SimulationZoneState &get_zone_state_for_write(const SimulationZoneID &zone_id); - void ensure_bake_loaded(const bNodeTree &ntree) const; -}; - -struct ModifierSimulationStateAtFrame { - SubFrame frame; - ModifierSimulationState state; -}; - enum class CacheState { /** The cache is up-to-date with the inputs. */ Valid, @@ -82,56 +29,34 @@ enum class CacheState { Baked, }; -struct StatesAroundFrame { - const ModifierSimulationStateAtFrame *prev = nullptr; - const ModifierSimulationStateAtFrame *current = nullptr; - const ModifierSimulationStateAtFrame *next = nullptr; +struct SimulationZoneFrameCache { + SubFrame frame; + Map> items; + /** Used when the baked data is loaded lazily. */ + std::optional meta_path; }; -struct ModifierSimulationCacheRealtime { - std::unique_ptr prev_state; - std::unique_ptr current_state; - SubFrame prev_frame; - SubFrame current_frame; +struct SimulationZonePrevState { + Map> items; + SubFrame frame; +}; + +struct SimulationZoneCache { + Vector> frame_caches; + std::optional prev_state; + + std::optional bdata_dir; + std::unique_ptr bdata_sharing; + bool failed_finding_bake = false; + CacheState cache_state = CacheState::Valid; + + void reset(); }; class ModifierSimulationCache { - private: - mutable std::mutex states_at_frames_mutex_; - /** - * All simulation states, sorted by frame. - */ - Vector> states_at_frames_; - /** - * Used for baking to deduplicate arrays when writing and writing from storage. Sharing info - * must be kept alive for multiple frames to detect if each data array's version has changed. - */ - std::unique_ptr bdata_sharing_; - - friend ModifierSimulationState; - - bool failed_finding_bake_ = false; - public: - CacheState cache_state = CacheState::Valid; - - /** A non-persistent cache used only to pass simulation state data from one frame to the next. */ - ModifierSimulationCacheRealtime realtime_cache; - - void try_discover_bake(StringRefNull absolute_bake_dir); - - bool has_state_at_frame(const SubFrame &frame) const; - bool has_states() const; - const ModifierSimulationState *get_state_at_exact_frame(const SubFrame &frame) const; - ModifierSimulationState &get_state_at_frame_for_write(const SubFrame &frame); - StatesAroundFrame get_states_around_frame(const SubFrame &frame) const; - - void invalidate() - { - this->cache_state = CacheState::Invalid; - } - - void reset(); + mutable std::mutex mutex; + Map> cache_by_zone_id; }; /** @@ -140,4 +65,12 @@ class ModifierSimulationCache { */ void scene_simulation_states_reset(Scene &scene); +std::optional get_simulation_zone_bake_path(const Main &bmain, + const Object &object, + const NodesModifierData &nmd, + int zone_id); +std::optional get_modifier_simulation_bake_path(const Main &bmain, + const Object &object, + const NodesModifierData &nmd); + } // namespace blender::bke::sim diff --git a/source/blender/blenkernel/BKE_simulation_state_serialize.hh b/source/blender/blenkernel/BKE_simulation_state_serialize.hh index 42b2e26f422..ef95dbee15a 100644 --- a/source/blender/blenkernel/BKE_simulation_state_serialize.hh +++ b/source/blender/blenkernel/BKE_simulation_state_serialize.hh @@ -29,22 +29,6 @@ std::string get_default_modifier_bake_directory(const Main &bmain, const Object &object, const ModifierData &md); -/** - * Encode the simulation state in a #DictionaryValue which also contains references to external - * binary data that has been written using #bdata_writer. - */ -void serialize_modifier_simulation_state(const ModifierSimulationState &state, - BDataWriter &bdata_writer, - BDataSharing &bdata_sharing, - DictionaryValue &r_io_root); -/** - * Fill the simulation state by parsing the provided #DictionaryValue which also contains - * references to external binary data that is read using #bdata_reader. - */ -void deserialize_modifier_simulation_state(const bNodeTree &ntree, - const DictionaryValue &io_root, - const BDataReader &bdata_reader, - const BDataSharing &bdata_sharing, - ModifierSimulationState &r_state); +constexpr int simulation_file_storage_version = 3; } // namespace blender::bke::sim diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 4f59e949a8c..3dfad7ced86 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -73,6 +73,7 @@ set(SRC intern/attribute_math.cc intern/autoexec.cc intern/bake_items.cc + intern/bake_items_paths.cc intern/bake_items_serialize.cc intern/bake_items_socket.cc intern/blender.cc @@ -333,6 +334,7 @@ set(SRC BKE_attribute_math.hh BKE_autoexec.h BKE_bake_items.hh + BKE_bake_items_paths.hh BKE_bake_items_serialize.hh BKE_bake_items_socket.hh BKE_blender.h diff --git a/source/blender/blenkernel/intern/bake_items_paths.cc b/source/blender/blenkernel/intern/bake_items_paths.cc new file mode 100644 index 00000000000..ddd1ddbe164 --- /dev/null +++ b/source/blender/blenkernel/intern/bake_items_paths.cc @@ -0,0 +1,76 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_bake_items_paths.hh" + +#include "BLI_fileops.hh" +#include "BLI_path_util.h" +#include "BLI_string.h" +#include "BLI_string_utils.h" + +namespace blender::bke::bake_paths { + +std::string frame_to_file_name(const SubFrame &frame) +{ + char file_name_c[FILE_MAX]; + SNPRINTF(file_name_c, "%011.5f", double(frame)); + BLI_string_replace_char(file_name_c, '.', '_'); + return file_name_c; +} + +std::optional file_name_to_frame(const StringRefNull file_name) +{ + char modified_file_name[FILE_MAX]; + STRNCPY(modified_file_name, file_name.c_str()); + BLI_string_replace_char(modified_file_name, '_', '.'); + const SubFrame frame = std::stof(modified_file_name); + return frame; +} + +Vector find_sorted_meta_files(const StringRefNull meta_dir) +{ + if (!BLI_is_dir(meta_dir.c_str())) { + return {}; + } + + direntry *dir_entries = nullptr; + const int dir_entries_num = BLI_filelist_dir_contents(meta_dir.c_str(), &dir_entries); + BLI_SCOPED_DEFER([&]() { BLI_filelist_free(dir_entries, dir_entries_num); }); + + Vector meta_files; + for (const int i : IndexRange(dir_entries_num)) { + const direntry &dir_entry = dir_entries[i]; + const StringRefNull dir_entry_path = dir_entry.path; + if (!dir_entry_path.endswith(".json")) { + continue; + } + const std::optional frame = file_name_to_frame(dir_entry.relname); + if (!frame) { + continue; + } + meta_files.append({*frame, dir_entry_path}); + } + + std::sort(meta_files.begin(), meta_files.end(), [](const MetaFile &a, const MetaFile &b) { + return a.frame < b.frame; + }); + + return meta_files; +} + +BakePath BakePath::from_single_root(StringRefNull root_dir) +{ + char meta_dir[FILE_MAX]; + BLI_path_join(meta_dir, sizeof(meta_dir), root_dir.c_str(), "meta"); + char bdata_dir[FILE_MAX]; + BLI_path_join(bdata_dir, sizeof(bdata_dir), root_dir.c_str(), "bdata"); + + BakePath bake_path; + bake_path.meta_dir = meta_dir; + bake_path.bdata_dir = bdata_dir; + bake_path.bake_dir = root_dir; + return bake_path; +} + +} // namespace blender::bke::bake_paths diff --git a/source/blender/blenkernel/intern/simulation_state.cc b/source/blender/blenkernel/intern/simulation_state.cc index 598c84585dd..5ab7396657a 100644 --- a/source/blender/blenkernel/intern/simulation_state.cc +++ b/source/blender/blenkernel/intern/simulation_state.cc @@ -4,6 +4,7 @@ #include "BKE_collection.h" #include "BKE_curves.hh" +#include "BKE_main.h" #include "BKE_simulation_state.hh" #include "BKE_simulation_state_serialize.hh" @@ -21,213 +22,13 @@ namespace blender::bke::sim { -void ModifierSimulationCache::try_discover_bake(const StringRefNull absolute_bake_dir) +void SimulationZoneCache::reset() { - if (failed_finding_bake_) { - return; - } - - char meta_dir[FILE_MAX]; - BLI_path_join(meta_dir, sizeof(meta_dir), absolute_bake_dir.c_str(), "meta"); - char bdata_dir[FILE_MAX]; - BLI_path_join(bdata_dir, sizeof(bdata_dir), absolute_bake_dir.c_str(), "bdata"); - - if (!BLI_is_dir(meta_dir) || !BLI_is_dir(bdata_dir)) { - failed_finding_bake_ = true; - return; - } - - direntry *dir_entries = nullptr; - const int dir_entries_num = BLI_filelist_dir_contents(meta_dir, &dir_entries); - BLI_SCOPED_DEFER([&]() { BLI_filelist_free(dir_entries, dir_entries_num); }); - - if (dir_entries_num == 0) { - failed_finding_bake_ = true; - return; - } - - this->reset(); - - { - std::lock_guard lock(states_at_frames_mutex_); - - for (const int i : IndexRange(dir_entries_num)) { - const direntry &dir_entry = dir_entries[i]; - const StringRefNull dir_entry_path = dir_entry.path; - if (!dir_entry_path.endswith(".json")) { - continue; - } - char modified_file_name[FILE_MAX]; - STRNCPY(modified_file_name, dir_entry.relname); - BLI_string_replace_char(modified_file_name, '_', '.'); - - const SubFrame frame = std::stof(modified_file_name); - - auto new_state_at_frame = std::make_unique(); - new_state_at_frame->frame = frame; - new_state_at_frame->state.bdata_dir_ = bdata_dir; - new_state_at_frame->state.meta_path_ = dir_entry.path; - new_state_at_frame->state.owner_ = this; - states_at_frames_.append(std::move(new_state_at_frame)); - } - - bdata_sharing_ = std::make_unique(); - this->cache_state = CacheState::Baked; - } -} - -static int64_t find_state_at_frame( - const Span> states, const SubFrame &frame) -{ - const int64_t i = binary_search::find_predicate_begin( - states, [&](const auto &item) { return item->frame >= frame; }); - if (i == states.size()) { - return -1; - } - return i; -} - -static int64_t find_state_at_frame_exact( - const Span> states, const SubFrame &frame) -{ - const int64_t i = find_state_at_frame(states, frame); - if (i == -1) { - return -1; - } - if (states[i]->frame != frame) { - return -1; - } - return i; -} - -bool ModifierSimulationCache::has_state_at_frame(const SubFrame &frame) const -{ - std::lock_guard lock(states_at_frames_mutex_); - return find_state_at_frame_exact(states_at_frames_, frame) != -1; -} - -bool ModifierSimulationCache::has_states() const -{ - std::lock_guard lock(states_at_frames_mutex_); - return !states_at_frames_.is_empty(); -} - -const ModifierSimulationState *ModifierSimulationCache::get_state_at_exact_frame( - const SubFrame &frame) const -{ - std::lock_guard lock(states_at_frames_mutex_); - const int64_t i = find_state_at_frame_exact(states_at_frames_, frame); - if (i == -1) { - return nullptr; - } - return &states_at_frames_[i]->state; -} - -ModifierSimulationState &ModifierSimulationCache::get_state_at_frame_for_write( - const SubFrame &frame) -{ - std::lock_guard lock(states_at_frames_mutex_); - const int64_t i = find_state_at_frame_exact(states_at_frames_, frame); - if (i != -1) { - return states_at_frames_[i]->state; - } - - if (!states_at_frames_.is_empty()) { - BLI_assert(frame > states_at_frames_.last()->frame); - } - - states_at_frames_.append(std::make_unique()); - states_at_frames_.last()->frame = frame; - states_at_frames_.last()->state.owner_ = this; - return states_at_frames_.last()->state; -} - -StatesAroundFrame ModifierSimulationCache::get_states_around_frame(const SubFrame &frame) const -{ - std::lock_guard lock(states_at_frames_mutex_); - const int64_t i = find_state_at_frame(states_at_frames_, frame); - StatesAroundFrame states_around_frame{}; - if (i == -1) { - if (!states_at_frames_.is_empty() && states_at_frames_.last()->frame < frame) { - states_around_frame.prev = states_at_frames_.last().get(); - } - return states_around_frame; - } - if (states_at_frames_[i]->frame == frame) { - states_around_frame.current = states_at_frames_[i].get(); - } - if (i > 0) { - states_around_frame.prev = states_at_frames_[i - 1].get(); - } - if (i < states_at_frames_.size() - 2) { - states_around_frame.next = states_at_frames_[i + 1].get(); - } - return states_around_frame; -} - -SimulationZoneState *ModifierSimulationState::get_zone_state(const SimulationZoneID &zone_id) -{ - std::lock_guard lock{mutex_}; - if (auto *ptr = zone_states_.lookup_ptr(zone_id)) { - return ptr->get(); - } - return nullptr; -} - -const SimulationZoneState *ModifierSimulationState::get_zone_state( - const SimulationZoneID &zone_id) const -{ - std::lock_guard lock{mutex_}; - if (auto *ptr = zone_states_.lookup_ptr(zone_id)) { - return ptr->get(); - } - return nullptr; -} - -SimulationZoneState &ModifierSimulationState::get_zone_state_for_write( - const SimulationZoneID &zone_id) -{ - std::lock_guard lock{mutex_}; - return *zone_states_.lookup_or_add_cb(zone_id, - []() { return std::make_unique(); }); -} - -void ModifierSimulationState::ensure_bake_loaded(const bNodeTree &ntree) const -{ - std::scoped_lock lock{mutex_}; - if (bake_loaded_) { - return; - } - if (!meta_path_ || !bdata_dir_) { - return; - } - - const std::shared_ptr io_root_value = io::serialize::read_json_file( - *meta_path_); - if (!io_root_value) { - return; - } - const DictionaryValue *io_root = io_root_value->as_dictionary_value(); - if (!io_root) { - return; - } - - const DiskBDataReader bdata_reader{*bdata_dir_}; - deserialize_modifier_simulation_state(ntree, - *io_root, - bdata_reader, - *owner_->bdata_sharing_, - const_cast(*this)); - bake_loaded_ = true; -} - -void ModifierSimulationCache::reset() -{ - std::lock_guard lock(states_at_frames_mutex_); - states_at_frames_.clear(); - bdata_sharing_.reset(); - this->realtime_cache.current_state.reset(); - this->realtime_cache.prev_state.reset(); + this->frame_caches.clear(); + this->prev_state.reset(); + this->bdata_dir.reset(); + this->bdata_sharing.reset(); + this->failed_finding_bake = false; this->cache_state = CacheState::Valid; } @@ -239,10 +40,52 @@ void scene_simulation_states_reset(Scene &scene) continue; } NodesModifierData *nmd = reinterpret_cast(md); - nmd->runtime->simulation_cache->reset(); + if (!nmd->runtime->simulation_cache) { + continue; + } + for (auto item : nmd->runtime->simulation_cache->cache_by_zone_id.items()) { + item.value->reset(); + } } } FOREACH_SCENE_OBJECT_END; } +std::optional get_modifier_simulation_bake_path(const Main &bmain, + const Object &object, + const NodesModifierData &nmd) +{ + const StringRefNull bmain_path = BKE_main_blendfile_path(&bmain); + if (bmain_path.is_empty()) { + return std::nullopt; + } + if (StringRef(nmd.simulation_bake_directory).is_empty()) { + return std::nullopt; + } + const char *base_path = ID_BLEND_PATH(&bmain, &object.id); + char absolute_bake_dir[FILE_MAX]; + STRNCPY(absolute_bake_dir, nmd.simulation_bake_directory); + BLI_path_abs(absolute_bake_dir, base_path); + return absolute_bake_dir; +} + +std::optional get_simulation_zone_bake_path(const Main &bmain, + const Object &object, + const NodesModifierData &nmd, + int zone_id) +{ + const std::optional modifier_bake_path = get_modifier_simulation_bake_path( + bmain, object, nmd); + if (!modifier_bake_path) { + return std::nullopt; + } + + char zone_bake_dir[FILE_MAX]; + BLI_path_join(zone_bake_dir, + sizeof(zone_bake_dir), + modifier_bake_path->c_str(), + std::to_string(zone_id).c_str()); + return bake_paths::BakePath::from_single_root(zone_bake_dir); +} + } // namespace blender::bke::sim diff --git a/source/blender/blenkernel/intern/simulation_state_serialize.cc b/source/blender/blenkernel/intern/simulation_state_serialize.cc index 12b7143165f..4e58a8f8451 100644 --- a/source/blender/blenkernel/intern/simulation_state_serialize.cc +++ b/source/blender/blenkernel/intern/simulation_state_serialize.cc @@ -83,111 +83,4 @@ std::string get_default_modifier_bake_directory(const Main &bmain, return dir; } -/** - * Version written to the baked data. - */ -static constexpr int serialize_format_version = 2; - -void serialize_modifier_simulation_state(const ModifierSimulationState &state, - BDataWriter &bdata_writer, - BDataSharing &bdata_sharing, - DictionaryValue &r_io_root) -{ - r_io_root.append_int("version", serialize_format_version); - auto io_zones = r_io_root.append_array("zones"); - - for (const auto item : state.zone_states_.items()) { - const SimulationZoneID &zone_id = item.key; - const SimulationZoneState &zone_state = *item.value; - - auto io_zone = io_zones->append_dict(); - - io_zone->append_int("state_id", zone_id.nested_node_id); - - auto io_state_items = io_zone->append_array("state_items"); - for (const MapItem> &state_item_with_id : - zone_state.item_by_identifier.items()) - { - auto io_state_item = io_state_items->append_dict(); - - io_state_item->append_int("id", state_item_with_id.key); - serialize_bake_item(*state_item_with_id.value, bdata_writer, bdata_sharing, *io_state_item); - } - } -} - -void deserialize_modifier_simulation_state(const bNodeTree &ntree, - const DictionaryValue &io_root, - const BDataReader &bdata_reader, - const BDataSharing &bdata_sharing, - ModifierSimulationState &r_state) -{ - io::serialize::JsonFormatter formatter; - const std::optional version = io_root.lookup_int("version"); - if (!version) { - return; - } - if (*version > serialize_format_version) { - return; - } - const io::serialize::ArrayValue *io_zones = io_root.lookup_array("zones"); - if (!io_zones) { - return; - } - for (const auto &io_zone_value : io_zones->elements()) { - const DictionaryValue *io_zone = io_zone_value->as_dictionary_value(); - if (!io_zone) { - continue; - } - bke::sim::SimulationZoneID zone_id; - if (const std::optional state_id = io_zone->lookup_int("state_id")) { - zone_id.nested_node_id = *state_id; - } - else if (const io::serialize::ArrayValue *io_zone_id = io_zone->lookup_array("zone_id")) { - /* In the initial release of simulation nodes, the entire node id path was written to the - * baked data. For backward compatibility the node ids are read here and then the nested node - * id is looked up. */ - Vector node_ids; - for (const auto &io_zone_id_element : io_zone_id->elements()) { - const io::serialize::IntValue *io_node_id = io_zone_id_element->as_int_value(); - if (!io_node_id) { - continue; - } - node_ids.append(io_node_id->value()); - } - const bNestedNodeRef *nested_node_ref = ntree.nested_node_ref_from_node_id_path(node_ids); - if (!nested_node_ref) { - continue; - } - zone_id.nested_node_id = nested_node_ref->id; - } - - const io::serialize::ArrayValue *io_state_items = io_zone->lookup_array("state_items"); - if (!io_state_items) { - continue; - } - - auto zone_state = std::make_unique(); - - for (const auto &io_state_item_value : io_state_items->elements()) { - const DictionaryValue *io_state_item = io_state_item_value->as_dictionary_value(); - if (!io_state_item) { - continue; - } - const std::optional state_item_id = io_state_item->lookup_int("id"); - if (!state_item_id) { - continue; - } - - std::unique_ptr new_state_item = deserialize_bake_item( - *io_state_item, bdata_reader, bdata_sharing); - - BLI_assert(new_state_item); - zone_state->item_by_identifier.add(*state_item_id, std::move(new_state_item)); - } - - r_state.zone_states_.add_overwrite(zone_id, std::move(zone_state)); - } -} - } // namespace blender::bke::sim diff --git a/source/blender/blenlib/BLI_sub_frame.hh b/source/blender/blenlib/BLI_sub_frame.hh index 3c2acf91feb..98166cf6172 100644 --- a/source/blender/blenlib/BLI_sub_frame.hh +++ b/source/blender/blenlib/BLI_sub_frame.hh @@ -85,11 +85,6 @@ struct SubFrame { { return a.frame_ >= b.frame_ || (a.frame_ == b.frame_ && a.subframe_ >= b.subframe_); } - - friend std::ostream &operator<<(std::ostream &stream, const SubFrame &a) - { - return stream << float(a); - } }; } // namespace blender diff --git a/source/blender/editors/object/object_bake_simulation.cc b/source/blender/editors/object/object_bake_simulation.cc index 8dd9d990ac2..86c3817f063 100644 --- a/source/blender/editors/object/object_bake_simulation.cc +++ b/source/blender/editors/object/object_bake_simulation.cc @@ -36,6 +36,7 @@ #include "BKE_lib_id.h" #include "BKE_main.h" #include "BKE_mesh.hh" +#include "BKE_node_runtime.hh" #include "BKE_object.h" #include "BKE_pointcloud.h" #include "BKE_report.h" @@ -98,7 +99,14 @@ static void calculate_simulation_job_startjob(void *customdata, LISTBASE_FOREACH (ModifierData *, md, &object->modifiers) { if (md->type == eModifierType_Nodes) { NodesModifierData *nmd = reinterpret_cast(md); - nmd->runtime->simulation_cache->reset(); + if (!nmd->runtime->simulation_cache) { + continue; + } + for (auto item : nmd->runtime->simulation_cache->cache_by_zone_id.items()) { + if (item.value->cache_state != CacheState::Baked) { + item.value->reset(); + } + } } } objects_to_calc.append(object); @@ -214,10 +222,15 @@ static bool bake_simulation_poll(bContext *C) return true; } +struct ZoneBakeData { + int zone_id; + bke::bake_paths::BakePath path; + std::unique_ptr bdata_sharing; +}; + struct ModifierBakeData { NodesModifierData *nmd; - std::string absolute_bake_dir; - std::unique_ptr bdata_sharing; + Vector zones; }; struct ObjectBakeData { @@ -251,19 +264,38 @@ static void bake_simulation_job_startjob(void *customdata, continue; } - const char *base_path = ID_BLEND_PATH(job.bmain, &object->id); - ObjectBakeData bake_data; bake_data.object = object; LISTBASE_FOREACH (ModifierData *, md, &object->modifiers) { if (md->type == eModifierType_Nodes) { NodesModifierData *nmd = reinterpret_cast(md); - nmd->runtime->simulation_cache->reset(); - char absolute_bake_dir[FILE_MAX]; - STRNCPY(absolute_bake_dir, nmd->simulation_bake_directory); - BLI_path_abs(absolute_bake_dir, base_path); - bake_data.modifiers.append( - {nmd, absolute_bake_dir, std::make_unique()}); + if (!nmd->node_group) { + continue; + } + if (!nmd->runtime->simulation_cache) { + continue; + } + ModifierBakeData modifier_bake_data; + modifier_bake_data.nmd = nmd; + + for (auto item : nmd->runtime->simulation_cache->cache_by_zone_id.items()) { + item.value->reset(); + } + + for (const bNestedNodeRef &nested_node_ref : nmd->node_group->nested_node_refs_span()) { + ZoneBakeData zone_bake_data; + zone_bake_data.zone_id = nested_node_ref.id; + zone_bake_data.bdata_sharing = std::make_unique(); + if (std::optional path = + bke::sim::get_simulation_zone_bake_path( + *job.bmain, *object, *nmd, nested_node_ref.id)) + { + zone_bake_data.path = std::move(*path); + modifier_bake_data.zones.append(std::move(zone_bake_data)); + } + } + + bake_data.modifiers.append(std::move(modifier_bake_data)); } } objects_to_bake.append(std::move(bake_data)); @@ -288,51 +320,56 @@ static void bake_simulation_job_startjob(void *customdata, job.scene->r.cfra = frame.frame(); job.scene->r.subframe = frame.subframe(); - char frame_file_c_str[64]; - SNPRINTF(frame_file_c_str, "%011.5f", double(frame)); - BLI_string_replace_char(frame_file_c_str, '.', '_'); - const StringRefNull frame_file_str = frame_file_c_str; - BKE_scene_graph_update_for_newframe(job.depsgraph); + const std::string frame_file_name = bke::bake_paths::frame_to_file_name(frame); + for (ObjectBakeData &object_bake_data : objects_to_bake) { for (ModifierBakeData &modifier_bake_data : object_bake_data.modifiers) { NodesModifierData &nmd = *modifier_bake_data.nmd; - if (!nmd.runtime->simulation_cache) { - continue; + const ModifierSimulationCache &simulation_cache = *nmd.runtime->simulation_cache; + for (ZoneBakeData &zone_bake_data : modifier_bake_data.zones) { + if (!simulation_cache.cache_by_zone_id.contains(zone_bake_data.zone_id)) { + continue; + } + const SimulationZoneCache &zone_cache = *simulation_cache.cache_by_zone_id.lookup( + zone_bake_data.zone_id); + if (zone_cache.frame_caches.is_empty()) { + continue; + } + const SimulationZoneFrameCache &frame_cache = *zone_cache.frame_caches.last(); + if (frame_cache.frame != frame) { + continue; + } + + const bke::bake_paths::BakePath path = zone_bake_data.path; + + const std::string bdata_file_name = frame_file_name + ".bdata"; + + char bdata_path[FILE_MAX]; + BLI_path_join( + bdata_path, sizeof(bdata_path), path.bdata_dir.c_str(), bdata_file_name.c_str()); + char meta_path[FILE_MAX]; + BLI_path_join(meta_path, + sizeof(meta_path), + path.meta_dir.c_str(), + (frame_file_name + ".json").c_str()); + BLI_file_ensure_parent_dir_exists(meta_path); + BLI_file_ensure_parent_dir_exists(bdata_path); + fstream bdata_file{bdata_path, std::ios::out | std::ios::binary}; + bke::DiskBDataWriter bdata_writer{bdata_file_name, bdata_file, 0}; + + io::serialize::DictionaryValue io_root; + io_root.append_int("version", simulation_file_storage_version); + io::serialize::DictionaryValue &io_items = *io_root.append_dict("items"); + for (auto item : frame_cache.items.items()) { + io::serialize::DictionaryValue &io_item = *io_items.append_dict( + std::to_string(item.key)); + bke::serialize_bake_item( + *item.value, bdata_writer, *zone_bake_data.bdata_sharing, io_item); + } + io::serialize::write_json_file(meta_path, io_root); } - ModifierSimulationCache &sim_cache = *nmd.runtime->simulation_cache; - const ModifierSimulationState *sim_state = sim_cache.get_state_at_exact_frame(frame); - if (sim_state == nullptr || sim_state->zone_states_.is_empty()) { - continue; - } - - const std::string bdata_file_name = frame_file_str + ".bdata"; - const std::string meta_file_name = frame_file_str + ".json"; - - char bdata_path[FILE_MAX]; - BLI_path_join(bdata_path, - sizeof(bdata_path), - modifier_bake_data.absolute_bake_dir.c_str(), - "bdata", - bdata_file_name.c_str()); - char meta_path[FILE_MAX]; - BLI_path_join(meta_path, - sizeof(meta_path), - modifier_bake_data.absolute_bake_dir.c_str(), - "meta", - meta_file_name.c_str()); - - BLI_file_ensure_parent_dir_exists(bdata_path); - fstream bdata_file{bdata_path, std::ios::out | std::ios::binary}; - bke::DiskBDataWriter bdata_writer{bdata_file_name, bdata_file, 0}; - - io::serialize::DictionaryValue io_root; - bke::sim::serialize_modifier_simulation_state( - *sim_state, bdata_writer, *modifier_bake_data.bdata_sharing, io_root); - - BLI_file_ensure_parent_dir_exists(meta_path); - io::serialize::write_json_file(meta_path, io_root); } } @@ -343,9 +380,13 @@ static void bake_simulation_job_startjob(void *customdata, for (ObjectBakeData &object_bake_data : objects_to_bake) { for (ModifierBakeData &modifier_bake_data : object_bake_data.modifiers) { NodesModifierData &nmd = *modifier_bake_data.nmd; - if (nmd.runtime->simulation_cache) { - /* Tag the caches as being baked so that they are not changed anymore. */ - nmd.runtime->simulation_cache->cache_state = CacheState::Baked; + for (ZoneBakeData &zone_bake_data : modifier_bake_data.zones) { + if (std::unique_ptr &zone_cache = + nmd.runtime->simulation_cache->cache_by_zone_id.lookup(zone_bake_data.zone_id)) + { + /* Tag the caches as being baked so that they are not changed anymore. */ + zone_cache->cache_state = CacheState::Baked; + } } } DEG_id_tag_update(&object_bake_data.object->id, ID_RECALC_GEOMETRY); @@ -593,40 +634,45 @@ static int delete_baked_simulation_exec(bContext *C, wmOperator *op) } for (Object *object : objects) { - const char *base_path = ID_BLEND_PATH(bmain, &object->id); LISTBASE_FOREACH (ModifierData *, md, &object->modifiers) { if (md->type == eModifierType_Nodes) { NodesModifierData *nmd = reinterpret_cast(md); - nmd->runtime->simulation_cache->reset(); - if (StringRef(nmd->simulation_bake_directory).is_empty()) { + if (!nmd->runtime->simulation_cache) { continue; } - char absolute_bake_dir[FILE_MAX]; - STRNCPY(absolute_bake_dir, nmd->simulation_bake_directory); - BLI_path_abs(absolute_bake_dir, base_path); + for (auto item : nmd->runtime->simulation_cache->cache_by_zone_id.items()) { + item.value->reset(); - char meta_dir[FILE_MAX]; - BLI_path_join(meta_dir, sizeof(meta_dir), absolute_bake_dir, "meta"); - char bdata_dir[FILE_MAX]; - BLI_path_join(bdata_dir, sizeof(bdata_dir), absolute_bake_dir, "bdata"); + const std::optional bake_path = + bke::sim::get_simulation_zone_bake_path(*bmain, *object, *nmd, item.key); + if (!bake_path) { + continue; + } - if (BLI_exists(absolute_bake_dir)) { + const char *meta_dir = bake_path->meta_dir.c_str(); if (BLI_exists(meta_dir)) { if (BLI_delete(meta_dir, true, true)) { BKE_reportf(op->reports, RPT_ERROR, "Failed to remove meta directory %s", meta_dir); } } + const char *bdata_dir = bake_path->bdata_dir.c_str(); if (BLI_exists(bdata_dir)) { if (BLI_delete(bdata_dir, true, true)) { BKE_reportf( op->reports, RPT_ERROR, "Failed to remove bdata directory %s", bdata_dir); } } - /* Delete the folder if it's empty. */ - BLI_delete(absolute_bake_dir, true, false); + if (bake_path->bake_dir.has_value()) { + const char *zone_bake_dir = bake_path->bake_dir->c_str(); + /* Try to delete zone bake directory if it is empty. */ + BLI_delete(zone_bake_dir, true, false); + } } - else { - BKE_reportf(op->reports, RPT_ERROR, "Bake directory %s not found", absolute_bake_dir); + if (const std::optional modifier_bake_dir = + bke::sim::get_modifier_simulation_bake_path(*bmain, *object, *nmd)) + { + /* Try to delete modifier bake directory if it is empty. */ + BLI_delete(modifier_bake_dir->c_str(), true, false); } } } diff --git a/source/blender/editors/space_action/action_draw.cc b/source/blender/editors/space_action/action_draw.cc index 971a364d18c..6c5b99dcb7e 100644 --- a/source/blender/editors/space_action/action_draw.cc +++ b/source/blender/editors/space_action/action_draw.cc @@ -748,19 +748,30 @@ static void timeline_cache_draw_single(PTCacheID *pid, float y_offset, float hei } static void timeline_cache_draw_simulation_nodes( - const Scene &scene, const blender::bke::sim::ModifierSimulationCache &cache, const float y_offset, const float height, const uint pos_id) { + std::lock_guard lock{cache.mutex}; + if (cache.cache_by_zone_id.is_empty()) { + return; + } + /* Draw the state if one of the simulation zones. This is fine for now, because there is no ui + * that allows caching zones independently. */ + const blender::bke::sim::SimulationZoneCache &zone_cache = + **cache.cache_by_zone_id.values().begin(); + if (zone_cache.frame_caches.is_empty()) { + return; + } + GPU_matrix_push(); GPU_matrix_translate_2f(0.0, float(V2D_SCROLL_HANDLE_HEIGHT) + y_offset); GPU_matrix_scale_2f(1.0, height); float color[4]; UI_GetThemeColor4fv(TH_SIMULATED_FRAMES, color); - switch (cache.cache_state) { + switch (zone_cache.cache_state) { case blender::bke::sim::CacheState::Invalid: { color[3] = 0.4f; break; @@ -777,16 +788,13 @@ static void timeline_cache_draw_simulation_nodes( immUniformColor4fv(color); - const int start_frame = scene.r.sfra; - const int end_frame = scene.r.efra; - const int frames_num = end_frame - start_frame + 1; - const blender::IndexRange frames_range(start_frame, frames_num); + immBeginAtMost(GPU_PRIM_TRIS, zone_cache.frame_caches.size() * 6); - immBeginAtMost(GPU_PRIM_TRIS, frames_num * 6); - for (const int frame : frames_range) { - if (cache.has_state_at_frame(frame)) { - immRectf_fast(pos_id, frame - 0.5f, 0, frame + 0.5f, 1.0f); - } + for (const std::unique_ptr &frame_cache : + zone_cache.frame_caches.as_span()) + { + const int frame = frame_cache->frame.frame(); + immRectf_fast(pos_id, frame - 0.5f, 0, frame + 0.5f, 1.0f); } immEnd(); @@ -840,7 +848,7 @@ void timeline_draw_cache(const SpaceAction *saction, const Object *ob, const Sce continue; } timeline_cache_draw_simulation_nodes( - *scene, *nmd->runtime->simulation_cache, y_offset, cache_draw_height, pos_id); + *nmd->runtime->simulation_cache, y_offset, cache_draw_height, pos_id); y_offset += cache_draw_height; } } diff --git a/source/blender/modifiers/intern/MOD_nodes.cc b/source/blender/modifiers/intern/MOD_nodes.cc index 3c7a5f83a8a..a03b73c2714 100644 --- a/source/blender/modifiers/intern/MOD_nodes.cc +++ b/source/blender/modifiers/intern/MOD_nodes.cc @@ -660,154 +660,400 @@ static void check_property_socket_sync(const Object *ob, ModifierData *md) } } -static void prepare_simulation_states_for_evaluation(const NodesModifierData &nmd, - const ModifierEvalContext &ctx, - nodes::GeoNodesModifierData &exec_data) -{ - if (!nmd.runtime->simulation_cache) { - return; - } - const Main *bmain = DEG_get_bmain(ctx.depsgraph); - const SubFrame current_frame = DEG_get_ctime(ctx.depsgraph); - const Scene *scene = DEG_get_input_scene(ctx.depsgraph); - const SubFrame start_frame = scene->r.sfra; - const bool is_start_frame = current_frame == start_frame; +namespace sim_input = nodes::sim_input; +namespace sim_output = nodes::sim_output; - /* This cache may be shared between original and evaluated modifiers. */ - blender::bke::sim::ModifierSimulationCache &simulation_cache = *nmd.runtime->simulation_cache; +class NodesModifierSimulationParams : public nodes::GeoNodesSimulationParams { + private: + static constexpr float max_delta_frames = 1.0f; + mutable Map> behavior_by_zone_id_; + const NodesModifierData &nmd_; + const ModifierEvalContext &ctx_; + const Main *bmain_; + SubFrame current_frame_; + SubFrame start_frame_; + bool is_start_frame_; + bool use_frame_cache_; + bool depsgraph_is_active_; + bke::sim::ModifierSimulationCache *simulation_cache_; + float fps_; + + public: + NodesModifierSimulationParams(NodesModifierData &nmd, const ModifierEvalContext &ctx) + : nmd_(nmd), ctx_(ctx) { - /* Try to use baked data. */ - const StringRefNull bmain_path = BKE_main_blendfile_path(bmain); - if (simulation_cache.cache_state != bke::sim::CacheState::Baked && !bmain_path.is_empty()) { - if (!StringRef(nmd.simulation_bake_directory).is_empty()) { - if (const char *base_path = ID_BLEND_PATH(bmain, &ctx.object->id)) { - char absolute_bake_dir[FILE_MAX]; - STRNCPY(absolute_bake_dir, nmd.simulation_bake_directory); - BLI_path_abs(absolute_bake_dir, base_path); - simulation_cache.try_discover_bake(absolute_bake_dir); - } - } + const Depsgraph *depsgraph = ctx_.depsgraph; + bmain_ = DEG_get_bmain(depsgraph); + current_frame_ = DEG_get_ctime(depsgraph); + const Scene *scene = DEG_get_input_scene(depsgraph); + start_frame_ = scene->r.sfra; + is_start_frame_ = current_frame_ == start_frame_; + use_frame_cache_ = ctx_.object->flag & OB_FLAG_USE_SIMULATION_CACHE; + depsgraph_is_active_ = DEG_is_active(depsgraph); + simulation_cache_ = nmd.runtime->simulation_cache.get(); + fps_ = FPS; + + if (!simulation_cache_) { + return; } - } - - if (ctx.object->flag & OB_FLAG_USE_SIMULATION_CACHE) { - if (DEG_is_active(ctx.depsgraph)) { - - { - /* Invalidate cached data on user edits. */ - if (nmd.modifier.flag & eModifierFlag_UserModified) { - if (simulation_cache.cache_state != bke::sim::CacheState::Baked) { - simulation_cache.invalidate(); - } - } - } - - { - /* Reset cached data if necessary. */ - const bke::sim::StatesAroundFrame sim_states = simulation_cache.get_states_around_frame( - current_frame); - if (simulation_cache.cache_state == bke::sim::CacheState::Invalid && - (current_frame == start_frame || - (sim_states.current == nullptr && sim_states.prev == nullptr && - sim_states.next != nullptr))) + std::lock_guard lock{simulation_cache_->mutex}; + if (depsgraph_is_active_) { + /* Invalidate data on user edits. */ + if (nmd.modifier.flag & eModifierFlag_UserModified) { + for (std::unique_ptr &zone_cache : + simulation_cache_->cache_by_zone_id.values()) { - simulation_cache.reset(); - } - } - /* Decide if a new simulation state should be created in this evaluation. */ - const bke::sim::StatesAroundFrame sim_states = simulation_cache.get_states_around_frame( - current_frame); - if (simulation_cache.cache_state != bke::sim::CacheState::Baked) { - if (sim_states.current == nullptr) { - if (is_start_frame || !simulation_cache.has_states()) { - bke::sim::ModifierSimulationState ¤t_sim_state = - simulation_cache.get_state_at_frame_for_write(current_frame); - exec_data.current_simulation_state_for_write = ¤t_sim_state; - exec_data.simulation_time_delta = 0.0f; - if (!is_start_frame) { - /* When starting a new simulation at another frame than the start frame, - * it can't match what would be baked, so invalidate it immediately. */ - simulation_cache.invalidate(); - } - } - else if (sim_states.prev != nullptr && sim_states.next == nullptr) { - const float max_delta_frames = 1.0f; - const float scene_delta_frames = float(current_frame) - float(sim_states.prev->frame); - const float delta_frames = std::min(max_delta_frames, scene_delta_frames); - if (delta_frames != scene_delta_frames) { - simulation_cache.invalidate(); - } - bke::sim::ModifierSimulationState ¤t_sim_state = - simulation_cache.get_state_at_frame_for_write(current_frame); - exec_data.current_simulation_state_for_write = ¤t_sim_state; - const float delta_seconds = delta_frames / FPS; - exec_data.simulation_time_delta = delta_seconds; + if (zone_cache->cache_state != bke::sim::CacheState::Baked) { + zone_cache->cache_state = bke::sim::CacheState::Invalid; } } } - } - - /* Load read-only states to give nodes access to cached data. */ - const bke::sim::StatesAroundFrame sim_states = simulation_cache.get_states_around_frame( - current_frame); - if (sim_states.current) { - sim_states.current->state.ensure_bake_loaded(*nmd.node_group); - exec_data.current_simulation_state = &sim_states.current->state; - } - if (sim_states.prev) { - sim_states.prev->state.ensure_bake_loaded(*nmd.node_group); - exec_data.prev_simulation_state = &sim_states.prev->state; - if (sim_states.next) { - sim_states.next->state.ensure_bake_loaded(*nmd.node_group); - exec_data.next_simulation_state = &sim_states.next->state; - exec_data.simulation_state_mix_factor = - (float(current_frame) - float(sim_states.prev->frame)) / - (float(sim_states.next->frame) - float(sim_states.prev->frame)); + /* Reset cached data if necessary. */ + if (is_start_frame_) { + for (std::unique_ptr &zone_cache : + simulation_cache_->cache_by_zone_id.values()) + { + if (zone_cache->cache_state == bke::sim::CacheState::Invalid) { + zone_cache->reset(); + } + } } } } - else { - if (DEG_is_active(ctx.depsgraph)) { - bke::sim::ModifierSimulationCacheRealtime &realtime_cache = simulation_cache.realtime_cache; - if (current_frame < realtime_cache.prev_frame) { - /* Reset the cache when going backwards in time. */ - simulation_cache.reset(); + nodes::SimulationZoneBehavior *get(const int zone_id) const override + { + if (!simulation_cache_) { + return nullptr; + } + std::lock_guard lock{simulation_cache_->mutex}; + return behavior_by_zone_id_ + .lookup_or_add_cb(zone_id, + [&]() { + auto info = std::make_unique(); + this->init_simulation_info(zone_id, *info); + return info; + }) + .get(); + } + + struct FrameIndices { + std::optional prev; + std::optional current; + std::optional next; + }; + + void init_simulation_info(const int zone_id, nodes::SimulationZoneBehavior &zone_behavior) const + { + using namespace bke::sim; + + SimulationZoneCache &zone_cache = *simulation_cache_->cache_by_zone_id.lookup_or_add_cb( + zone_id, []() { return std::make_unique(); }); + + /* Try load baked data. */ + if (!zone_cache.failed_finding_bake) { + if (zone_cache.cache_state != CacheState::Baked) { + if (std::optional zone_bake_path = + get_simulation_zone_bake_path(*bmain_, *ctx_.object, nmd_, zone_id)) + { + + Vector meta_files = bke::bake_paths::find_sorted_meta_files( + zone_bake_path->meta_dir); + if (!meta_files.is_empty()) { + zone_cache.reset(); + + for (const bke::bake_paths::MetaFile &meta_file : meta_files) { + auto frame_cache = std::make_unique(); + frame_cache->frame = meta_file.frame; + frame_cache->meta_path = meta_file.path; + zone_cache.frame_caches.append(std::move(frame_cache)); + } + zone_cache.bdata_dir = zone_bake_path->bdata_dir; + zone_cache.bdata_sharing = std::make_unique(); + zone_cache.cache_state = CacheState::Baked; + } + } } - if (realtime_cache.current_frame == current_frame && realtime_cache.current_state) { - /* Don't simulate in the same frame again. */ - exec_data.current_simulation_state = realtime_cache.current_state.get(); + if (zone_cache.cache_state != CacheState::Baked) { + zone_cache.failed_finding_bake = true; + } + } + + const FrameIndices frame_indices = this->get_frame_indices(zone_cache); + if (zone_cache.cache_state == CacheState::Baked) { + this->read_from_cache(frame_indices, zone_cache, zone_behavior); + return; + } + if (use_frame_cache_) { + /* If the depsgraph is active, we allow creating new simulation states. Otherwise, the access + * is read-only. */ + if (depsgraph_is_active_) { + if (zone_cache.frame_caches.is_empty()) { + /* Initialize the simulation. */ + this->input_pass_through(zone_behavior); + this->output_store_frame_cache(zone_cache, zone_behavior); + if (!is_start_frame_) { + /* If we initialize at a frame that is not the start frame, the simulation is not + * valid. */ + zone_cache.cache_state = CacheState::Invalid; + } + return; + } + if (frame_indices.prev && !frame_indices.current && !frame_indices.next) { + /* Read the previous frame's data and store the newly computed simulation state. */ + auto &output_copy_info = zone_behavior.input.emplace(); + const bke::sim::SimulationZoneFrameCache &prev_frame_cache = + *zone_cache.frame_caches[*frame_indices.prev]; + const float delta_frames = std::min( + max_delta_frames, float(current_frame_) - float(prev_frame_cache.frame)); + if (delta_frames != 1) { + zone_cache.cache_state = CacheState::Invalid; + } + output_copy_info.delta_time = delta_frames / fps_; + output_copy_info.items_by_id = this->to_readonly_items_map(prev_frame_cache.items); + this->output_store_frame_cache(zone_cache, zone_behavior); + return; + } + } + this->read_from_cache(frame_indices, zone_cache, zone_behavior); + return; + } + + /* When there is no per-frame cache, check if there is a previous state. */ + if (zone_cache.prev_state) { + if (zone_cache.prev_state->frame < current_frame_) { + /* Do a simulation step. */ + const float delta_frames = std::min( + max_delta_frames, float(zone_cache.prev_state->frame) - float(current_frame_)); + auto &output_move_info = zone_behavior.input.emplace(); + output_move_info.delta_time = delta_frames / fps_; + output_move_info.items_by_id = std::move(zone_cache.prev_state->items); + this->store_as_prev_items(zone_cache, zone_behavior); return; } - - /* Advance in time, making the last "current" state the new "previous" state. */ - realtime_cache.prev_frame = realtime_cache.current_frame; - realtime_cache.prev_state = std::move(realtime_cache.current_state); - if (realtime_cache.prev_state) { - exec_data.prev_simulation_state_mutable = realtime_cache.prev_state.get(); + if (zone_cache.prev_state->frame == current_frame_) { + /* Just read from the previous state if the frame has not changed. */ + auto &output_copy_info = zone_behavior.input.emplace(); + output_copy_info.delta_time = 0.0f; + output_copy_info.items_by_id = this->to_readonly_items_map(zone_cache.prev_state->items); + auto &read_single_info = zone_behavior.output.emplace(); + read_single_info.items_by_id = this->to_readonly_items_map(zone_cache.prev_state->items); + return; } + if (!depsgraph_is_active_) { + /* There is no previous state, and it's not possible to initialize the simulation because + * the depsgraph is not active. */ + zone_behavior.input.emplace(); + zone_behavior.output.emplace(); + return; + } + /* Reset the simulation when the scene time moved backwards. */ + zone_cache.prev_state.reset(); + } + zone_behavior.input.emplace(); + if (depsgraph_is_active_) { + /* Initialize the simulation. */ + this->store_as_prev_items(zone_cache, zone_behavior); + } + else { + zone_behavior.output.emplace(); + } + } - /* Create a new current state used to pass the data to the next frame. */ - realtime_cache.current_state = std::make_unique(); - realtime_cache.current_frame = current_frame; - exec_data.current_simulation_state_for_write = realtime_cache.current_state.get(); - exec_data.current_simulation_state = exec_data.current_simulation_state_for_write; + FrameIndices get_frame_indices(const bke::sim::SimulationZoneCache &zone_cache) const + { + FrameIndices frame_indices; + if (!zone_cache.frame_caches.is_empty()) { + const int first_future_frame_index = binary_search::find_predicate_begin( + zone_cache.frame_caches, + [&](const std::unique_ptr &value) { + return value->frame > current_frame_; + }); + frame_indices.next = (first_future_frame_index == zone_cache.frame_caches.size()) ? + std::nullopt : + std::optional(first_future_frame_index); + if (first_future_frame_index > 0) { + const int index = first_future_frame_index - 1; + if (zone_cache.frame_caches[index]->frame < current_frame_) { + frame_indices.prev = index; + } + else { + BLI_assert(zone_cache.frame_caches[index]->frame == current_frame_); + frame_indices.current = index; + if (index > 0) { + frame_indices.prev = index - 1; + } + } + } + } + return frame_indices; + } - /* Calculate the delta time. */ - if (realtime_cache.prev_state) { - const float max_delta_frames = 1.0f; - const float scene_delta_frames = float(current_frame) - float(realtime_cache.prev_frame); - const float delta_frames = std::min(max_delta_frames, scene_delta_frames); - const float delta_seconds = delta_frames / FPS; - exec_data.simulation_time_delta = delta_seconds; + void input_pass_through(nodes::SimulationZoneBehavior &zone_behavior) const + { + zone_behavior.input.emplace(); + } + + void output_store_frame_cache(bke::sim::SimulationZoneCache &zone_cache, + nodes::SimulationZoneBehavior &zone_behavior) const + { + auto &store_and_pass_through_info = + zone_behavior.output.emplace(); + store_and_pass_through_info.store_fn = + [simulation_cache = simulation_cache_, + zone_cache = &zone_cache, + current_frame = current_frame_](Map> items) { + std::lock_guard lock{simulation_cache->mutex}; + auto frame_cache = std::make_unique(); + frame_cache->frame = current_frame; + frame_cache->items = std::move(items); + zone_cache->frame_caches.append(std::move(frame_cache)); + }; + } + + void store_as_prev_items(bke::sim::SimulationZoneCache &zone_cache, + nodes::SimulationZoneBehavior &zone_behavior) const + { + auto &store_and_pass_through_info = + zone_behavior.output.emplace(); + store_and_pass_through_info.store_fn = + [simulation_cache = simulation_cache_, + zone_cache = &zone_cache, + current_frame = current_frame_](Map> items) { + std::lock_guard lock{simulation_cache->mutex}; + if (!zone_cache->prev_state) { + zone_cache->prev_state.emplace(); + } + zone_cache->prev_state->items = std::move(items); + zone_cache->prev_state->frame = current_frame; + }; + } + + void read_from_cache(const FrameIndices &frame_indices, + bke::sim::SimulationZoneCache &zone_cache, + nodes::SimulationZoneBehavior &zone_behavior) const + { + if (frame_indices.prev) { + auto &output_copy_info = zone_behavior.input.emplace(); + bke::sim::SimulationZoneFrameCache &frame_cache = + *zone_cache.frame_caches[*frame_indices.prev]; + const float delta_frames = std::min(max_delta_frames, + float(current_frame_) - float(frame_cache.frame)); + output_copy_info.delta_time = delta_frames / fps_; + for (auto item : frame_cache.items.items()) { + output_copy_info.items_by_id.add_new(item.key, item.value.get()); + } + } + else { + zone_behavior.input.emplace(); + } + if (frame_indices.current) { + this->read_single(*frame_indices.current, zone_cache, zone_behavior); + } + else if (frame_indices.next) { + if (frame_indices.prev) { + this->read_interpolated( + *frame_indices.prev, *frame_indices.next, zone_cache, zone_behavior); } else { - exec_data.simulation_time_delta = 0.0f; + this->read_empty(zone_behavior); + } + } + else if (frame_indices.prev) { + this->read_single(*frame_indices.prev, zone_cache, zone_behavior); + } + else { + this->read_empty(zone_behavior); + } + } + + void read_empty(nodes::SimulationZoneBehavior &zone_behavior) const + { + zone_behavior.output.emplace(); + } + + void read_single(const int frame_index, + bke::sim::SimulationZoneCache &zone_cache, + nodes::SimulationZoneBehavior &zone_behavior) const + { + bke::sim::SimulationZoneFrameCache &frame_cache = *zone_cache.frame_caches[frame_index]; + this->ensure_bake_loaded(zone_cache, frame_cache); + auto &read_single_info = zone_behavior.output.emplace(); + read_single_info.items_by_id = this->to_readonly_items_map(frame_cache.items); + } + + void read_interpolated(const int prev_frame_index, + const int next_frame_index, + bke::sim::SimulationZoneCache &zone_cache, + nodes::SimulationZoneBehavior &zone_behavior) const + { + bke::sim::SimulationZoneFrameCache &prev_frame_cache = + *zone_cache.frame_caches[prev_frame_index]; + bke::sim::SimulationZoneFrameCache &next_frame_cache = + *zone_cache.frame_caches[next_frame_index]; + this->ensure_bake_loaded(zone_cache, prev_frame_cache); + this->ensure_bake_loaded(zone_cache, next_frame_cache); + auto &read_interpolated_info = zone_behavior.output.emplace(); + read_interpolated_info.mix_factor = (float(current_frame_) - float(prev_frame_cache.frame)) / + (float(next_frame_cache.frame) - + float(prev_frame_cache.frame)); + read_interpolated_info.prev_items_by_id = this->to_readonly_items_map(prev_frame_cache.items); + read_interpolated_info.next_items_by_id = this->to_readonly_items_map(next_frame_cache.items); + } + + Map to_readonly_items_map( + const Map> &items) const + { + Map map; + for (auto item : items.items()) { + map.add_new(item.key, item.value.get()); + } + return map; + } + + void ensure_bake_loaded(bke::sim::SimulationZoneCache &zone_cache, + bke::sim::SimulationZoneFrameCache &frame_cache) const + { + if (!frame_cache.items.is_empty()) { + return; + } + if (!zone_cache.bdata_dir) { + return; + } + if (!frame_cache.meta_path) { + return; + } + std::shared_ptr io_root_value = io::serialize::read_json_file( + *frame_cache.meta_path); + const io::serialize::DictionaryValue *io_root = io_root_value->as_dictionary_value(); + if (!io_root) { + return; + } + if (io_root->lookup_int("version").value_or(0) != bke::sim::simulation_file_storage_version) { + return; + } + const io::serialize::DictionaryValue *io_items = io_root->lookup_dict("items"); + if (!io_items) { + return; + } + + bke::DiskBDataReader bdata_reader{*zone_cache.bdata_dir}; + + for (const auto &io_item_value : io_items->elements()) { + const io::serialize::DictionaryValue *io_item = io_item_value.second->as_dictionary_value(); + if (!io_item) { + continue; + } + const int zone_id = std::stoi(io_item_value.first); + std::unique_ptr item = bke::deserialize_bake_item( + *io_item, bdata_reader, *zone_cache.bdata_sharing); + if (item) { + frame_cache.items.add(zone_id, std::move(item)); } } } -} +}; static void modifyGeometry(ModifierData *md, const ModifierEvalContext *ctx, @@ -868,7 +1114,8 @@ static void modifyGeometry(ModifierData *md, modifier_eval_data.self_object = ctx->object; auto eval_log = std::make_unique(); - prepare_simulation_states_for_evaluation(*nmd, *ctx, modifier_eval_data); + NodesModifierSimulationParams simulation_params(*nmd, *ctx); + modifier_eval_data.simulation_params = &simulation_params; Set socket_log_contexts; if (logging_enabled(ctx)) { diff --git a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh index e0ce42950db..4c1863f1d2a 100644 --- a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh +++ b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh @@ -20,6 +20,8 @@ * #lazy_function::Graph is build that can be used when evaluating the graph (e.g. for logging). */ +#include + #include "FN_lazy_function_graph.hh" #include "FN_lazy_function_graph_executor.hh" @@ -40,6 +42,92 @@ namespace blender::nodes { using lf::LazyFunction; using mf::MultiFunction; +/** The structs in here describe the different possible behaviors of a simulation input node. */ +namespace sim_input { + +/** + * The data is just passed through the node. Data that is incompatible with simulations (like + * anonymous attributes), is removed though. + */ +struct PassThrough { +}; + +/** + * The input is not evaluated, instead the values provided here are output by the node. + */ +struct OutputCopy { + float delta_time; + Map items_by_id; +}; + +/** + * Same as above, but the values can be output by move, instead of copy. This can reduce the amount + * of unnecessary copies, when the old simulation state is not needed anymore. + */ +struct OutputMove { + float delta_time; + Map> items_by_id; +}; + +using Behavior = std::variant; + +} // namespace sim_input + +/** The structs in here describe the different possible behaviors of a simulation output node. */ +namespace sim_output { + +/** + * The data is just passed through the node. Data that is incompatible with simulations (like + * anonymous attributes), is removed though. + */ +struct PassThrough { +}; + +/** + * Same as above, but also calls the given function with the data that is passed through the node. + * This allows the caller of geometry nodes (e.g. the modifier), to cache the new simulation state. + */ +struct StoreAndPassThrough { + std::function> items_by_id)> store_fn; +}; + +/** + * The inputs are not evaluated, instead the given cached items are output directly. + */ +struct ReadSingle { + Map items_by_id; +}; + +/** + * The inputs are not evaluated, instead of a mix of the two given states is output. + */ +struct ReadInterpolated { + /** Factor between 0 and 1 that determines the influence of the two simulation states. */ + float mix_factor; + Map prev_items_by_id; + Map next_items_by_id; +}; + +using Behavior = std::variant; + +} // namespace sim_output + +/** Controls the behavior of one simulation zone. */ +struct SimulationZoneBehavior { + sim_input::Behavior input; + sim_output::Behavior output; +}; + +class GeoNodesSimulationParams { + public: + /** + * Get the expected behavior for the simulation zone with the given id (see #bNestedNodeRef). + * It's possible that this method called multiple times for the same id. In this case, the same + * pointer should be returned in each call. + */ + virtual SimulationZoneBehavior *get(const int zone_id) const = 0; +}; + /** * Data that is passed into geometry nodes evaluation from the modifier. */ @@ -51,20 +139,7 @@ struct GeoNodesModifierData { /** Optional logger. */ geo_eval_log::GeoModifierLog *eval_log = nullptr; - /** Read-only simulation states around the current frame. */ - const bke::sim::ModifierSimulationState *current_simulation_state = nullptr; - const bke::sim::ModifierSimulationState *prev_simulation_state = nullptr; - const bke::sim::ModifierSimulationState *next_simulation_state = nullptr; - float simulation_state_mix_factor = 0.0f; - /** Used when the evaluation should create a new simulation state. */ - bke::sim::ModifierSimulationState *current_simulation_state_for_write = nullptr; - float simulation_time_delta = 0.0f; - - /** - * The same as #prev_simulation_state, but the cached values can be moved from, - * to keep data managed by implicit sharing mutable. - */ - bke::sim::ModifierSimulationState *prev_simulation_state_mutable = nullptr; + GeoNodesSimulationParams *simulation_params = nullptr; /** * Some nodes should be executed even when their output is not used (e.g. active viewer nodes and @@ -264,8 +339,14 @@ std::unique_ptr get_simulation_input_lazy_function( GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info); std::unique_ptr get_switch_node_lazy_function(const bNode &node); -std::optional get_simulation_zone_id( - const GeoNodesLFUserData &user_data, const int output_node_id); +struct FoundNestedNodeID { + int id; + bool is_in_simulation = false; + bool is_in_loop = false; +}; + +std::optional find_nested_node_id(const GeoNodesLFUserData &user_data, + const int node_id); /** * An anonymous attribute created by a node. diff --git a/source/blender/nodes/geometry/node_geometry_util.hh b/source/blender/nodes/geometry/node_geometry_util.hh index 7c0663aef4f..116e0985f16 100644 --- a/source/blender/nodes/geometry/node_geometry_util.hh +++ b/source/blender/nodes/geometry/node_geometry_util.hh @@ -136,17 +136,17 @@ void socket_declarations_for_simulation_items(Span items, NodeDeclaration &r_declaration); const CPPType &get_simulation_item_cpp_type(eNodeSocketDatatype socket_type); const CPPType &get_simulation_item_cpp_type(const NodeSimulationItem &item); -void move_values_to_simulation_state(const Span node_simulation_items, - const Span input_values, - bke::sim::SimulationZoneState &r_zone_state); + +Map> move_values_to_simulation_state( + const Span node_simulation_items, const Span input_values); void move_simulation_state_to_values(const Span node_simulation_items, - bke::sim::SimulationZoneState &zone_state, + Map> zone_state, const Object &self_object, const ComputeContext &compute_context, const bNode &sim_output_node, Span r_output_values); void copy_simulation_state_to_values(const Span node_simulation_items, - const bke::sim::SimulationZoneState &zone_state, + const Map &zone_state, const Object &self_object, const ComputeContext &compute_context, const bNode &sim_output_node, diff --git a/source/blender/nodes/geometry/nodes/node_geo_simulation_input.cc b/source/blender/nodes/geometry/nodes/node_geo_simulation_input.cc index 9934cba5f09..b5c6e8ba675 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_simulation_input.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_simulation_input.cc @@ -64,54 +64,56 @@ class LazyFunctionForSimulationInputNode final : public LazyFunction { return; } const GeoNodesModifierData &modifier_data = *user_data.modifier_data; - - if (!params.output_was_set(0)) { - const float delta_time = modifier_data.simulation_time_delta; - params.set_output(0, fn::ValueOrField(delta_time)); - } - - if (modifier_data.current_simulation_state == nullptr) { - this->pass_through(params, user_data); - return; - } - - const std::optional zone_id = get_simulation_zone_id( - user_data, output_node_id_); - if (!zone_id) { + if (!modifier_data.simulation_params) { params.set_default_remaining_outputs(); return; } - - /* When caching is turned off and the old state doesn't need to persist, moving data - * from the last state instead of copying it can avoid copies of geometry data arrays. */ - if (auto *state = modifier_data.prev_simulation_state_mutable) { - if (bke::sim::SimulationZoneState *zone = state->get_zone_state(*zone_id)) { - this->output_simulation_state_move(params, user_data, *zone); - return; - } + std::optional found_id = find_nested_node_id(user_data, output_node_id_); + if (!found_id) { + params.set_default_remaining_outputs(); + return; } - - /* If there is a read-only state from the last frame, output that directly. */ - if (const auto *state = modifier_data.prev_simulation_state) { - if (const bke::sim::SimulationZoneState *zone = state->get_zone_state(*zone_id)) { - this->output_simulation_state_copy(params, user_data, *zone); - return; - } + if (found_id->is_in_loop) { + params.set_default_remaining_outputs(); + return; + } + SimulationZoneBehavior *zone_behavior = modifier_data.simulation_params->get(found_id->id); + if (!zone_behavior) { + params.set_default_remaining_outputs(); + return; + } + sim_input::Behavior &input_behavior = zone_behavior->input; + float delta_time = 0.0f; + if (auto *info = std::get_if(&input_behavior)) { + delta_time = info->delta_time; + this->output_simulation_state_copy(params, user_data, info->items_by_id); + } + else if (auto *info = std::get_if(&input_behavior)) { + delta_time = info->delta_time; + this->output_simulation_state_move(params, user_data, std::move(info->items_by_id)); + } + else if (std::get_if(&input_behavior)) { + delta_time = 0.0f; + this->pass_through(params, user_data); + } + else { + BLI_assert_unreachable(); + } + if (!params.output_was_set(0)) { + params.set_output(0, fn::ValueOrField(delta_time)); } - - this->pass_through(params, user_data); } void output_simulation_state_copy(lf::Params ¶ms, const GeoNodesLFUserData &user_data, - const bke::sim::SimulationZoneState &state) const + const Map &zone_state) const { Array outputs(simulation_items_.size()); for (const int i : simulation_items_.index_range()) { outputs[i] = params.get_output_data_ptr(i + 1); } copy_simulation_state_to_values(simulation_items_, - state, + std::move(zone_state), *user_data.modifier_data->self_object, *user_data.compute_context, node_, @@ -123,14 +125,14 @@ class LazyFunctionForSimulationInputNode final : public LazyFunction { void output_simulation_state_move(lf::Params ¶ms, const GeoNodesLFUserData &user_data, - bke::sim::SimulationZoneState &state) const + Map> zone_state) const { Array outputs(simulation_items_.size()); for (const int i : simulation_items_.index_range()) { outputs[i] = params.get_output_data_ptr(i + 1); } move_simulation_state_to_values(simulation_items_, - state, + std::move(zone_state), *user_data.modifier_data->self_object, *user_data.compute_context, node_, @@ -153,9 +155,9 @@ class LazyFunctionForSimulationInputNode final : public LazyFunction { /* Instead of outputting the initial values directly, convert them to a simulation state and * then back. This ensures that some geometry processing happens on the data consistently (e.g. * removing anonymous attributes). */ - bke::sim::SimulationZoneState state; - move_values_to_simulation_state(simulation_items_, input_values, state); - this->output_simulation_state_move(params, user_data, state); + Map> bake_items = move_values_to_simulation_state( + simulation_items_, input_values); + this->output_simulation_state_move(params, user_data, std::move(bake_items)); } }; diff --git a/source/blender/nodes/geometry/nodes/node_geo_simulation_output.cc b/source/blender/nodes/geometry/nodes/node_geo_simulation_output.cc index 59b9b326a19..c470a4ef05c 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_simulation_output.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_simulation_output.cc @@ -185,7 +185,7 @@ static std::shared_ptr make_attribute_field( } void move_simulation_state_to_values(const Span node_simulation_items, - bke::sim::SimulationZoneState &zone_state, + Map> zone_state, const Object &self_object, const ComputeContext &compute_context, const bNode &node, @@ -194,7 +194,7 @@ void move_simulation_state_to_values(const Span node_simulat const bke::BakeSocketConfig config = make_bake_socket_config(node_simulation_items); Vector bake_items; for (const NodeSimulationItem &item : node_simulation_items) { - auto *bake_item = zone_state.item_by_identifier.lookup_ptr(item.identifier); + auto *bake_item = zone_state.lookup_ptr(item.identifier); bake_items.append(bake_item ? bake_item->get() : nullptr); } @@ -209,17 +209,17 @@ void move_simulation_state_to_values(const Span node_simulat } void copy_simulation_state_to_values(const Span node_simulation_items, - const bke::sim::SimulationZoneState &zone_state, + const Map &zone_state, const Object &self_object, const ComputeContext &compute_context, const bNode &node, Span r_output_values) { const bke::BakeSocketConfig config = make_bake_socket_config(node_simulation_items); - Vector bake_items; + Vector bake_items; for (const NodeSimulationItem &item : node_simulation_items) { - auto *bake_item = zone_state.item_by_identifier.lookup_ptr(item.identifier); - bake_items.append(bake_item ? bake_item->get() : nullptr); + const bke::BakeItem *const *bake_item = zone_state.lookup_ptr(item.identifier); + bake_items.append(bake_item ? *bake_item : nullptr); } bke::copy_bake_items_to_socket_values( @@ -232,22 +232,23 @@ void copy_simulation_state_to_values(const Span node_simulat r_output_values); } -void move_values_to_simulation_state(const Span node_simulation_items, - const Span input_values, - bke::sim::SimulationZoneState &r_zone_state) +Map> move_values_to_simulation_state( + const Span node_simulation_items, const Span input_values) { const bke::BakeSocketConfig config = make_bake_socket_config(node_simulation_items); Array> bake_items = bke::move_socket_values_to_bake_items( input_values, config); + Map> bake_items_map; for (const int i : node_simulation_items.index_range()) { const NodeSimulationItem &item = node_simulation_items[i]; std::unique_ptr &bake_item = bake_items[i]; if (bake_item) { - r_zone_state.item_by_identifier.add_new(item.identifier, std::move(bake_item)); + bake_items_map.add_new(item.identifier, std::move(bake_item)); } } + return bake_items_map; } } // namespace blender::nodes @@ -256,10 +257,6 @@ namespace blender::nodes::node_geo_simulation_output_cc { NODE_STORAGE_FUNCS(NodeGeometrySimulationOutput); -struct EvalData { - bool is_first_evaluation = true; -}; - static bool sharing_info_equal(const ImplicitSharingInfo *a, const ImplicitSharingInfo *b) { if (!a || !b) { @@ -542,16 +539,6 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction { } } - void *init_storage(LinearAllocator<> &allocator) const - { - return allocator.construct().release(); - } - - void destruct_storage(void *storage) const - { - std::destroy_at(static_cast(storage)); - } - void execute_impl(lf::Params ¶ms, const lf::Context &context) const final { GeoNodesLFUserData &user_data = *static_cast(context.user_data); @@ -559,78 +546,51 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction { params.set_default_remaining_outputs(); return; } - GeoNodesModifierData &modifier_data = *user_data.modifier_data; - EvalData &eval_data = *static_cast(context.storage); - BLI_SCOPED_DEFER([&]() { eval_data.is_first_evaluation = false; }); - - const std::optional zone_id = get_simulation_zone_id( - user_data, node_.identifier); - if (!zone_id) { + const GeoNodesModifierData &modifier_data = *user_data.modifier_data; + if (!modifier_data.simulation_params) { params.set_default_remaining_outputs(); return; } - - const bke::sim::SimulationZoneState *current_zone_state = - modifier_data.current_simulation_state ? - modifier_data.current_simulation_state->get_zone_state(*zone_id) : - nullptr; - if (eval_data.is_first_evaluation && current_zone_state != nullptr) { - /* Common case when data is cached already. */ - this->output_cached_state(params, user_data, *current_zone_state); + std::optional found_id = find_nested_node_id(user_data, node_.identifier); + if (!found_id) { + params.set_default_remaining_outputs(); return; } - - if (modifier_data.current_simulation_state_for_write == nullptr) { - const bke::sim::SimulationZoneState *prev_zone_state = - modifier_data.prev_simulation_state ? - modifier_data.prev_simulation_state->get_zone_state(*zone_id) : - nullptr; - if (prev_zone_state == nullptr) { - /* There is no previous simulation state and we also don't create a new one, so just pass - * the data through. */ - this->pass_through(params, user_data); - return; - } - const bke::sim::SimulationZoneState *next_zone_state = - modifier_data.next_simulation_state ? - modifier_data.next_simulation_state->get_zone_state(*zone_id) : - nullptr; - if (next_zone_state == nullptr) { - /* Output the last cached simulation state. */ - this->output_cached_state(params, user_data, *prev_zone_state); - return; - } - /* A previous and next frame is cached already, but the current frame is not. */ + if (found_id->is_in_loop) { + params.set_default_remaining_outputs(); + return; + } + SimulationZoneBehavior *zone_behavior = modifier_data.simulation_params->get(found_id->id); + if (!zone_behavior) { + params.set_default_remaining_outputs(); + return; + } + sim_output::Behavior &output_behavior = zone_behavior->output; + if (auto *info = std::get_if(&output_behavior)) { + this->output_cached_state(params, user_data, info->items_by_id); + } + else if (auto *info = std::get_if(&output_behavior)) { this->output_mixed_cached_state(params, *modifier_data.self_object, *user_data.compute_context, - *prev_zone_state, - *next_zone_state, - modifier_data.simulation_state_mix_factor); - return; + info->prev_items_by_id, + info->next_items_by_id, + info->mix_factor); } - - bke::sim::SimulationZoneState &new_zone_state = - modifier_data.current_simulation_state_for_write->get_zone_state_for_write(*zone_id); - if (eval_data.is_first_evaluation) { - new_zone_state.item_by_identifier.clear(); + else if (std::get_if(&output_behavior)) { + this->pass_through(params, user_data); } - - Array input_values(simulation_items_.size(), nullptr); - for (const int i : simulation_items_.index_range()) { - input_values[i] = params.try_get_input_data_ptr_or_request(i); + else if (auto *info = std::get_if(&output_behavior)) { + this->store_and_pass_through(params, user_data, *info); } - if (input_values.as_span().contains(nullptr)) { - /* Wait until all inputs are available. */ - return; + else { + BLI_assert_unreachable(); } - move_values_to_simulation_state(simulation_items_, input_values, new_zone_state); - this->output_cached_state(params, user_data, new_zone_state); } void output_cached_state(lf::Params ¶ms, GeoNodesLFUserData &user_data, - const bke::sim::SimulationZoneState &state) const + const Map &state) const { Array output_values(simulation_items_.size()); for (const int i : simulation_items_.index_range()) { @@ -650,8 +610,8 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction { void output_mixed_cached_state(lf::Params ¶ms, const Object &self_object, const ComputeContext &compute_context, - const bke::sim::SimulationZoneState &prev_state, - const bke::sim::SimulationZoneState &next_state, + const Map &prev_state, + const Map &next_state, const float mix_factor) const { Array output_values(simulation_items_.size()); @@ -685,6 +645,52 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction { } void pass_through(lf::Params ¶ms, GeoNodesLFUserData &user_data) const + { + /* Instead of outputting the initial values directly, convert them to a simulation state and + * then back. This ensures that some geometry processing happens on the data consistently (e.g. + * removing anonymous attributes). */ + std::optional>> bake_items = + this->get_bake_items_from_inputs(params); + if (!bake_items) { + /* Wait for inputs to be computed. */ + return; + } + + Array output_values(simulation_items_.size()); + for (const int i : simulation_items_.index_range()) { + output_values[i] = params.get_output_data_ptr(i); + } + move_simulation_state_to_values(simulation_items_, + std::move(*bake_items), + *user_data.modifier_data->self_object, + *user_data.compute_context, + node_, + output_values); + for (const int i : simulation_items_.index_range()) { + params.output_set(i); + } + } + + void store_and_pass_through(lf::Params ¶ms, + GeoNodesLFUserData &user_data, + const sim_output::StoreAndPassThrough &info) const + { + std::optional>> bake_items = + this->get_bake_items_from_inputs(params); + if (!bake_items) { + /* Wait for inputs to be computed. */ + return; + } + Map bake_item_pointers; + for (const auto item : bake_items->items()) { + bake_item_pointers.add_new(item.key, item.value.get()); + } + this->output_cached_state(params, user_data, bake_item_pointers); + info.store_fn(std::move(*bake_items)); + } + + std::optional>> get_bake_items_from_inputs( + lf::Params ¶ms) const { Array input_values(inputs_.size()); for (const int i : inputs_.index_range()) { @@ -692,14 +698,10 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction { } if (input_values.as_span().contains(nullptr)) { /* Wait for inputs to be computed. */ - return; + return std::nullopt; } - /* Instead of outputting the initial values directly, convert them to a simulation state and - * then back. This ensures that some geometry processing happens on the data consistently (e.g. - * removing anonymous attributes). */ - bke::sim::SimulationZoneState state; - move_values_to_simulation_state(simulation_items_, input_values, state); - this->output_cached_state(params, user_data, state); + + return move_values_to_simulation_state(simulation_items_, input_values); } }; @@ -715,30 +717,6 @@ std::unique_ptr get_simulation_output_lazy_function( return std::make_unique(node, own_lf_graph_info); } -std::optional get_simulation_zone_id( - const GeoNodesLFUserData &user_data, const int output_node_id) -{ - Vector node_ids; - for (const ComputeContext *context = user_data.compute_context; context != nullptr; - context = context->parent()) - { - if (const auto *node_context = dynamic_cast(context)) { - node_ids.append(node_context->node_id()); - } - else if (dynamic_cast(context) != nullptr) { - /* Simulation can't be used in a repeat zone. */ - return std::nullopt; - } - } - std::reverse(node_ids.begin(), node_ids.end()); - node_ids.append(output_node_id); - const bNestedNodeRef *nested_node_ref = user_data.root_ntree->nested_node_ref_from_node_id_path( - node_ids); - bke::sim::SimulationZoneID zone_id; - zone_id.nested_node_id = nested_node_ref->id; - return zone_id; -} - } // namespace blender::nodes namespace blender::nodes::node_geo_simulation_output_cc { diff --git a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc index 45d09c300be..591eceb6f67 100644 --- a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc +++ b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc @@ -866,13 +866,15 @@ class LazyFunctionForViewerInputUsage : public LazyFunction { }; class LazyFunctionForSimulationInputsUsage : public LazyFunction { + private: + const bNode *output_bnode_; public: - LazyFunctionForSimulationInputsUsage() + LazyFunctionForSimulationInputsUsage(const bNode &output_bnode) : output_bnode_(&output_bnode) { debug_name_ = "Simulation Inputs Usage"; - outputs_.append_as("Is Initialization", CPPType::get()); - outputs_.append_as("Do Simulation Step", CPPType::get()); + outputs_.append_as("Need Input Inputs", CPPType::get()); + outputs_.append_as("Need Output Inputs", CPPType::get()); } void execute_impl(lf::Params ¶ms, const lf::Context &context) const override @@ -883,11 +885,39 @@ class LazyFunctionForSimulationInputsUsage : public LazyFunction { return; } const GeoNodesModifierData &modifier_data = *user_data.modifier_data; + if (!modifier_data.simulation_params) { + params.set_default_remaining_outputs(); + return; + } + const std::optional found_id = find_nested_node_id( + user_data, output_bnode_->identifier); + if (!found_id) { + params.set_default_remaining_outputs(); + return; + } + if (found_id->is_in_loop) { + params.set_default_remaining_outputs(); + return; + } + SimulationZoneBehavior *zone_behavior = modifier_data.simulation_params->get(found_id->id); + if (!zone_behavior) { + params.set_default_remaining_outputs(); + return; + } - params.set_output(0, - modifier_data.current_simulation_state_for_write != nullptr && - modifier_data.prev_simulation_state == nullptr); - params.set_output(1, modifier_data.current_simulation_state_for_write != nullptr); + bool solve_contains_side_effect = false; + if (modifier_data.side_effect_nodes) { + const Span side_effect_nodes = + modifier_data.side_effect_nodes->lookup(user_data.compute_context->hash()); + solve_contains_side_effect = !side_effect_nodes.is_empty(); + } + + params.set_output(0, std::holds_alternative(zone_behavior->input)); + params.set_output( + 1, + solve_contains_side_effect || + std::holds_alternative(zone_behavior->output) || + std::holds_alternative(zone_behavior->output)); } }; @@ -1869,7 +1899,8 @@ struct GeometryNodesLazyFunctionGraphBuilder { lf::Node &lf_border_link_usage_node = this->build_border_link_input_usage_node(zone, lf_graph); lf::Node &lf_simulation_usage_node = [&]() -> lf::Node & { - auto &lazy_function = scope_.construct(); + auto &lazy_function = scope_.construct( + *zone.output_node); lf::Node &lf_node = lf_graph.add_function(lazy_function); if (lf_main_input_usage_node) { @@ -4006,4 +4037,33 @@ GeoNodesLFLocalUserData::GeoNodesLFLocalUserData(GeoNodesLFUserData &user_data) } } +std::optional find_nested_node_id(const GeoNodesLFUserData &user_data, + const int node_id) +{ + FoundNestedNodeID found; + Vector node_ids; + for (const ComputeContext *context = user_data.compute_context; context != nullptr; + context = context->parent()) + { + if (const auto *node_context = dynamic_cast(context)) { + node_ids.append(node_context->node_id()); + } + else if (dynamic_cast(context) != nullptr) { + found.is_in_loop = true; + } + else if (dynamic_cast(context) != nullptr) { + found.is_in_simulation = true; + } + } + std::reverse(node_ids.begin(), node_ids.end()); + node_ids.append(node_id); + const bNestedNodeRef *nested_node_ref = user_data.root_ntree->nested_node_ref_from_node_id_path( + node_ids); + if (nested_node_ref == nullptr) { + return std::nullopt; + } + found.id = nested_node_ref->id; + return found; +} + } // namespace blender::nodes