tornavis/source/blender/blenkernel/intern/image_partial_update.cc

593 lines
18 KiB
C++

/* SPDX-FileCopyrightText: 2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/**
* \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 use case is to update #GPUTexture for drawing where only that part is uploaded that
* only changed.
*
* Usage:
*
* \code{.cc}
* 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);
* \endcode
*/
#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_listbase.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 PartialUpdateRegisterImpl;
struct PartialUpdateUserImpl;
/**
* Wrap PartialUpdateUserImpl to its C-struct (PartialUpdateUser).
*/
static PartialUpdateUser *wrap(PartialUpdateUserImpl *user)
{
return static_cast<PartialUpdateUser *>(static_cast<void *>(user));
}
/**
* Unwrap the PartialUpdateUser C-struct to its CPP counterpart (PartialUpdateUserImpl).
*/
static PartialUpdateUserImpl *unwrap(PartialUpdateUser *user)
{
return static_cast<PartialUpdateUserImpl *>(static_cast<void *>(user));
}
/**
* Wrap PartialUpdateRegisterImpl to its C-struct (PartialUpdateRegister).
*/
static PartialUpdateRegister *wrap(PartialUpdateRegisterImpl *partial_update_register)
{
return static_cast<PartialUpdateRegister *>(static_cast<void *>(partial_update_register));
}
/**
* Unwrap the PartialUpdateRegister C-struct to its CPP counterpart (PartialUpdateRegisterImpl).
*/
static PartialUpdateRegisterImpl *unwrap(PartialUpdateRegister *partial_update_register)
{
return static_cast<PartialUpdateRegisterImpl *>(static_cast<void *>(partial_update_register));
}
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 - 1) / CHUNK_SIZE;
int chunk_y_len = (tile_height + CHUNK_SIZE - 1) / 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-initialize 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)
{
if (changeset_id < first_changeset_id) {
return false;
}
if (changeset_id > last_changeset_id) {
return false;
}
return true;
}
/**
* \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. */
PartialUpdateUser *BKE_image_partial_update_create(const 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();
}
}