Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'source/blender')
-rw-r--r--source/blender/blenkernel/BKE_image.h40
-rw-r--r--source/blender/blenkernel/BKE_image_partial_update.hh298
-rw-r--r--source/blender/blenkernel/CMakeLists.txt2
-rw-r--r--source/blender/blenkernel/intern/image.c24
-rw-r--r--source/blender/blenkernel/intern/image_gpu.cc184
-rw-r--r--source/blender/blenkernel/intern/image_partial_update.cc598
-rw-r--r--source/blender/blenkernel/intern/image_partial_update_test.cc393
-rw-r--r--source/blender/editors/render/render_internal.cc10
-rw-r--r--source/blender/makesdna/DNA_image_types.h20
9 files changed, 1450 insertions, 119 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(&region.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(&region, 10, 20, 40, 50);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+
+ /* 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, &region), 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(&region_a, 10, 20, 40, 50);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region_a);
+ rcti region_b;
+ BLI_rcti_init(&region_b, 710, 720, 740, 750);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region_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, &region_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, &region_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(&region, -100, 0, 50, 100);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, 1024, 1100, 50, 100);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, 50, 100, -100, 0);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, 50, 100, 1024, 1100);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ /* Diagonals. */
+ BLI_rcti_init(&region, -100, 0, -100, 0);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, -100, 0, 1024, 1100);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, 1024, 1100, -100, 0);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ result = BKE_image_partial_update_collect_changes(image, partial_update_user);
+ EXPECT_EQ(result, ePartialUpdateCollectResult::NoChangesDetected);
+
+ BLI_rcti_init(&region, 1024, 1100, 1024, 1100);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ 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(&region, 0, 1, 0, 1);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ 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(&region, 1023, 1024, 0, 1);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ 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(&region, 1023, 1024, 1023, 1024);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ 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(&region, 1023, 1024, 0, 1);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+ 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(&region, 10, 20, 40, 50);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+
+ /* 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, &region), 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(&region, 710, 720, 740, 750);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+
+ /* 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, &region), 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(&region, 300, 700, 300, 700);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+
+ /* 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, &region, 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(&region, 300, 700, 300, 700);
+ BKE_image_partial_update_mark_region(image, image_tile, image_buffer, &region);
+
+ /* 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, &region, nullptr);
+ num_tiles_found++;
+ }
+ EXPECT_EQ(num_tiles_found, 4);
+}
+
+} // namespace blender::bke::image::partial_update
diff --git a/source/blender/editors/render/render_internal.cc b/source/blender/editors/render/render_internal.cc
index a156b9234dc..8e9a052381c 100644
--- a/source/blender/editors/render/render_internal.cc
+++ b/source/blender/editors/render/render_internal.cc
@@ -616,8 +616,14 @@ static void image_rect_update(void *rjv, RenderResult *rr, volatile rcti *renrec
ED_draw_imbuf_method(ibuf) != IMAGE_DRAW_METHOD_GLSL) {
image_buffer_rect_update(rj, rr, ibuf, &rj->iuser, &tile_rect, offset_x, offset_y, viewname);
}
- BKE_image_update_gputexture_delayed(
- ima, ibuf, offset_x, offset_y, BLI_rcti_size_x(&tile_rect), BLI_rcti_size_y(&tile_rect));
+ ImageTile *image_tile = BKE_image_get_tile(ima, 0);
+ BKE_image_update_gputexture_delayed(ima,
+ image_tile,
+ ibuf,
+ offset_x,
+ offset_y,
+ BLI_rcti_size_x(&tile_rect),
+ BLI_rcti_size_y(&tile_rect));
/* make jobs timer to send notifier */
*(rj->do_update) = true;
diff --git a/source/blender/makesdna/DNA_image_types.h b/source/blender/makesdna/DNA_image_types.h
index 64c8fd3e3a9..7a789227128 100644
--- a/source/blender/makesdna/DNA_image_types.h
+++ b/source/blender/makesdna/DNA_image_types.h
@@ -142,10 +142,20 @@ typedef enum eImageTextureResolution {
IMA_TEXTURE_RESOLUTION_LEN
} eImageTextureResolution;
+/* Defined in BKE_image.h. */
+struct PartialUpdateRegister;
+struct PartialUpdateUser;
+
typedef struct Image_Runtime {
/* Mutex used to guarantee thread-safe access to the cached ImBuf of the corresponding image ID.
*/
void *cache_mutex;
+
+ /** \brief Register containing partial updates. */
+ struct PartialUpdateRegister *partial_update_register;
+ /** \brief Partial update user for GPUTextures stored inside the Image. */
+ struct PartialUpdateUser *partial_update_user;
+
} Image_Runtime;
typedef struct Image {
@@ -171,8 +181,6 @@ typedef struct Image {
int lastframe;
/* GPU texture flag. */
- /* Contains `ImagePartialRefresh`. */
- ListBase gpu_refresh_areas;
int gpuframenr;
short gpuflag;
short gpu_pass;
@@ -247,15 +255,13 @@ enum {
enum {
/** GPU texture needs to be refreshed. */
IMA_GPU_REFRESH = (1 << 0),
- /** GPU texture needs to be partially refreshed. */
- IMA_GPU_PARTIAL_REFRESH = (1 << 1),
/** All mipmap levels in OpenGL texture set? */
- IMA_GPU_MIPMAP_COMPLETE = (1 << 2),
+ IMA_GPU_MIPMAP_COMPLETE = (1 << 1),
/* Reuse the max resolution textures as they fit in the limited scale. */
- IMA_GPU_REUSE_MAX_RESOLUTION = (1 << 3),
+ IMA_GPU_REUSE_MAX_RESOLUTION = (1 << 2),
/* Has any limited scale textures been allocated.
* Adds additional checks to reuse max resolution images when they fit inside limited scale. */
- IMA_GPU_HAS_LIMITED_SCALE_TEXTURES = (1 << 4),
+ IMA_GPU_HAS_LIMITED_SCALE_TEXTURES = (1 << 3),
};
/* Image.source, where the image comes from */