diff options
Diffstat (limited to 'source/blender/blenkernel/intern/image_partial_update.cc')
-rw-r--r-- | source/blender/blenkernel/intern/image_partial_update.cc | 598 |
1 files changed, 598 insertions, 0 deletions
diff --git a/source/blender/blenkernel/intern/image_partial_update.cc b/source/blender/blenkernel/intern/image_partial_update.cc new file mode 100644 index 00000000000..7e187c2014e --- /dev/null +++ b/source/blender/blenkernel/intern/image_partial_update.cc @@ -0,0 +1,598 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright 2021, Blender Foundation. + */ +/** + * \file image_gpu_partial_update.cc + * \ingroup bke + * + * To reduce the overhead of image processing this file contains a mechanism to detect areas of the + * image that are changed. These areas are organized in chunks. Changes that happen over time are + * organized in changesets. + * + * A common usecase is to update GPUTexture for drawing where only that part is uploaded that only + * changed. + * + * Usage: + * + * ``` + * Image *image = ...; + * ImBuf *image_buffer = ...; + * + * // Partial_update_user should be kept for the whole session where the changes needs to be + * // tracked. Keep this instance alive as long as you need to track image changes. + * + * PartialUpdateUser *partial_update_user = BKE_image_partial_update_create(image); + * + * ... + * + * switch (BKE_image_partial_update_collect_changes(image, image_buffer)) + * { + * case ePartialUpdateCollectResult::FullUpdateNeeded: + * // Unable to do partial updates. Perform a full update. + * break; + * case ePartialUpdateCollectResult::PartialChangesDetected: + * PartialUpdateRegion change; + * while (BKE_image_partial_update_get_next_change(partial_update_user, &change) == + * ePartialUpdateIterResult::ChangeAvailable){ + * // Do something with the change. + * } + * case ePartialUpdateCollectResult::NoChangesDetected: + * break; + * } + * + * ... + * + * // Free partial_update_user. + * BKE_image_partial_update_free(partial_update_user); + * + * ``` + */ + +#include <optional> + +#include "BKE_image.h" +#include "BKE_image_partial_update.hh" + +#include "DNA_image_types.h" + +#include "IMB_imbuf.h" +#include "IMB_imbuf_types.h" + +#include "BLI_vector.hh" + +namespace blender::bke::image::partial_update { + +/** \brief Size of chunks to track changes. */ +constexpr int CHUNK_SIZE = 256; + +/** + * \brief Max number of changesets to keep in history. + * + * A higher number would need more memory and processing + * to calculate a changeset, but would lead to do partial updates for requests that don't happen + * every frame. + * + * A to small number would lead to more full updates when changes couldn't be reconstructed from + * the available history. + */ +constexpr int MAX_HISTORY_LEN = 4; + +/** + * \brief get the chunk number for the give pixel coordinate. + * + * As chunks are squares the this member can be used for both x and y axis. + */ +static int chunk_number_for_pixel(int pixel_offset) +{ + int chunk_offset = pixel_offset / CHUNK_SIZE; + if (pixel_offset < 0) { + chunk_offset -= 1; + } + return chunk_offset; +} + +struct PartialUpdateUserImpl; +struct PartialUpdateRegisterImpl; + +/** + * Wrap PartialUpdateUserImpl to its C-struct (PartialUpdateUser). + */ +static struct PartialUpdateUser *wrap(PartialUpdateUserImpl *user) +{ + return static_cast<struct PartialUpdateUser *>(static_cast<void *>(user)); +} + +/** + * Unwrap the PartialUpdateUser C-struct to its CPP counterpart (PartialUpdateUserImpl). + */ +static PartialUpdateUserImpl *unwrap(struct PartialUpdateUser *user) +{ + return static_cast<PartialUpdateUserImpl *>(static_cast<void *>(user)); +} + +/** + * Wrap PartialUpdateRegisterImpl to its C-struct (PartialUpdateRegister). + */ +static struct PartialUpdateRegister *wrap(PartialUpdateRegisterImpl *partial_update_register) +{ + return static_cast<struct PartialUpdateRegister *>(static_cast<void *>(partial_update_register)); +} + +/** + * Unwrap the PartialUpdateRegister C-struct to its CPP counterpart (PartialUpdateRegisterImpl). + */ +static PartialUpdateRegisterImpl *unwrap(struct PartialUpdateRegister *partial_update_register) +{ + return static_cast<PartialUpdateRegisterImpl *>(static_cast<void *>(partial_update_register)); +} + +using TileNumber = int32_t; +using ChangesetID = int64_t; +constexpr ChangesetID UnknownChangesetID = -1; + +struct PartialUpdateUserImpl { + /** \brief last changeset id that was seen by this user. */ + ChangesetID last_changeset_id = UnknownChangesetID; + + /** \brief regions that have been updated. */ + Vector<PartialUpdateRegion> updated_regions; + +#ifdef NDEBUG + /** \brief reference to image to validate correct API usage. */ + const void *debug_image_; +#endif + + /** + * \brief Clear the list of updated regions. + * + * Updated regions should be cleared at the start of #BKE_image_partial_update_collect_changes so + * the + */ + void clear_updated_regions() + { + updated_regions.clear(); + } +}; + +/** + * \brief Dirty chunks of an ImageTile. + * + * Internally dirty tiles are grouped together in change sets to make sure that the correct + * answer can be built for different users reducing the amount of merges. + */ +struct TileChangeset { + private: + /** \brief Dirty flag for each chunk. */ + std::vector<bool> chunk_dirty_flags_; + /** \brief are there dirty/ */ + bool has_dirty_chunks_ = false; + + public: + /** \brief Width of the tile in pixels. */ + int tile_width; + /** \brief Height of the tile in pixels. */ + int tile_height; + /** \brief Number of chunks along the x-axis. */ + int chunk_x_len; + /** \brief Number of chunks along the y-axis. */ + int chunk_y_len; + + TileNumber tile_number; + + void clear() + { + init_chunks(chunk_x_len, chunk_y_len); + } + + /** + * \brief Update the resolution of the tile. + * + * \returns true: resolution has been updated. + * false: resolution was unchanged. + */ + bool update_resolution(const ImBuf *image_buffer) + { + if (tile_width == image_buffer->x && tile_height == image_buffer->y) { + return false; + } + + tile_width = image_buffer->x; + tile_height = image_buffer->y; + + int chunk_x_len = tile_width / CHUNK_SIZE; + int chunk_y_len = tile_height / CHUNK_SIZE; + init_chunks(chunk_x_len, chunk_y_len); + return true; + } + + void mark_region(const rcti *updated_region) + { + int start_x_chunk = chunk_number_for_pixel(updated_region->xmin); + int end_x_chunk = chunk_number_for_pixel(updated_region->xmax - 1); + int start_y_chunk = chunk_number_for_pixel(updated_region->ymin); + int end_y_chunk = chunk_number_for_pixel(updated_region->ymax - 1); + + /* Clamp tiles to tiles in image. */ + start_x_chunk = max_ii(0, start_x_chunk); + start_y_chunk = max_ii(0, start_y_chunk); + end_x_chunk = min_ii(chunk_x_len - 1, end_x_chunk); + end_y_chunk = min_ii(chunk_y_len - 1, end_y_chunk); + + /* Early exit when no tiles need to be updated. */ + if (start_x_chunk >= chunk_x_len) { + return; + } + if (start_y_chunk >= chunk_y_len) { + return; + } + if (end_x_chunk < 0) { + return; + } + if (end_y_chunk < 0) { + return; + } + + mark_chunks_dirty(start_x_chunk, start_y_chunk, end_x_chunk, end_y_chunk); + } + + void mark_chunks_dirty(int start_x_chunk, int start_y_chunk, int end_x_chunk, int end_y_chunk) + { + for (int chunk_y = start_y_chunk; chunk_y <= end_y_chunk; chunk_y++) { + for (int chunk_x = start_x_chunk; chunk_x <= end_x_chunk; chunk_x++) { + int chunk_index = chunk_y * chunk_x_len + chunk_x; + chunk_dirty_flags_[chunk_index] = true; + } + } + has_dirty_chunks_ = true; + } + + bool has_dirty_chunks() const + { + return has_dirty_chunks_; + } + + void init_chunks(int chunk_x_len_, int chunk_y_len_) + { + chunk_x_len = chunk_x_len_; + chunk_y_len = chunk_y_len_; + const int chunk_len = chunk_x_len * chunk_y_len; + const int previous_chunk_len = chunk_dirty_flags_.size(); + + chunk_dirty_flags_.resize(chunk_len); + /* Fast exit. When the changeset was already empty no need to re-init the chunk_validity. */ + if (!has_dirty_chunks()) { + return; + } + for (int index = 0; index < min_ii(chunk_len, previous_chunk_len); index++) { + chunk_dirty_flags_[index] = false; + } + has_dirty_chunks_ = false; + } + + /** \brief Merge the given changeset into the receiver. */ + void merge(const TileChangeset &other) + { + BLI_assert(chunk_x_len == other.chunk_x_len); + BLI_assert(chunk_y_len == other.chunk_y_len); + const int chunk_len = chunk_x_len * chunk_y_len; + + for (int chunk_index = 0; chunk_index < chunk_len; chunk_index++) { + chunk_dirty_flags_[chunk_index] = chunk_dirty_flags_[chunk_index] | + other.chunk_dirty_flags_[chunk_index]; + } + has_dirty_chunks_ |= other.has_dirty_chunks_; + } + + /** \brief has a chunk changed inside this changeset. */ + bool is_chunk_dirty(int chunk_x, int chunk_y) const + { + const int chunk_index = chunk_y * chunk_x_len + chunk_x; + return chunk_dirty_flags_[chunk_index]; + } +}; + +/** \brief Changeset keeping track of changes for an image */ +struct Changeset { + private: + Vector<TileChangeset> tiles; + + public: + /** \brief Keep track if any of the tiles have dirty chunks. */ + bool has_dirty_chunks; + + /** + * \brief Retrieve the TileChangeset for the given ImageTile. + * + * When the TileChangeset isn't found, it will be added. + */ + TileChangeset &operator[](const ImageTile *image_tile) + { + for (TileChangeset &tile_changeset : tiles) { + if (tile_changeset.tile_number == image_tile->tile_number) { + return tile_changeset; + } + } + + TileChangeset tile_changeset; + tile_changeset.tile_number = image_tile->tile_number; + tiles.append_as(tile_changeset); + + return tiles.last(); + } + + /** \brief Does this changeset contain data for the given tile. */ + bool has_tile(const ImageTile *image_tile) + { + for (TileChangeset &tile_changeset : tiles) { + if (tile_changeset.tile_number == image_tile->tile_number) { + return true; + } + } + return false; + } + + /** \brief Clear this changeset. */ + void clear() + { + tiles.clear(); + has_dirty_chunks = false; + } +}; + +/** + * \brief Partial update changes stored inside the image runtime. + * + * The PartialUpdateRegisterImpl will keep track of changes over time. Changes are groups inside + * TileChangesets. + */ +struct PartialUpdateRegisterImpl { + /** \brief changeset id of the first changeset kept in #history. */ + ChangesetID first_changeset_id; + /** \brief changeset id of the top changeset kept in #history. */ + ChangesetID last_changeset_id; + + /** \brief history of changesets. */ + Vector<Changeset> history; + /** \brief The current changeset. New changes will be added to this changeset. */ + Changeset current_changeset; + + void update_resolution(const ImageTile *image_tile, const ImBuf *image_buffer) + { + TileChangeset &tile_changeset = current_changeset[image_tile]; + const bool has_dirty_chunks = tile_changeset.has_dirty_chunks(); + const bool resolution_changed = tile_changeset.update_resolution(image_buffer); + + if (has_dirty_chunks && resolution_changed && !history.is_empty()) { + mark_full_update(); + } + } + + void mark_full_update() + { + history.clear(); + last_changeset_id++; + current_changeset.clear(); + first_changeset_id = last_changeset_id; + } + + void mark_region(const ImageTile *image_tile, const rcti *updated_region) + { + TileChangeset &tile_changeset = current_changeset[image_tile]; + tile_changeset.mark_region(updated_region); + current_changeset.has_dirty_chunks |= tile_changeset.has_dirty_chunks(); + } + + void ensure_empty_changeset() + { + if (!current_changeset.has_dirty_chunks) { + /* No need to create a new changeset when previous changeset does not contain any dirty + * tiles. */ + return; + } + commit_current_changeset(); + limit_history(); + } + + /** \brief Move the current changeset to the history and resets the current changeset. */ + void commit_current_changeset() + { + history.append_as(std::move(current_changeset)); + current_changeset.clear(); + last_changeset_id++; + } + + /** \brief Limit the number of items in the changeset. */ + void limit_history() + { + const int num_items_to_remove = max_ii(history.size() - MAX_HISTORY_LEN, 0); + if (num_items_to_remove == 0) { + return; + } + history.remove(0, num_items_to_remove); + first_changeset_id += num_items_to_remove; + } + + /** + * /brief Check if data is available to construct the update tiles for the given + * changeset_id. + * + * The update tiles can be created when changeset id is between + */ + bool can_construct(ChangesetID changeset_id) + { + return changeset_id >= first_changeset_id; + } + + /** + * \brief collect all historic changes since a given changeset. + */ + std::optional<TileChangeset> changed_tile_chunks_since(const ImageTile *image_tile, + const ChangesetID from_changeset) + { + std::optional<TileChangeset> changed_chunks = std::nullopt; + for (int index = from_changeset - first_changeset_id; index < history.size(); index++) { + if (!history[index].has_tile(image_tile)) { + continue; + } + + TileChangeset &tile_changeset = history[index][image_tile]; + if (!changed_chunks.has_value()) { + changed_chunks = std::make_optional<TileChangeset>(); + changed_chunks->init_chunks(tile_changeset.chunk_x_len, tile_changeset.chunk_y_len); + changed_chunks->tile_number = image_tile->tile_number; + } + + changed_chunks->merge(tile_changeset); + } + return changed_chunks; + } +}; + +static PartialUpdateRegister *image_partial_update_register_ensure(Image *image) +{ + if (image->runtime.partial_update_register == nullptr) { + PartialUpdateRegisterImpl *partial_update_register = MEM_new<PartialUpdateRegisterImpl>( + __func__); + image->runtime.partial_update_register = wrap(partial_update_register); + } + return image->runtime.partial_update_register; +} + +ePartialUpdateCollectResult BKE_image_partial_update_collect_changes(Image *image, + PartialUpdateUser *user) +{ + PartialUpdateUserImpl *user_impl = unwrap(user); +#ifdef NDEBUG + BLI_assert(image == user_impl->debug_image_); +#endif + + user_impl->clear_updated_regions(); + + PartialUpdateRegisterImpl *partial_updater = unwrap(image_partial_update_register_ensure(image)); + partial_updater->ensure_empty_changeset(); + + if (!partial_updater->can_construct(user_impl->last_changeset_id)) { + user_impl->last_changeset_id = partial_updater->last_changeset_id; + return ePartialUpdateCollectResult::FullUpdateNeeded; + } + + /* Check if there are changes since last invocation for the user. */ + if (user_impl->last_changeset_id == partial_updater->last_changeset_id) { + return ePartialUpdateCollectResult::NoChangesDetected; + } + + /* Collect changed tiles. */ + LISTBASE_FOREACH (ImageTile *, tile, &image->tiles) { + std::optional<TileChangeset> changed_chunks = partial_updater->changed_tile_chunks_since( + tile, user_impl->last_changeset_id); + /* Check if chunks of this tile are dirty. */ + if (!changed_chunks.has_value()) { + continue; + } + if (!changed_chunks->has_dirty_chunks()) { + continue; + } + + /* Convert tiles in the changeset to rectangles that are dirty. */ + for (int chunk_y = 0; chunk_y < changed_chunks->chunk_y_len; chunk_y++) { + for (int chunk_x = 0; chunk_x < changed_chunks->chunk_x_len; chunk_x++) { + if (!changed_chunks->is_chunk_dirty(chunk_x, chunk_y)) { + continue; + } + + PartialUpdateRegion region; + region.tile_number = tile->tile_number; + BLI_rcti_init(®ion.region, + chunk_x * CHUNK_SIZE, + (chunk_x + 1) * CHUNK_SIZE, + chunk_y * CHUNK_SIZE, + (chunk_y + 1) * CHUNK_SIZE); + user_impl->updated_regions.append_as(region); + } + } + } + + user_impl->last_changeset_id = partial_updater->last_changeset_id; + return ePartialUpdateCollectResult::PartialChangesDetected; +} + +ePartialUpdateIterResult BKE_image_partial_update_get_next_change(PartialUpdateUser *user, + PartialUpdateRegion *r_region) +{ + PartialUpdateUserImpl *user_impl = unwrap(user); + if (user_impl->updated_regions.is_empty()) { + return ePartialUpdateIterResult::Finished; + } + PartialUpdateRegion region = user_impl->updated_regions.pop_last(); + *r_region = region; + return ePartialUpdateIterResult::ChangeAvailable; +} + +} // namespace blender::bke::image::partial_update + +extern "C" { + +using namespace blender::bke::image::partial_update; + +// TODO(jbakker): cleanup parameter. +struct PartialUpdateUser *BKE_image_partial_update_create(const struct Image *image) +{ + PartialUpdateUserImpl *user_impl = MEM_new<PartialUpdateUserImpl>(__func__); + +#ifdef NDEBUG + user_impl->debug_image_ = image; +#else + UNUSED_VARS(image); +#endif + + return wrap(user_impl); +} + +void BKE_image_partial_update_free(PartialUpdateUser *user) +{ + PartialUpdateUserImpl *user_impl = unwrap(user); + MEM_delete<PartialUpdateUserImpl>(user_impl); +} + +/* --- Image side --- */ + +void BKE_image_partial_update_register_free(Image *image) +{ + PartialUpdateRegisterImpl *partial_update_register = unwrap( + image->runtime.partial_update_register); + if (partial_update_register) { + MEM_delete<PartialUpdateRegisterImpl>(partial_update_register); + } + image->runtime.partial_update_register = nullptr; +} + +void BKE_image_partial_update_mark_region(Image *image, + const ImageTile *image_tile, + const ImBuf *image_buffer, + const rcti *updated_region) +{ + PartialUpdateRegisterImpl *partial_updater = unwrap(image_partial_update_register_ensure(image)); + partial_updater->update_resolution(image_tile, image_buffer); + partial_updater->mark_region(image_tile, updated_region); +} + +void BKE_image_partial_update_mark_full_update(Image *image) +{ + PartialUpdateRegisterImpl *partial_updater = unwrap(image_partial_update_register_ensure(image)); + partial_updater->mark_full_update(); +} +} |