diff options
Diffstat (limited to 'source/blender/blenkernel')
-rw-r--r-- | source/blender/blenkernel/BKE_image.h | 40 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_image_partial_update.hh | 298 | ||||
-rw-r--r-- | source/blender/blenkernel/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/image.c | 24 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/image_gpu.cc | 184 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/image_partial_update.cc | 598 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/image_partial_update_test.cc | 393 |
7 files changed, 1429 insertions, 110 deletions
diff --git a/source/blender/blenkernel/BKE_image.h b/source/blender/blenkernel/BKE_image.h index 80c6b155be0..598818ba3c0 100644 --- a/source/blender/blenkernel/BKE_image.h +++ b/source/blender/blenkernel/BKE_image.h @@ -24,6 +24,8 @@ #include "BLI_utildefines.h" +#include "BLI_rect.h" + #ifdef __cplusplus extern "C" { #endif @@ -561,19 +563,27 @@ struct GPUTexture *BKE_image_get_gpu_tilemap(struct Image *image, * Is the alpha of the `GPUTexture` for a given image/ibuf premultiplied. */ bool BKE_image_has_gpu_texture_premultiplied_alpha(struct Image *image, struct ImBuf *ibuf); + /** * Partial update of texture for texture painting. * This is often much quicker than fully updating the texture for high resolution images. */ void BKE_image_update_gputexture( struct Image *ima, struct ImageUser *iuser, int x, int y, int w, int h); + /** * Mark areas on the #GPUTexture that needs to be updated. The areas are marked in chunks. * The next time the #GPUTexture is used these tiles will be refreshes. This saves time * when writing to the same place multiple times This happens for during foreground rendering. */ -void BKE_image_update_gputexture_delayed( - struct Image *ima, struct ImBuf *ibuf, int x, int y, int w, int h); +void BKE_image_update_gputexture_delayed(struct Image *ima, + struct ImageTile *image_tile, + struct ImBuf *ibuf, + int x, + int y, + int w, + int h); + /** * Called on entering and exiting texture paint mode, * temporary disabling/enabling mipmapping on all images for quick texture @@ -591,6 +601,32 @@ bool BKE_image_remove_renderslot(struct Image *ima, struct ImageUser *iuser, int struct RenderSlot *BKE_image_get_renderslot(struct Image *ima, int index); bool BKE_image_clear_renderslot(struct Image *ima, struct ImageUser *iuser, int slot); +/* --- image_partial_update.cc --- */ +/** Image partial updates. */ +struct PartialUpdateUser; + +/** + * \brief Create a new PartialUpdateUser. An Object that contains data to use partial updates. + */ +struct PartialUpdateUser *BKE_image_partial_update_create(const struct Image *image); + +/** + * \brief free a partial update user. + */ +void BKE_image_partial_update_free(struct PartialUpdateUser *user); + +/* --- partial updater (image side) --- */ +struct PartialUpdateRegister; + +void BKE_image_partial_update_register_free(struct Image *image); +/** \brief Mark a region of the image to update. */ +void BKE_image_partial_update_mark_region(struct Image *image, + const struct ImageTile *image_tile, + const struct ImBuf *image_buffer, + const rcti *updated_region); +/** \brief Mark the whole image to be updated. */ +void BKE_image_partial_update_mark_full_update(struct Image *image); + #ifdef __cplusplus } #endif diff --git a/source/blender/blenkernel/BKE_image_partial_update.hh b/source/blender/blenkernel/BKE_image_partial_update.hh new file mode 100644 index 00000000000..6af44b2c3c9 --- /dev/null +++ b/source/blender/blenkernel/BKE_image_partial_update.hh @@ -0,0 +1,298 @@ +/* + * 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 + * \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. + */ + +#pragma once + +#include "BLI_utildefines.h" + +#include "BLI_rect.h" + +#include "DNA_image_types.h" + +extern "C" { +struct PartialUpdateUser; +struct PartialUpdateRegister; +} + +namespace blender::bke::image { + +using TileNumber = int; + +namespace partial_update { + +/* --- image_partial_update.cc --- */ +/** Image partial updates. */ + +/** + * \brief Result codes of #BKE_image_partial_update_collect_changes. + */ +enum class ePartialUpdateCollectResult { + /** \brief Unable to construct partial updates. Caller should perform a full update. */ + FullUpdateNeeded, + + /** \brief No changes detected since the last time requested. */ + NoChangesDetected, + + /** \brief Changes detected since the last time requested. */ + PartialChangesDetected, +}; + +/** + * \brief A region to update. + * + * Data is organized in tiles. These tiles are in texel space (1 unit is a single texel). When + * tiles are requested they are merged with neighboring tiles. + */ +struct PartialUpdateRegion { + /** \brief region of the image that has been updated. Region can be bigger than actual changes. + */ + struct rcti region; + + /** + * \brief Tile number (UDIM) that this region belongs to. + */ + TileNumber tile_number; +}; + +/** + * \brief Return codes of #BKE_image_partial_update_get_next_change. + */ +enum class ePartialUpdateIterResult { + /** \brief no tiles left when iterating over tiles. */ + Finished = 0, + + /** \brief a chunk was available and has been loaded. */ + ChangeAvailable = 1, +}; + +/** + * \brief collect the partial update since the last request. + * + * Invoke #BKE_image_partial_update_get_next_change to iterate over the collected tiles. + * + * \returns ePartialUpdateCollectResult::FullUpdateNeeded: called should not use partial updates + * but recalculate the full image. This result can be expected when called for the first time for a + * user and when it isn't possible to reconstruct the changes as the internal state doesn't have + * enough data stored. ePartialUpdateCollectResult::NoChangesDetected: The have been no changes + * detected since last invoke for the same user. + * ePartialUpdateCollectResult::PartialChangesDetected: Parts of the image has been updated since + * last invoke for the same user. The changes can be read by using + * #BKE_image_partial_update_get_next_change. + */ +ePartialUpdateCollectResult BKE_image_partial_update_collect_changes( + struct Image *image, struct PartialUpdateUser *user); + +ePartialUpdateIterResult BKE_image_partial_update_get_next_change( + struct PartialUpdateUser *user, struct PartialUpdateRegion *r_region); + +/** \brief Abstract class to load tile data when using the PartialUpdateChecker. */ +class AbstractTileData { + protected: + virtual ~AbstractTileData() = default; + + public: + /** + * \brief Load the data for the given tile_number. + * + * Invoked when changes are on a different tile compared to the previous tile.. + */ + virtual void init_data(TileNumber tile_number) = 0; + /** + * \brief Unload the data that has been loaded. + * + * Invoked when changes are on a different tile compared to the previous tile or when finished + * iterating over the changes. + */ + virtual void free_data() = 0; +}; + +/** + * \brief Class to not load any tile specific data when iterating over changes. + */ +class NoTileData : AbstractTileData { + public: + NoTileData(Image *UNUSED(image), ImageUser *UNUSED(image_user)) + { + } + + void init_data(TileNumber UNUSED(new_tile_number)) override + { + } + + void free_data() override + { + } +}; + +/** + * \brief Load the ImageTile and ImBuf associated with the partial change. + */ +class ImageTileData : AbstractTileData { + public: + /** + * \brief Not owned Image that is being iterated over. + */ + Image *image; + + /** + * \brief Local copy of the image user. + * + * The local copy is required so we don't change the image user of the caller. + * We need to change it in order to request data for a specific tile. + */ + ImageUser image_user = {0}; + + /** + * \brief ImageTile associated with the loaded tile. + * Data is not owned by this instance but by the `image`. + */ + ImageTile *tile = nullptr; + + /** + * \brief ImBuf of the loaded tile. + * + * Can be nullptr when the file doesn't exist or when the tile hasn't been initialized. + */ + ImBuf *tile_buffer = nullptr; + + ImageTileData(Image *image, ImageUser *image_user) : image(image) + { + if (image_user != nullptr) { + this->image_user = *image_user; + } + } + + void init_data(TileNumber new_tile_number) override + { + image_user.tile = new_tile_number; + tile = BKE_image_get_tile(image, new_tile_number); + tile_buffer = BKE_image_acquire_ibuf(image, &image_user, NULL); + } + + void free_data() override + { + BKE_image_release_ibuf(image, tile_buffer, nullptr); + tile = nullptr; + tile_buffer = nullptr; + } +}; + +template<typename TileData = NoTileData> struct PartialUpdateChecker { + + /** + * \brief Not owned Image that is being iterated over. + */ + Image *image; + ImageUser *image_user; + + /** + * \brief the collected changes are stored inside the PartialUpdateUser. + */ + PartialUpdateUser *user; + + struct CollectResult { + PartialUpdateChecker<TileData> *checker; + + /** + * \brief Tile specific data. + */ + TileData tile_data; + PartialUpdateRegion changed_region; + ePartialUpdateCollectResult result_code; + + private: + TileNumber last_tile_number; + + public: + CollectResult(PartialUpdateChecker<TileData> *checker, ePartialUpdateCollectResult result_code) + : checker(checker), + tile_data(checker->image, checker->image_user), + result_code(result_code) + { + } + + const ePartialUpdateCollectResult get_result_code() const + { + return result_code; + } + + /** + * \brief Load the next changed region. + * + * This member function can only be called when partial changes are detected. + * (`get_result_code()` returns `ePartialUpdateCollectResult::PartialChangesDetected`). + * + * When changes for another tile than the previous tile is loaded the #tile_data will be + * updated. + */ + ePartialUpdateIterResult get_next_change() + { + BLI_assert(result_code == ePartialUpdateCollectResult::PartialChangesDetected); + ePartialUpdateIterResult result = BKE_image_partial_update_get_next_change(checker->user, + &changed_region); + switch (result) { + case ePartialUpdateIterResult::Finished: + tile_data.free_data(); + return result; + + case ePartialUpdateIterResult::ChangeAvailable: + if (last_tile_number == changed_region.tile_number) { + return result; + } + tile_data.free_data(); + tile_data.init_data(changed_region.tile_number); + last_tile_number = changed_region.tile_number; + return result; + + default: + BLI_assert_unreachable(); + return result; + } + } + }; + + public: + PartialUpdateChecker(Image *image, ImageUser *image_user, PartialUpdateUser *user) + : image(image), image_user(image_user), user(user) + { + } + + /** + * \brief Check for new changes since the last time this method was invoked for this #user. + */ + CollectResult collect_changes() + { + ePartialUpdateCollectResult collect_result = BKE_image_partial_update_collect_changes(image, + user); + return CollectResult(this, collect_result); + } +}; + +} // namespace partial_update +} // namespace blender::bke::image
\ No newline at end of file diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 6d6579f49f6..220d4673075 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -165,6 +165,7 @@ set(SRC intern/idprop_utils.c intern/idtype.c intern/image.c + intern/image_partial_update.cc intern/image_gen.c intern/image_gpu.cc intern/image_save.c @@ -822,6 +823,7 @@ if(WITH_GTESTS) intern/cryptomatte_test.cc intern/fcurve_test.cc intern/idprop_serialize_test.cc + intern/image_partial_update_test.cc intern/lattice_deform_test.cc intern/layer_test.cc intern/lib_id_remapper_test.cc diff --git a/source/blender/blenkernel/intern/image.c b/source/blender/blenkernel/intern/image.c index c7d58a277e0..040257fe976 100644 --- a/source/blender/blenkernel/intern/image.c +++ b/source/blender/blenkernel/intern/image.c @@ -134,6 +134,22 @@ static void image_runtime_reset_on_copy(struct Image *image) { image->runtime.cache_mutex = MEM_mallocN(sizeof(ThreadMutex), "image runtime cache_mutex"); BLI_mutex_init(image->runtime.cache_mutex); + + image->runtime.partial_update_register = NULL; + image->runtime.partial_update_user = NULL; +} + +static void image_runtime_free_data(struct Image *image) +{ + BLI_mutex_end(image->runtime.cache_mutex); + MEM_freeN(image->runtime.cache_mutex); + image->runtime.cache_mutex = NULL; + + if (image->runtime.partial_update_user != NULL) { + BKE_image_partial_update_free(image->runtime.partial_update_user); + image->runtime.partial_update_user = NULL; + } + BKE_image_partial_update_register_free(image); } static void image_init_data(ID *id) @@ -213,10 +229,8 @@ static void image_free_data(ID *id) BKE_previewimg_free(&image->preview); BLI_freelistN(&image->tiles); - BLI_freelistN(&image->gpu_refresh_areas); - BLI_mutex_end(image->runtime.cache_mutex); - MEM_freeN(image->runtime.cache_mutex); + image_runtime_free_data(image); } static void image_foreach_cache(ID *id, @@ -321,7 +335,8 @@ static void image_blend_write(BlendWriter *writer, ID *id, const void *id_addres ima->cache = NULL; ima->gpuflag = 0; BLI_listbase_clear(&ima->anims); - BLI_listbase_clear(&ima->gpu_refresh_areas); + ima->runtime.partial_update_register = NULL; + ima->runtime.partial_update_user = NULL; for (int i = 0; i < 3; i++) { for (int j = 0; j < 2; j++) { for (int resolution = 0; resolution < IMA_TEXTURE_RESOLUTION_LEN; resolution++) { @@ -401,7 +416,6 @@ static void image_blend_read_data(BlendDataReader *reader, ID *id) ima->lastused = 0; ima->gpuflag = 0; - BLI_listbase_clear(&ima->gpu_refresh_areas); image_runtime_reset(ima); } diff --git a/source/blender/blenkernel/intern/image_gpu.cc b/source/blender/blenkernel/intern/image_gpu.cc index c82de02e52a..91937e709da 100644 --- a/source/blender/blenkernel/intern/image_gpu.cc +++ b/source/blender/blenkernel/intern/image_gpu.cc @@ -38,6 +38,7 @@ #include "BKE_global.h" #include "BKE_image.h" +#include "BKE_image_partial_update.hh" #include "BKE_main.h" #include "GPU_capabilities.h" @@ -46,6 +47,10 @@ #include "PIL_time.h" +using namespace blender::bke::image::partial_update; + +extern "C" { + /* Prototypes. */ static void gpu_free_unused_buffers(); static void image_free_gpu(Image *ima, const bool immediate); @@ -337,6 +342,48 @@ static void image_update_reusable_textures(Image *ima, } } +static void image_gpu_texture_partial_update_changes_available( + Image *image, PartialUpdateChecker<ImageTileData>::CollectResult &changes) +{ + while (changes.get_next_change() == ePartialUpdateIterResult::ChangeAvailable) { + const int tile_offset_x = changes.changed_region.region.xmin; + const int tile_offset_y = changes.changed_region.region.ymin; + const int tile_width = min_ii(changes.tile_data.tile_buffer->x, + BLI_rcti_size_x(&changes.changed_region.region)); + const int tile_height = min_ii(changes.tile_data.tile_buffer->y, + BLI_rcti_size_y(&changes.changed_region.region)); + image_update_gputexture_ex(image, + changes.tile_data.tile, + changes.tile_data.tile_buffer, + tile_offset_x, + tile_offset_y, + tile_width, + tile_height); + } +} + +static void image_gpu_texture_try_partial_update(Image *image, ImageUser *iuser) +{ + PartialUpdateChecker<ImageTileData> checker(image, iuser, image->runtime.partial_update_user); + PartialUpdateChecker<ImageTileData>::CollectResult changes = checker.collect_changes(); + switch (changes.get_result_code()) { + case ePartialUpdateCollectResult::FullUpdateNeeded: { + image_free_gpu(image, true); + break; + } + + case ePartialUpdateCollectResult::PartialChangesDetected: { + image_gpu_texture_partial_update_changes_available(image, changes); + break; + } + + case ePartialUpdateCollectResult::NoChangesDetected: { + /* GPUTextures are up to date. */ + break; + } + } +} + static GPUTexture *image_get_gpu_texture(Image *ima, ImageUser *iuser, ImBuf *ibuf, @@ -370,31 +417,20 @@ static GPUTexture *image_get_gpu_texture(Image *ima, } #undef GPU_FLAGS_TO_CHECK - /* Check if image has been updated and tagged to be updated (full or partial). */ - ImageTile *tile = BKE_image_get_tile(ima, 0); - if (((ima->gpuflag & IMA_GPU_REFRESH) != 0) || - ((ibuf == nullptr || tile == nullptr) && ((ima->gpuflag & IMA_GPU_PARTIAL_REFRESH) != 0))) { - image_free_gpu(ima, true); - BLI_freelistN(&ima->gpu_refresh_areas); - ima->gpuflag &= ~(IMA_GPU_REFRESH | IMA_GPU_PARTIAL_REFRESH); - } - else if (ima->gpuflag & IMA_GPU_PARTIAL_REFRESH) { - BLI_assert(ibuf); - BLI_assert(tile); - ImagePartialRefresh *refresh_area; - while (( - refresh_area = static_cast<ImagePartialRefresh *>(BLI_pophead(&ima->gpu_refresh_areas)))) { - const int tile_offset_x = refresh_area->tile_x * IMA_PARTIAL_REFRESH_TILE_SIZE; - const int tile_offset_y = refresh_area->tile_y * IMA_PARTIAL_REFRESH_TILE_SIZE; - const int tile_width = MIN2(IMA_PARTIAL_REFRESH_TILE_SIZE, ibuf->x - tile_offset_x); - const int tile_height = MIN2(IMA_PARTIAL_REFRESH_TILE_SIZE, ibuf->y - tile_offset_y); - image_update_gputexture_ex( - ima, tile, ibuf, tile_offset_x, tile_offset_y, tile_width, tile_height); - MEM_freeN(refresh_area); - } - ima->gpuflag &= ~IMA_GPU_PARTIAL_REFRESH; + /* TODO(jbakker): We should replace the IMA_GPU_REFRESH flag with a call to + * BKE_image-partial_update_mark_full_update. Although the flag is quicker it leads to double + * administration. */ + if ((ima->gpuflag & IMA_GPU_REFRESH) != 0) { + BKE_image_partial_update_mark_full_update(ima); + ima->gpuflag &= ~IMA_GPU_REFRESH; } + if (ima->runtime.partial_update_user == nullptr) { + ima->runtime.partial_update_user = BKE_image_partial_update_create(ima); + } + + image_gpu_texture_try_partial_update(ima, iuser); + /* Tag as in active use for garbage collector. */ BKE_image_tag_time(ima); @@ -417,6 +453,7 @@ static GPUTexture *image_get_gpu_texture(Image *ima, /* Check if we have a valid image. If not, we return a dummy * texture with zero bind-code so we don't keep trying. */ + ImageTile *tile = BKE_image_get_tile(ima, 0); if (tile == nullptr) { *tex = image_gpu_texture_error_create(textarget); return *tex; @@ -427,8 +464,7 @@ static GPUTexture *image_get_gpu_texture(Image *ima, if (ibuf_intern == nullptr) { ibuf_intern = BKE_image_acquire_ibuf(ima, iuser, nullptr); if (ibuf_intern == nullptr) { - *tex = image_gpu_texture_error_create(textarget); - return *tex; + return image_gpu_texture_error_create(textarget); } } @@ -477,15 +513,14 @@ static GPUTexture *image_get_gpu_texture(Image *ima, break; } - /* if `ibuf` was given, we don't own the `ibuf_intern` */ - if (ibuf == nullptr) { - BKE_image_release_ibuf(ima, ibuf_intern, nullptr); - } - if (*tex) { GPU_texture_orig_size_set(*tex, ibuf_intern->x, ibuf_intern->y); } + if (ibuf != ibuf_intern) { + BKE_image_release_ibuf(ima, ibuf_intern, nullptr); + } + return *tex; } @@ -903,87 +938,29 @@ static void image_update_gputexture_ex( void BKE_image_update_gputexture(Image *ima, ImageUser *iuser, int x, int y, int w, int h) { + ImageTile *image_tile = BKE_image_get_tile_from_iuser(ima, iuser); ImBuf *ibuf = BKE_image_acquire_ibuf(ima, iuser, nullptr); - ImageTile *tile = BKE_image_get_tile_from_iuser(ima, iuser); - - if ((ibuf == nullptr) || (w == 0) || (h == 0)) { - /* Full reload of texture. */ - BKE_image_free_gputextures(ima); - } - image_update_gputexture_ex(ima, tile, ibuf, x, y, w, h); + BKE_image_update_gputexture_delayed(ima, image_tile, ibuf, x, y, w, h); BKE_image_release_ibuf(ima, ibuf, nullptr); } -void BKE_image_update_gputexture_delayed( - struct Image *ima, struct ImBuf *ibuf, int x, int y, int w, int h) +void BKE_image_update_gputexture_delayed(struct Image *ima, + struct ImageTile *image_tile, + struct ImBuf *ibuf, + int x, + int y, + int w, + int h) { /* Check for full refresh. */ - if (ibuf && x == 0 && y == 0 && w == ibuf->x && h == ibuf->y) { - ima->gpuflag |= IMA_GPU_REFRESH; - } - /* Check if we can promote partial refresh to a full refresh. */ - if ((ima->gpuflag & (IMA_GPU_REFRESH | IMA_GPU_PARTIAL_REFRESH)) == - (IMA_GPU_REFRESH | IMA_GPU_PARTIAL_REFRESH)) { - ima->gpuflag &= ~IMA_GPU_PARTIAL_REFRESH; - BLI_freelistN(&ima->gpu_refresh_areas); - } - /* Image is already marked for complete refresh. */ - if (ima->gpuflag & IMA_GPU_REFRESH) { - return; - } - - /* Schedule the tiles that covers the requested area. */ - const int start_tile_x = x / IMA_PARTIAL_REFRESH_TILE_SIZE; - const int start_tile_y = y / IMA_PARTIAL_REFRESH_TILE_SIZE; - const int end_tile_x = (x + w) / IMA_PARTIAL_REFRESH_TILE_SIZE; - const int end_tile_y = (y + h) / IMA_PARTIAL_REFRESH_TILE_SIZE; - const int num_tiles_x = (end_tile_x + 1) - (start_tile_x); - const int num_tiles_y = (end_tile_y + 1) - (start_tile_y); - const int num_tiles = num_tiles_x * num_tiles_y; - const bool allocate_on_heap = BLI_BITMAP_SIZE(num_tiles) > 16; - BLI_bitmap *requested_tiles = nullptr; - if (allocate_on_heap) { - requested_tiles = BLI_BITMAP_NEW(num_tiles, __func__); + if (ibuf != nullptr && ima->source != IMA_SRC_TILED && x == 0 && y == 0 && w == ibuf->x && + h == ibuf->y) { + BKE_image_partial_update_mark_full_update(ima); } else { - requested_tiles = BLI_BITMAP_NEW_ALLOCA(num_tiles); - } - - /* Mark the tiles that have already been requested. They don't need to be requested again. */ - int num_tiles_not_scheduled = num_tiles; - LISTBASE_FOREACH (ImagePartialRefresh *, area, &ima->gpu_refresh_areas) { - if (area->tile_x < start_tile_x || area->tile_x > end_tile_x || area->tile_y < start_tile_y || - area->tile_y > end_tile_y) { - continue; - } - int requested_tile_index = (area->tile_x - start_tile_x) + - (area->tile_y - start_tile_y) * num_tiles_x; - BLI_BITMAP_ENABLE(requested_tiles, requested_tile_index); - num_tiles_not_scheduled--; - if (num_tiles_not_scheduled == 0) { - break; - } - } - - /* Schedule the tiles that aren't requested yet. */ - if (num_tiles_not_scheduled) { - int tile_index = 0; - for (int tile_y = start_tile_y; tile_y <= end_tile_y; tile_y++) { - for (int tile_x = start_tile_x; tile_x <= end_tile_x; tile_x++) { - if (!BLI_BITMAP_TEST_BOOL(requested_tiles, tile_index)) { - ImagePartialRefresh *area = static_cast<ImagePartialRefresh *>( - MEM_mallocN(sizeof(ImagePartialRefresh), __func__)); - area->tile_x = tile_x; - area->tile_y = tile_y; - BLI_addtail(&ima->gpu_refresh_areas, area); - } - tile_index++; - } - } - ima->gpuflag |= IMA_GPU_PARTIAL_REFRESH; - } - if (allocate_on_heap) { - MEM_freeN(requested_tiles); + rcti dirty_region; + BLI_rcti_init(&dirty_region, x, x + w, y, y + h); + BKE_image_partial_update_mark_region(ima, image_tile, ibuf, &dirty_region); } } @@ -1016,3 +993,4 @@ void BKE_image_paint_set_mipmap(Main *bmain, bool mipmap) } /** \} */ +} 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(); +} +} diff --git a/source/blender/blenkernel/intern/image_partial_update_test.cc b/source/blender/blenkernel/intern/image_partial_update_test.cc new file mode 100644 index 00000000000..70aa51f7c98 --- /dev/null +++ b/source/blender/blenkernel/intern/image_partial_update_test.cc @@ -0,0 +1,393 @@ +/* + * 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. + * + * The Original Code is Copyright (C) 2020 by Blender Foundation. + */ +#include "testing/testing.h" + +#include "CLG_log.h" + +#include "BKE_appdir.h" +#include "BKE_idtype.h" +#include "BKE_image.h" +#include "BKE_image_partial_update.hh" +#include "BKE_main.h" + +#include "IMB_imbuf.h" +#include "IMB_moviecache.h" + +#include "DNA_image_types.h" + +#include "MEM_guardedalloc.h" + +namespace blender::bke::image::partial_update { + +constexpr float black_color[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + +class ImagePartialUpdateTest : public testing::Test { + protected: + Main *bmain; + Image *image; + ImageTile *image_tile; + ImageUser image_user = {nullptr}; + ImBuf *image_buffer; + PartialUpdateUser *partial_update_user; + + private: + Image *create_test_image(int width, int height) + { + return BKE_image_add_generated(bmain, + width, + height, + "Test Image", + 32, + true, + IMA_GENTYPE_BLANK, + black_color, + false, + false, + false); + } + + protected: + void SetUp() override + { + CLG_init(); + BKE_idtype_init(); + BKE_appdir_init(); + IMB_init(); + + bmain = BKE_main_new(); + /* Creating an image generates a mem-leak during tests. */ + image = create_test_image(1024, 1024); + image_tile = BKE_image_get_tile(image, 0); + image_buffer = BKE_image_acquire_ibuf(image, nullptr, nullptr); + + partial_update_user = BKE_image_partial_update_create(image); + } + + void TearDown() override + { + BKE_image_release_ibuf(image, image_buffer, nullptr); + BKE_image_partial_update_free(partial_update_user); + BKE_main_free(bmain); + + IMB_moviecache_destruct(); + IMB_exit(); + BKE_appdir_exit(); + CLG_exit(); + } +}; + +TEST_F(ImagePartialUpdateTest, mark_full_update) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark full update */ + BKE_image_partial_update_mark_full_update(image); + + /* Validate need full update followed by no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); +} + +TEST_F(ImagePartialUpdateTest, mark_single_tile) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region; + BLI_rcti_init(®ion, 10, 20, 40, 50); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + + /* Partial Update should be available. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + PartialUpdateRegion changed_region; + ePartialUpdateIterResult iter_result; + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::ChangeAvailable); + EXPECT_EQ(BLI_rcti_inside_rcti(&changed_region.region, ®ion), true); + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::Finished); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); +} + +TEST_F(ImagePartialUpdateTest, mark_unconnected_tiles) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region_a; + BLI_rcti_init(®ion_a, 10, 20, 40, 50); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion_a); + rcti region_b; + BLI_rcti_init(®ion_b, 710, 720, 740, 750); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion_b); + + /* Partial Update should be available. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + PartialUpdateRegion changed_region; + ePartialUpdateIterResult iter_result; + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::ChangeAvailable); + EXPECT_EQ(BLI_rcti_inside_rcti(&changed_region.region, ®ion_b), true); + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::ChangeAvailable); + EXPECT_EQ(BLI_rcti_inside_rcti(&changed_region.region, ®ion_a), true); + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::Finished); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); +} + +TEST_F(ImagePartialUpdateTest, donot_mark_outside_image) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region; + /* Axis. */ + BLI_rcti_init(®ion, -100, 0, 50, 100); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, 1024, 1100, 50, 100); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, 50, 100, -100, 0); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, 50, 100, 1024, 1100); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Diagonals. */ + BLI_rcti_init(®ion, -100, 0, -100, 0); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, -100, 0, 1024, 1100); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, 1024, 1100, -100, 0); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + BLI_rcti_init(®ion, 1024, 1100, 1024, 1100); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); +} + +TEST_F(ImagePartialUpdateTest, mark_inside_image) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region; + BLI_rcti_init(®ion, 0, 1, 0, 1); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + BLI_rcti_init(®ion, 1023, 1024, 0, 1); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + BLI_rcti_init(®ion, 1023, 1024, 1023, 1024); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + BLI_rcti_init(®ion, 1023, 1024, 0, 1); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); +} + +TEST_F(ImagePartialUpdateTest, sequential_mark_region) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + { + /* Mark region. */ + rcti region; + BLI_rcti_init(®ion, 10, 20, 40, 50); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + + /* Partial Update should be available. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + PartialUpdateRegion changed_region; + ePartialUpdateIterResult iter_result; + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::ChangeAvailable); + EXPECT_EQ(BLI_rcti_inside_rcti(&changed_region.region, ®ion), true); + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::Finished); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + } + + { + /* Mark different region. */ + rcti region; + BLI_rcti_init(®ion, 710, 720, 740, 750); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + + /* Partial Update should be available. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + PartialUpdateRegion changed_region; + ePartialUpdateIterResult iter_result; + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::ChangeAvailable); + EXPECT_EQ(BLI_rcti_inside_rcti(&changed_region.region, ®ion), true); + iter_result = BKE_image_partial_update_get_next_change(partial_update_user, &changed_region); + EXPECT_EQ(iter_result, ePartialUpdateIterResult::Finished); + + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + } +} + +TEST_F(ImagePartialUpdateTest, mark_multiple_chunks) +{ + ePartialUpdateCollectResult result; + /* First tile should always return a full update. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region; + BLI_rcti_init(®ion, 300, 700, 300, 700); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + + /* Partial Update should be available. */ + result = BKE_image_partial_update_collect_changes(image, partial_update_user); + EXPECT_EQ(result, ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + PartialUpdateRegion changed_region; + int num_chunks_found = 0; + while (BKE_image_partial_update_get_next_change(partial_update_user, &changed_region) == + ePartialUpdateIterResult::ChangeAvailable) { + BLI_rcti_isect(&changed_region.region, ®ion, nullptr); + num_chunks_found++; + } + EXPECT_EQ(num_chunks_found, 4); +} + +TEST_F(ImagePartialUpdateTest, iterator) +{ + PartialUpdateChecker<NoTileData> checker(image, &image_user, partial_update_user); + /* First tile should always return a full update. */ + PartialUpdateChecker<NoTileData>::CollectResult changes = checker.collect_changes(); + EXPECT_EQ(changes.get_result_code(), ePartialUpdateCollectResult::FullUpdateNeeded); + /* Second invoke should now detect no changes. */ + changes = checker.collect_changes(); + EXPECT_EQ(changes.get_result_code(), ePartialUpdateCollectResult::NoChangesDetected); + + /* Mark region. */ + rcti region; + BLI_rcti_init(®ion, 300, 700, 300, 700); + BKE_image_partial_update_mark_region(image, image_tile, image_buffer, ®ion); + + /* Partial Update should be available. */ + changes = checker.collect_changes(); + EXPECT_EQ(changes.get_result_code(), ePartialUpdateCollectResult::PartialChangesDetected); + + /* Check tiles. */ + int num_tiles_found = 0; + while (changes.get_next_change() == ePartialUpdateIterResult::ChangeAvailable) { + BLI_rcti_isect(&changes.changed_region.region, ®ion, nullptr); + num_tiles_found++; + } + EXPECT_EQ(num_tiles_found, 4); +} + +} // namespace blender::bke::image::partial_update |