Image: Partial Update Redesign.

This patch reimplements the image partial updates. Biggest design motivation for the redesign
is that currently GPUTextures must be owned by the image. This reduces flexibility and adds
complexity to a single component especially when we want to have different structures.

The new design is not limited to GPUTextures and can also be used by reducing overhead in image
operations like scaling. Or partial image updating in Cycles.

The usecase in hand is that we want to support virtual images in the image editor so we can
work with images that don't fit in a single GPUTexture.

Using `BKE_image_partial_update_mark_region` or `BKE_image_partial_update_mark_full_update`
a part of an image can be marked as dirty. These regions are stored per ImageTile (UDIM).

When a part of the code wants to receive partial changes it needs to construct a `PartialUpdateUser`
by calling `BKE_image_partial_update_create`. As long as this instance is kept alive the changes can
be received.

When a user wants to update its own data it will call `BKE_image_partial_update_collect_changes`
This will collect the changes since the last time the user called this function. When the partial changes
are available the partial change can be read by calling `BKE_image_partial_update_get_next_change`

It can happen that the introduced mechanism doesn't have the data anymore to construct the
changes since the last time a PartialUpdateUser requested it. In this case it will get a request
to perform a full update.

Maniphest Tasks: T92613

Differential Revision: https://developer.blender.org/D13238
This commit is contained in:
Jeroen Bakker 2022-01-28 08:05:31 +01:00
parent 1e0758333d
commit 0a32ac02e9
Notes: blender-bot 2023-02-14 06:05:22 +01:00
Referenced by issue #96880, Regression: 'Viewport Render Animation' stuck and crashes
Referenced by issue #95298, Regression: Multi-view images fail to display properly in the Image Editor
Referenced by issue #92613, GPUTexture partial update
9 changed files with 1449 additions and 118 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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);
/* 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;
}
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;
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)
}
/** \} */
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -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;

View File

@ -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 */