The single byte version of hash_data was casting from unsigned char instead of signed. This didn't cause any errors since the result of each aren't compared. Even so, better keep them matching.
1806 lines
52 KiB
C
1806 lines
52 KiB
C
/*
|
|
* ***** BEGIN GPL LICENSE BLOCK *****
|
|
*
|
|
* 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.
|
|
*
|
|
* ***** END GPL LICENSE BLOCK *****
|
|
*/
|
|
|
|
/** \file blender/blenlib/intern/array_store.c
|
|
* \ingroup bli
|
|
* \brief Array storage to minimize duplication.
|
|
*
|
|
* This is done by splitting arrays into chunks and using copy-on-write (COW),
|
|
* to de-duplicate chunks,
|
|
* from the users perspective this is an implementation detail.
|
|
*
|
|
*
|
|
* Overview
|
|
* ========
|
|
*
|
|
*
|
|
* Data Structure
|
|
* --------------
|
|
*
|
|
* This diagram is an overview of the structure of a single array-store.
|
|
*
|
|
* \note The only 2 structures here which are referenced externally are the.
|
|
*
|
|
* - BArrayStore: The whole array store.
|
|
* - BArrayState: Represents a single state (array) of data.
|
|
* These can be add using a reference state, while this could be considered the previous or parent state.
|
|
* no relationship is kept, so the caller is free to add any state from the same BArrayStore as a reference.
|
|
*
|
|
* <pre>
|
|
* <+> BArrayStore: root data-structure,
|
|
* | can store many 'states', which share memory.
|
|
* |
|
|
* | This can store many arrays, however they must share the same 'stride'.
|
|
* | Arrays of different types will need to use a new BArrayStore.
|
|
* |
|
|
* +- <+> states (Collection of BArrayState's):
|
|
* | | Each represents an array added by the user of this API.
|
|
* | | and references a chunk_list (each state is a chunk_list user).
|
|
* | | Note that the list order has no significance.
|
|
* | |
|
|
* | +- <+> chunk_list (BChunkList):
|
|
* | | The chunks that make up this state.
|
|
* | | Each state is a chunk_list user,
|
|
* | | avoids duplicating lists when there is no change between states.
|
|
* | |
|
|
* | +- chunk_refs (List of BChunkRef): Each chunk_ref links to a a BChunk.
|
|
* | Each reference is a chunk user,
|
|
* | avoids duplicating smaller chunks of memory found in multiple states.
|
|
* |
|
|
* +- info (BArrayInfo):
|
|
* | Sizes and offsets for this array-store.
|
|
* | Also caches some variables for reuse.
|
|
* |
|
|
* +- <+> memory (BArrayMemory):
|
|
* | Memory pools for storing BArrayStore data.
|
|
* |
|
|
* +- chunk_list (Pool of BChunkList):
|
|
* | All chunk_lists, (reference counted, used by BArrayState).
|
|
* |
|
|
* +- chunk_ref (Pool of BChunkRef):
|
|
* | All chunk_refs (link between BChunkList & BChunk).
|
|
* |
|
|
* +- chunks (Pool of BChunk):
|
|
* All chunks, (reference counted, used by BChunkList).
|
|
* These have their headers hashed for reuse so we can quickly check for duplicates.
|
|
* </pre>
|
|
*
|
|
*
|
|
* De-Duplication
|
|
* --------------
|
|
*
|
|
* When creating a new state, a previous state can be given as a reference,
|
|
* matching chunks from this state are re-used in the new state.
|
|
*
|
|
* First matches at either end of the array are detected.
|
|
* For identical arrays this is all thats needed.
|
|
*
|
|
* De-duplication is performed on any remaining chunks, by hashing the first few bytes of the chunk
|
|
* (see: BCHUNK_HASH_TABLE_ACCUMULATE_STEPS).
|
|
*
|
|
* \note This is cached for reuse since the referenced data never changes.
|
|
*
|
|
* An array is created to store hash values at every 'stride',
|
|
* then stepped over to search for matching chunks.
|
|
*
|
|
* Once a match is found, there is a high chance next chunks match too,
|
|
* so this is checked to avoid performing so many hash-lookups.
|
|
* Otherwise new chunks are created.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_mempool.h"
|
|
|
|
#include "BLI_strict_flags.h"
|
|
|
|
#include "BLI_array_store.h" /* own include */
|
|
|
|
/* only for BLI_array_store_is_valid */
|
|
#include "BLI_ghash.h"
|
|
|
|
/** \name Defines
|
|
*
|
|
* Some of the logic for merging is quite involved,
|
|
* support disabling some parts of this.
|
|
* \{ */
|
|
|
|
/* Scan first chunks (happy path when beginning of the array matches).
|
|
* When the array is a perfect match, we can re-use the entire list.
|
|
*
|
|
* Note that disabling makes some tests fail that check for output-size.
|
|
*/
|
|
#define USE_FASTPATH_CHUNKS_FIRST
|
|
|
|
/* Scan last chunks (happy path when end of the array matches).
|
|
* When the end of the array matches, we can quickly add these chunks.
|
|
* note that we will add contiguous matching chunks
|
|
* so this isn't as useful as USE_FASTPATH_CHUNKS_FIRST,
|
|
* however it avoids adding matching chunks into the lookup table,
|
|
* so creating the lookup table won't be as expensive.
|
|
*/
|
|
#ifdef USE_FASTPATH_CHUNKS_FIRST
|
|
# define USE_FASTPATH_CHUNKS_LAST
|
|
#endif
|
|
|
|
/* For arrays of matching length, test that *enough* of the chunks are aligned,
|
|
* and simply step over both arrays, using matching chunks.
|
|
* This avoids overhead of using a lookup table for cases when we can assume they're mostly aligned.
|
|
*/
|
|
#define USE_ALIGN_CHUNKS_TEST
|
|
|
|
/* Accumulate hashes from right to left so we can create a hash for the chunk-start.
|
|
* This serves to increase uniqueness and will help when there is many values which are the same.
|
|
*/
|
|
#define USE_HASH_TABLE_ACCUMULATE
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
/* Number of times to propagate hashes back.
|
|
* Effectively a 'triangle-number'.
|
|
* so 4 -> 7, 5 -> 10, 6 -> 15... etc.
|
|
*/
|
|
# define BCHUNK_HASH_TABLE_ACCUMULATE_STEPS 4
|
|
#else
|
|
/* How many items to hash (multiplied by stride)
|
|
*/
|
|
# define BCHUNK_HASH_LEN 4
|
|
#endif
|
|
|
|
/* Calculate the key once and reuse it
|
|
*/
|
|
#define USE_HASH_TABLE_KEY_CACHE
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
# define HASH_TABLE_KEY_UNSET ((uint64_t)-1)
|
|
# define HASH_TABLE_KEY_FALLBACK ((uint64_t)-2)
|
|
#endif
|
|
|
|
/* How much larger the table is then the total number of chunks.
|
|
*/
|
|
#define BCHUNK_HASH_TABLE_MUL 3
|
|
|
|
/* Merge too small/large chunks:
|
|
*
|
|
* Using this means chunks below a threshold will be merged together.
|
|
* Even though short term this uses more memory,
|
|
* long term the overhead of maintaining many small chunks is reduced.
|
|
* This is defined by setting the minimum chunk size (as a fraction of the regular chunk size).
|
|
*
|
|
* Chunks may also become too large (when incrementally growing an array),
|
|
* this also enables chunk splitting.
|
|
*/
|
|
#define USE_MERGE_CHUNKS
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
/* Merge chunks smaller then: (chunk_size / BCHUNK_MIN_SIZE_DIV)
|
|
*/
|
|
# define BCHUNK_SIZE_MIN_DIV 8
|
|
|
|
/* Disallow chunks bigger then the regular chunk size scaled by this value
|
|
* note: must be at least 2!
|
|
* however, this code runs wont run in tests unless its ~1.1 ugh.
|
|
* so lower only to check splitting works.
|
|
*/
|
|
# define BCHUNK_SIZE_MAX_MUL 2
|
|
#endif /* USE_MERGE_CHUNKS */
|
|
|
|
/* slow (keep disabled), but handy for debugging */
|
|
// #define USE_VALIDATE_LIST_SIZE
|
|
|
|
// #define USE_VALIDATE_LIST_DATA_PARTIAL
|
|
|
|
// #define USE_PARANOID_CHECKS
|
|
|
|
/** \} */
|
|
|
|
|
|
/** \name Internal Structs
|
|
* \{ */
|
|
|
|
typedef uint64_t hash_key;
|
|
|
|
|
|
typedef struct BArrayInfo {
|
|
size_t chunk_stride;
|
|
// uint chunk_count; /* UNUSED (other values are derived from this) */
|
|
|
|
/* pre-calculated */
|
|
size_t chunk_byte_size;
|
|
/* min/max limits (inclusive) */
|
|
size_t chunk_byte_size_min;
|
|
size_t chunk_byte_size_max;
|
|
|
|
size_t accum_read_ahead_bytes;
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
size_t accum_steps;
|
|
size_t accum_read_ahead_len;
|
|
#endif
|
|
} BArrayInfo;
|
|
|
|
typedef struct BArrayMemory {
|
|
BLI_mempool *chunk_list; /* BChunkList */
|
|
BLI_mempool *chunk_ref; /* BChunkRef */
|
|
BLI_mempool *chunk; /* BChunk */
|
|
} BArrayMemory;
|
|
|
|
/**
|
|
* Main storage for all states
|
|
*/
|
|
struct BArrayStore {
|
|
/* static */
|
|
BArrayInfo info;
|
|
|
|
/* memory storage */
|
|
BArrayMemory memory;
|
|
|
|
/**
|
|
* #BArrayState may be in any order (logic should never depend on state order).
|
|
*/
|
|
ListBase states;
|
|
};
|
|
|
|
/**
|
|
* A single instance of an array.
|
|
*
|
|
* This is how external API's hold a reference to an in-memory state,
|
|
* although the struct is private.
|
|
*
|
|
* \note Currently each 'state' is allocated separately.
|
|
* While this could be moved to a memory pool,
|
|
* it makes it easier to trace invalid usage, so leave as-is for now.
|
|
*/
|
|
struct BArrayState {
|
|
/** linked list in #BArrayStore.states */
|
|
struct BArrayState *next, *prev;
|
|
|
|
struct BChunkList *chunk_list; /* BChunkList's */
|
|
|
|
};
|
|
|
|
typedef struct BChunkList {
|
|
ListBase chunk_refs; /* BChunkRef's */
|
|
uint chunk_refs_len; /* BLI_listbase_count(chunks), store for reuse. */
|
|
size_t total_size; /* size of all chunks */
|
|
|
|
/** number of #BArrayState using this. */
|
|
int users;
|
|
} BChunkList;
|
|
|
|
/* a chunk of an array */
|
|
typedef struct BChunk {
|
|
const uchar *data;
|
|
size_t data_len;
|
|
/** number of #BChunkList using this. */
|
|
int users;
|
|
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
hash_key key;
|
|
#endif
|
|
} BChunk;
|
|
|
|
/**
|
|
* Links to store #BChunk data in #BChunkList.chunks.
|
|
*/
|
|
typedef struct BChunkRef {
|
|
struct BChunkRef *next, *prev;
|
|
BChunk *link;
|
|
} BChunkRef;
|
|
|
|
/**
|
|
* Single linked list used when putting chunks into a temporary table,
|
|
* used for lookups.
|
|
*
|
|
* Point to the #BChunkRef, not the #BChunk,
|
|
* to allow talking down the chunks in-order until a mis-match is found,
|
|
* this avoids having to do so many table lookups.
|
|
*/
|
|
typedef struct BTableRef {
|
|
struct BTableRef *next;
|
|
const BChunkRef *cref;
|
|
} BTableRef;
|
|
|
|
/** \} */
|
|
|
|
|
|
static size_t bchunk_list_size(const BChunkList *chunk_list);
|
|
|
|
|
|
/** \name Internal BChunk API
|
|
* \{ */
|
|
|
|
static BChunk *bchunk_new(
|
|
BArrayMemory *bs_mem, const uchar *data, const size_t data_len)
|
|
{
|
|
BChunk *chunk = BLI_mempool_alloc(bs_mem->chunk);
|
|
chunk->data = data;
|
|
chunk->data_len = data_len;
|
|
chunk->users = 0;
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
chunk->key = HASH_TABLE_KEY_UNSET;
|
|
#endif
|
|
return chunk;
|
|
}
|
|
|
|
static BChunk *bchunk_new_copydata(
|
|
BArrayMemory *bs_mem, const uchar *data, const size_t data_len)
|
|
{
|
|
uchar *data_copy = MEM_mallocN(data_len, __func__);
|
|
memcpy(data_copy, data, data_len);
|
|
return bchunk_new(bs_mem, data_copy, data_len);
|
|
}
|
|
|
|
static void bchunk_decref(
|
|
BArrayMemory *bs_mem, BChunk *chunk)
|
|
{
|
|
BLI_assert(chunk->users > 0);
|
|
if (chunk->users == 1) {
|
|
MEM_freeN((void *)chunk->data);
|
|
BLI_mempool_free(bs_mem->chunk, chunk);
|
|
}
|
|
else {
|
|
chunk->users -= 1;
|
|
}
|
|
}
|
|
|
|
static bool bchunk_data_compare(
|
|
const BChunk *chunk,
|
|
const uchar *data_base, const size_t data_base_len,
|
|
const size_t offset)
|
|
{
|
|
if (offset + (size_t)chunk->data_len <= data_base_len) {
|
|
return (memcmp(&data_base[offset], chunk->data, chunk->data_len) == 0);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** \} */
|
|
|
|
|
|
/** \name Internal BChunkList API
|
|
* \{ */
|
|
|
|
static BChunkList *bchunk_list_new(
|
|
BArrayMemory *bs_mem, size_t total_size)
|
|
{
|
|
BChunkList *chunk_list = BLI_mempool_alloc(bs_mem->chunk_list);
|
|
|
|
BLI_listbase_clear(&chunk_list->chunk_refs);
|
|
chunk_list->chunk_refs_len = 0;
|
|
chunk_list->total_size = total_size;
|
|
chunk_list->users = 0;
|
|
return chunk_list;
|
|
}
|
|
|
|
static void bchunk_list_decref(
|
|
BArrayMemory *bs_mem, BChunkList *chunk_list)
|
|
{
|
|
BLI_assert(chunk_list->users > 0);
|
|
if (chunk_list->users == 1) {
|
|
for (BChunkRef *cref = chunk_list->chunk_refs.first, *cref_next; cref; cref = cref_next) {
|
|
cref_next = cref->next;
|
|
bchunk_decref(bs_mem, cref->link);
|
|
BLI_mempool_free(bs_mem->chunk_ref, cref);
|
|
}
|
|
|
|
BLI_mempool_free(bs_mem->chunk_list, chunk_list);
|
|
}
|
|
else {
|
|
chunk_list->users -= 1;
|
|
}
|
|
}
|
|
|
|
#ifdef USE_VALIDATE_LIST_SIZE
|
|
# ifndef NDEBUG
|
|
# define ASSERT_CHUNKLIST_SIZE(chunk_list, n) BLI_assert(bchunk_list_size(chunk_list) == n)
|
|
# endif
|
|
#endif
|
|
#ifndef ASSERT_CHUNKLIST_SIZE
|
|
# define ASSERT_CHUNKLIST_SIZE(chunk_list, n) (EXPR_NOP(chunk_list), EXPR_NOP(n))
|
|
#endif
|
|
|
|
|
|
#ifdef USE_VALIDATE_LIST_DATA_PARTIAL
|
|
static size_t bchunk_list_data_check(
|
|
const BChunkList *chunk_list, const uchar *data)
|
|
{
|
|
size_t offset = 0;
|
|
for (BChunkRef *cref = chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
if (memcmp(&data[offset], cref->link->data, cref->link->data_len) != 0) {
|
|
return false;
|
|
}
|
|
offset += cref->link->data_len;
|
|
}
|
|
return true;
|
|
}
|
|
# define ASSERT_CHUNKLIST_DATA(chunk_list, data) BLI_assert(bchunk_list_data_check(chunk_list, data))
|
|
#else
|
|
# define ASSERT_CHUNKLIST_DATA(chunk_list, data) (EXPR_NOP(chunk_list), EXPR_NOP(data))
|
|
#endif
|
|
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
static void bchunk_list_ensure_min_size_last(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list)
|
|
{
|
|
BChunkRef *cref = chunk_list->chunk_refs.last;
|
|
if (cref && cref->prev) {
|
|
/* both are decref'd after use (end of this block) */
|
|
BChunk *chunk_curr = cref->link;
|
|
BChunk *chunk_prev = cref->prev->link;
|
|
|
|
if (MIN2(chunk_prev->data_len, chunk_curr->data_len) < info->chunk_byte_size_min) {
|
|
const size_t data_merge_len = chunk_prev->data_len + chunk_curr->data_len;
|
|
/* we could pass, but no need */
|
|
if (data_merge_len <= info->chunk_byte_size_max) {
|
|
/* we have enough space to merge */
|
|
|
|
/* remove last from linklist */
|
|
BLI_assert(chunk_list->chunk_refs.last != chunk_list->chunk_refs.first);
|
|
cref->prev->next = NULL;
|
|
chunk_list->chunk_refs.last = cref->prev;
|
|
chunk_list->chunk_refs_len -= 1;
|
|
|
|
uchar *data_merge = MEM_mallocN(data_merge_len, __func__);
|
|
memcpy(data_merge, chunk_prev->data, chunk_prev->data_len);
|
|
memcpy(&data_merge[chunk_prev->data_len], chunk_curr->data, chunk_curr->data_len);
|
|
|
|
cref->prev->link = bchunk_new(bs_mem, data_merge, data_merge_len);
|
|
cref->prev->link->users += 1;
|
|
|
|
BLI_mempool_free(bs_mem->chunk_ref, cref);
|
|
}
|
|
else {
|
|
/* If we always merge small slices, we should _almost_ never end up having very large chunks.
|
|
* Gradual expanding on contracting will cause this.
|
|
*
|
|
* if we do, the code below works (test by setting 'BCHUNK_SIZE_MAX_MUL = 1.2') */
|
|
|
|
/* keep chunk on the left hand side a regular size */
|
|
const size_t split = info->chunk_byte_size;
|
|
|
|
/* merge and split */
|
|
const size_t data_prev_len = split;
|
|
const size_t data_curr_len = data_merge_len - split;
|
|
uchar *data_prev = MEM_mallocN(data_prev_len, __func__);
|
|
uchar *data_curr = MEM_mallocN(data_curr_len, __func__);
|
|
|
|
if (data_prev_len <= chunk_prev->data_len) {
|
|
const size_t data_curr_shrink_len = chunk_prev->data_len - data_prev_len;
|
|
|
|
/* setup 'data_prev' */
|
|
memcpy(data_prev, chunk_prev->data, data_prev_len);
|
|
|
|
/* setup 'data_curr' */
|
|
memcpy(data_curr, &chunk_prev->data[data_prev_len], data_curr_shrink_len);
|
|
memcpy(&data_curr[data_curr_shrink_len], chunk_curr->data, chunk_curr->data_len);
|
|
}
|
|
else {
|
|
BLI_assert(data_curr_len <= chunk_curr->data_len);
|
|
BLI_assert(data_prev_len >= chunk_prev->data_len);
|
|
|
|
const size_t data_prev_grow_len = data_prev_len - chunk_prev->data_len;
|
|
|
|
/* setup 'data_prev' */
|
|
memcpy(data_prev, chunk_prev->data, chunk_prev->data_len);
|
|
memcpy(&data_prev[chunk_prev->data_len], chunk_curr->data, data_prev_grow_len);
|
|
|
|
/* setup 'data_curr' */
|
|
memcpy(data_curr, &chunk_curr->data[data_prev_grow_len], data_curr_len);
|
|
}
|
|
|
|
cref->prev->link = bchunk_new(bs_mem, data_prev, data_prev_len);
|
|
cref->prev->link->users += 1;
|
|
|
|
cref->link = bchunk_new(bs_mem, data_curr, data_curr_len);
|
|
cref->link->users += 1;
|
|
}
|
|
|
|
/* free zero users */
|
|
bchunk_decref(bs_mem, chunk_curr);
|
|
bchunk_decref(bs_mem, chunk_prev);
|
|
}
|
|
}
|
|
}
|
|
#endif /* USE_MERGE_CHUNKS */
|
|
|
|
|
|
/**
|
|
* Split length into 2 values
|
|
* \param r_data_trim_len: Length which is aligned to the #BArrayInfo.chunk_byte_size
|
|
* \param r_data_last_chunk_len: The remaining bytes.
|
|
*
|
|
* \note This function ensures the size of \a r_data_last_chunk_len
|
|
* is larger than #BArrayInfo.chunk_byte_size_min.
|
|
*/
|
|
static void bchunk_list_calc_trim_len(
|
|
const BArrayInfo *info, const size_t data_len,
|
|
size_t *r_data_trim_len, size_t *r_data_last_chunk_len)
|
|
{
|
|
size_t data_last_chunk_len = 0;
|
|
size_t data_trim_len = data_len;
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
/* avoid creating too-small chunks
|
|
* more efficient then merging after */
|
|
if (data_len > info->chunk_byte_size) {
|
|
data_last_chunk_len = (data_trim_len % info->chunk_byte_size);
|
|
data_trim_len = data_trim_len - data_last_chunk_len;
|
|
if (data_last_chunk_len) {
|
|
if (data_last_chunk_len < info->chunk_byte_size_min) {
|
|
/* may be zero and thats OK */
|
|
data_trim_len -= info->chunk_byte_size;
|
|
data_last_chunk_len += info->chunk_byte_size;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
data_trim_len = 0;
|
|
data_last_chunk_len = data_len;
|
|
}
|
|
|
|
BLI_assert((data_trim_len == 0) || (data_trim_len >= info->chunk_byte_size));
|
|
#else
|
|
data_last_chunk_len = (data_trim_len % info->chunk_byte_size);
|
|
data_trim_len = data_trim_len - data_last_chunk_len;
|
|
#endif
|
|
|
|
BLI_assert(data_trim_len + data_last_chunk_len == data_len);
|
|
|
|
*r_data_trim_len = data_trim_len;
|
|
*r_data_last_chunk_len = data_last_chunk_len;
|
|
}
|
|
|
|
/**
|
|
* Append and don't manage merging small chunks.
|
|
*/
|
|
static void bchunk_list_append_only(
|
|
BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list, BChunk *chunk)
|
|
{
|
|
BChunkRef *cref = BLI_mempool_alloc(bs_mem->chunk_ref);
|
|
BLI_addtail(&chunk_list->chunk_refs, cref);
|
|
cref->link = chunk;
|
|
chunk_list->chunk_refs_len += 1;
|
|
chunk->users += 1;
|
|
}
|
|
|
|
/**
|
|
* \note This is for writing single chunks,
|
|
* use #bchunk_list_append_data_n when writing large blocks of memory into many chunks.
|
|
*/
|
|
static void bchunk_list_append_data(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list,
|
|
const uchar *data, const size_t data_len)
|
|
{
|
|
BLI_assert(data_len != 0);
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
BLI_assert(data_len <= info->chunk_byte_size_max);
|
|
|
|
if (!BLI_listbase_is_empty(&chunk_list->chunk_refs)) {
|
|
BChunkRef *cref = chunk_list->chunk_refs.last;
|
|
BChunk *chunk_prev = cref->link;
|
|
|
|
if (MIN2(chunk_prev->data_len, data_len) < info->chunk_byte_size_min) {
|
|
const size_t data_merge_len = chunk_prev->data_len + data_len;
|
|
/* realloc for single user */
|
|
if (cref->link->users == 1) {
|
|
uchar *data_merge = MEM_reallocN((void *)cref->link->data, data_merge_len);
|
|
memcpy(&data_merge[chunk_prev->data_len], data, data_len);
|
|
cref->link->data = data_merge;
|
|
cref->link->data_len = data_merge_len;
|
|
}
|
|
else {
|
|
uchar *data_merge = MEM_mallocN(data_merge_len, __func__);
|
|
memcpy(data_merge, chunk_prev->data, chunk_prev->data_len);
|
|
memcpy(&data_merge[chunk_prev->data_len], data, data_len);
|
|
cref->link = bchunk_new(bs_mem, data_merge, data_merge_len);
|
|
cref->link->users += 1;
|
|
bchunk_decref(bs_mem, chunk_prev);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
#else
|
|
UNUSED_VARS(info);
|
|
#endif /* USE_MERGE_CHUNKS */
|
|
|
|
BChunk *chunk = bchunk_new_copydata(bs_mem, data, data_len);
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
|
|
/* don't run this, instead preemptively avoid creating a chunk only to merge it (above). */
|
|
#if 0
|
|
#ifdef USE_MERGE_CHUNKS
|
|
bchunk_list_ensure_min_size_last(info, bs_mem, chunk_list);
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Similar to #bchunk_list_append_data, but handle multiple chunks.
|
|
* Use for adding arrays of arbitrary sized memory at once.
|
|
*
|
|
* \note This function takes care not to perform redundant chunk-merging checks,
|
|
* so we can write successive fixed size chunks quickly.
|
|
*/
|
|
static void bchunk_list_append_data_n(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list,
|
|
const uchar *data, size_t data_len)
|
|
{
|
|
size_t data_trim_len, data_last_chunk_len;
|
|
bchunk_list_calc_trim_len(info, data_len, &data_trim_len, &data_last_chunk_len);
|
|
|
|
if (data_trim_len != 0) {
|
|
size_t i_prev;
|
|
|
|
{
|
|
const size_t i = info->chunk_byte_size;
|
|
bchunk_list_append_data(info, bs_mem, chunk_list, data, i);
|
|
i_prev = i;
|
|
}
|
|
|
|
while (i_prev != data_trim_len) {
|
|
const size_t i = i_prev + info->chunk_byte_size;
|
|
BChunk *chunk = bchunk_new_copydata(bs_mem, &data[i_prev], i - i_prev);
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
i_prev = i;
|
|
}
|
|
|
|
if (data_last_chunk_len) {
|
|
BChunk *chunk = bchunk_new_copydata(bs_mem, &data[i_prev], data_last_chunk_len);
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
// i_prev = data_len; /* UNUSED */
|
|
}
|
|
}
|
|
else {
|
|
/* if we didn't write any chunks previously,
|
|
* we may need to merge with the last. */
|
|
if (data_last_chunk_len) {
|
|
bchunk_list_append_data(info, bs_mem, chunk_list, data, data_last_chunk_len);
|
|
// i_prev = data_len; /* UNUSED */
|
|
}
|
|
}
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
if (data_len > info->chunk_byte_size) {
|
|
BLI_assert(((BChunkRef *)chunk_list->chunk_refs.last)->link->data_len >= info->chunk_byte_size_min);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void bchunk_list_append(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list,
|
|
BChunk *chunk)
|
|
{
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
bchunk_list_ensure_min_size_last(info, bs_mem, chunk_list);
|
|
#else
|
|
UNUSED_VARS(info);
|
|
#endif
|
|
}
|
|
|
|
static void bchunk_list_fill_from_array(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
BChunkList *chunk_list,
|
|
const uchar *data,
|
|
const size_t data_len)
|
|
{
|
|
BLI_assert(BLI_listbase_is_empty(&chunk_list->chunk_refs));
|
|
|
|
size_t data_trim_len, data_last_chunk_len;
|
|
bchunk_list_calc_trim_len(info, data_len, &data_trim_len, &data_last_chunk_len);
|
|
|
|
size_t i_prev = 0;
|
|
while (i_prev != data_trim_len) {
|
|
const size_t i = i_prev + info->chunk_byte_size;
|
|
BChunk *chunk = bchunk_new_copydata(bs_mem, &data[i_prev], i - i_prev);
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
i_prev = i;
|
|
}
|
|
|
|
if (data_last_chunk_len) {
|
|
BChunk *chunk = bchunk_new_copydata(bs_mem, &data[i_prev], data_last_chunk_len);
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
// i_prev = data_len;
|
|
}
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
if (data_len > info->chunk_byte_size) {
|
|
BLI_assert(((BChunkRef *)chunk_list->chunk_refs.last)->link->data_len >= info->chunk_byte_size_min);
|
|
}
|
|
#endif
|
|
|
|
/* works but better avoid redundant re-alloc */
|
|
#if 0
|
|
#ifdef USE_MERGE_CHUNKS
|
|
bchunk_list_ensure_min_size_last(info, bs_mem, chunk_list);
|
|
#endif
|
|
#endif
|
|
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, data_len);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
}
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Internal Table Lookup Functions
|
|
*/
|
|
|
|
/** \name Internal Hashing/De-Duplication API
|
|
*
|
|
* Only used by #bchunk_list_from_data_merge
|
|
* \{ */
|
|
|
|
#define HASH_INIT (5381)
|
|
|
|
BLI_INLINE uint hash_data_single(const uchar p)
|
|
{
|
|
return ((HASH_INIT << 5) + HASH_INIT) + (unsigned int)(*((signed char *)&p));
|
|
}
|
|
|
|
/* hash bytes, from BLI_ghashutil_strhash_n */
|
|
static uint hash_data(const uchar *key, size_t n)
|
|
{
|
|
const signed char *p;
|
|
unsigned int h = HASH_INIT;
|
|
|
|
for (p = (const signed char *)key; n--; p++) {
|
|
h = ((h << 5) + h) + (unsigned int)*p;
|
|
}
|
|
|
|
return h;
|
|
}
|
|
|
|
#undef HASH_INIT
|
|
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
static void hash_array_from_data(
|
|
const BArrayInfo *info, const uchar *data_slice, const size_t data_slice_len,
|
|
hash_key *hash_array)
|
|
{
|
|
if (info->chunk_stride != 1) {
|
|
for (size_t i = 0, i_step = 0; i_step < data_slice_len; i++, i_step += info->chunk_stride) {
|
|
hash_array[i] = hash_data(&data_slice[i_step], info->chunk_stride);
|
|
}
|
|
}
|
|
else {
|
|
/* fast-path for bytes */
|
|
for (size_t i = 0; i < data_slice_len; i++) {
|
|
hash_array[i] = hash_data_single(data_slice[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Similar to hash_array_from_data,
|
|
* but able to step into the next chunk if we run-out of data.
|
|
*/
|
|
static void hash_array_from_cref(
|
|
const BArrayInfo *info, const BChunkRef *cref, const size_t data_len,
|
|
hash_key *hash_array)
|
|
{
|
|
const size_t hash_array_len = data_len / info->chunk_stride;
|
|
size_t i = 0;
|
|
do {
|
|
size_t i_next = hash_array_len - i;
|
|
size_t data_trim_len = i_next * info->chunk_stride;
|
|
if (data_trim_len > cref->link->data_len) {
|
|
data_trim_len = cref->link->data_len;
|
|
i_next = data_trim_len / info->chunk_stride;
|
|
}
|
|
BLI_assert(data_trim_len <= cref->link->data_len);
|
|
hash_array_from_data(info, cref->link->data, data_trim_len, &hash_array[i]);
|
|
i += i_next;
|
|
cref = cref->next;
|
|
} while ((i < hash_array_len) && (cref != NULL));
|
|
|
|
/* If this isn't equal, the caller didn't properly check
|
|
* that there was enough data left in all chunks */
|
|
BLI_assert(i == hash_array_len);
|
|
}
|
|
|
|
static void hash_accum(hash_key *hash_array, const size_t hash_array_len, size_t iter_steps)
|
|
{
|
|
/* _very_ unlikely, can happen if you select a chunk-size of 1 for example. */
|
|
if (UNLIKELY((iter_steps > hash_array_len))) {
|
|
iter_steps = hash_array_len;
|
|
}
|
|
|
|
const size_t hash_array_search_len = hash_array_len - iter_steps;
|
|
while (iter_steps != 0) {
|
|
const size_t hash_offset = iter_steps;
|
|
for (uint i = 0; i < hash_array_search_len; i++) {
|
|
hash_array[i] += (hash_array[i + hash_offset]) * ((hash_array[i] & 0xff) + 1);
|
|
}
|
|
iter_steps -= 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When we only need a single value, can use a small optimization.
|
|
* we can avoid accumulating the tail of the array a little, each iteration.
|
|
*/
|
|
static void hash_accum_single(hash_key *hash_array, const size_t hash_array_len, size_t iter_steps)
|
|
{
|
|
BLI_assert(iter_steps <= hash_array_len);
|
|
if (UNLIKELY(!(iter_steps <= hash_array_len))) {
|
|
/* while this shouldn't happen, avoid crashing */
|
|
iter_steps = hash_array_len;
|
|
}
|
|
/* We can increase this value each step to avoid accumulating quite as much
|
|
* while getting the same results as hash_accum */
|
|
size_t iter_steps_sub = iter_steps;
|
|
|
|
while (iter_steps != 0) {
|
|
const size_t hash_array_search_len = hash_array_len - iter_steps_sub;
|
|
const size_t hash_offset = iter_steps;
|
|
for (uint i = 0; i < hash_array_search_len; i++) {
|
|
hash_array[i] += (hash_array[i + hash_offset]) * ((hash_array[i] & 0xff) + 1);
|
|
}
|
|
iter_steps -= 1;
|
|
iter_steps_sub += iter_steps;
|
|
}
|
|
}
|
|
|
|
static hash_key key_from_chunk_ref(
|
|
const BArrayInfo *info, const BChunkRef *cref,
|
|
/* avoid reallocating each time */
|
|
hash_key *hash_store, const size_t hash_store_len)
|
|
{
|
|
/* in C, will fill in a reusable array */
|
|
BChunk *chunk = cref->link;
|
|
BLI_assert((info->accum_read_ahead_bytes * info->chunk_stride) != 0);
|
|
|
|
if (info->accum_read_ahead_bytes <= chunk->data_len) {
|
|
hash_key key;
|
|
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
key = chunk->key;
|
|
if (key != HASH_TABLE_KEY_UNSET) {
|
|
/* Using key cache!
|
|
* avoids calculating every time */
|
|
}
|
|
else {
|
|
hash_array_from_cref(info, cref, info->accum_read_ahead_bytes, hash_store);
|
|
hash_accum_single(hash_store, hash_store_len, info->accum_steps);
|
|
key = hash_store[0];
|
|
|
|
/* cache the key */
|
|
if (UNLIKELY(key == HASH_TABLE_KEY_UNSET)) {
|
|
key = HASH_TABLE_KEY_FALLBACK;
|
|
}
|
|
chunk->key = key;
|
|
}
|
|
#else
|
|
hash_array_from_cref(info, cref, info->accum_read_ahead_bytes, hash_store);
|
|
hash_accum_single(hash_store, hash_store_len, info->accum_steps);
|
|
key = hash_store[0];
|
|
#endif
|
|
return key;
|
|
}
|
|
else {
|
|
/* corner case - we're too small, calculate the key each time. */
|
|
|
|
hash_array_from_cref(info, cref, info->accum_read_ahead_bytes, hash_store);
|
|
hash_accum_single(hash_store, hash_store_len, info->accum_steps);
|
|
hash_key key = hash_store[0];
|
|
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
if (UNLIKELY(key == HASH_TABLE_KEY_UNSET)) {
|
|
key = HASH_TABLE_KEY_FALLBACK;
|
|
}
|
|
#endif
|
|
return key;
|
|
}
|
|
}
|
|
|
|
static const BChunkRef *table_lookup(
|
|
const BArrayInfo *info, BTableRef **table, const size_t table_len, const size_t i_table_start,
|
|
const uchar *data, const size_t data_len, const size_t offset, const hash_key *table_hash_array)
|
|
{
|
|
size_t size_left = data_len - offset;
|
|
hash_key key = table_hash_array[((offset - i_table_start) / info->chunk_stride)];
|
|
size_t key_index = (size_t)(key % (hash_key)table_len);
|
|
for (const BTableRef *tref = table[key_index]; tref; tref = tref->next) {
|
|
const BChunkRef *cref = tref->cref;
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
if (cref->link->key == key)
|
|
#endif
|
|
{
|
|
BChunk *chunk_test = cref->link;
|
|
if (chunk_test->data_len <= size_left) {
|
|
if (bchunk_data_compare(chunk_test, data, data_len, offset)) {
|
|
/* we could remove the chunk from the table, to avoid multiple hits */
|
|
return cref;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#else /* USE_HASH_TABLE_ACCUMULATE */
|
|
|
|
/* NON USE_HASH_TABLE_ACCUMULATE code (simply hash each chunk) */
|
|
|
|
static hash_key key_from_chunk_ref(const BArrayInfo *info, const BChunkRef *cref)
|
|
{
|
|
const size_t data_hash_len = BCHUNK_HASH_LEN * info->chunk_stride;
|
|
hash_key key;
|
|
BChunk *chunk = cref->link;
|
|
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
key = chunk->key;
|
|
if (key != HASH_TABLE_KEY_UNSET) {
|
|
/* Using key cache!
|
|
* avoids calculating every time */
|
|
}
|
|
else {
|
|
/* cache the key */
|
|
key = hash_data(chunk->data, data_hash_len);
|
|
if (key == HASH_TABLE_KEY_UNSET) {
|
|
key = HASH_TABLE_KEY_FALLBACK;
|
|
}
|
|
chunk->key = key;
|
|
}
|
|
#else
|
|
key = hash_data(chunk->data, data_hash_len);
|
|
#endif
|
|
|
|
return key;
|
|
}
|
|
|
|
static const BChunkRef *table_lookup(
|
|
const BArrayInfo *info, BTableRef **table, const size_t table_len, const uint UNUSED(i_table_start),
|
|
const uchar *data, const size_t data_len, const size_t offset, const hash_key *UNUSED(table_hash_array))
|
|
{
|
|
const size_t data_hash_len = BCHUNK_HASH_LEN * info->chunk_stride; /* TODO, cache */
|
|
|
|
size_t size_left = data_len - offset;
|
|
hash_key key = hash_data(&data[offset], MIN2(data_hash_len, size_left));
|
|
size_t key_index = (size_t)(key % (hash_key)table_len);
|
|
for (BTableRef *tref = table[key_index]; tref; tref = tref->next) {
|
|
const BChunkRef *cref = tref->cref;
|
|
#ifdef USE_HASH_TABLE_KEY_CACHE
|
|
if (cref->link->key == key)
|
|
#endif
|
|
{
|
|
BChunk *chunk_test = cref->link;
|
|
if (chunk_test->data_len <= size_left) {
|
|
if (bchunk_data_compare(chunk_test, data, data_len, offset)) {
|
|
/* we could remove the chunk from the table, to avoid multiple hits */
|
|
return cref;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#endif /* USE_HASH_TABLE_ACCUMULATE */
|
|
|
|
/* End Table Lookup
|
|
* ---------------- */
|
|
|
|
/** \} */
|
|
|
|
/**
|
|
* \param data: Data to store in the returned value.
|
|
* \param data_len_original: Length of data in bytes.
|
|
* \param chunk_list_reference: Reuse this list or chunks within it, don't modify its content.
|
|
* \note Caller is responsible for adding the user.
|
|
*/
|
|
static BChunkList *bchunk_list_from_data_merge(
|
|
const BArrayInfo *info, BArrayMemory *bs_mem,
|
|
const uchar *data, const size_t data_len_original,
|
|
const BChunkList *chunk_list_reference)
|
|
{
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list_reference, chunk_list_reference->total_size);
|
|
|
|
/* -----------------------------------------------------------------------
|
|
* Fast-Path for exact match
|
|
* Check for exact match, if so, return the current list.
|
|
*/
|
|
|
|
const BChunkRef *cref_match_first = NULL;
|
|
|
|
uint chunk_list_reference_skip_len = 0;
|
|
size_t chunk_list_reference_skip_bytes = 0;
|
|
size_t i_prev = 0;
|
|
|
|
#ifdef USE_FASTPATH_CHUNKS_FIRST
|
|
{
|
|
bool full_match = true;
|
|
|
|
const BChunkRef *cref = chunk_list_reference->chunk_refs.first;
|
|
while (i_prev < data_len_original) {
|
|
if (cref != NULL && bchunk_data_compare(cref->link, data, data_len_original, i_prev)) {
|
|
cref_match_first = cref;
|
|
chunk_list_reference_skip_len += 1;
|
|
chunk_list_reference_skip_bytes += cref->link->data_len;
|
|
i_prev += cref->link->data_len;
|
|
cref = cref->next;
|
|
}
|
|
else {
|
|
full_match = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (full_match) {
|
|
if (chunk_list_reference->total_size == data_len_original) {
|
|
return (BChunkList *)chunk_list_reference;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* End Fast-Path (first)
|
|
* --------------------- */
|
|
|
|
#endif /* USE_FASTPATH_CHUNKS_FIRST */
|
|
|
|
/* Copy until we have a mismatch */
|
|
BChunkList *chunk_list = bchunk_list_new(bs_mem, data_len_original);
|
|
if (cref_match_first != NULL) {
|
|
size_t chunk_size_step = 0;
|
|
const BChunkRef *cref = chunk_list_reference->chunk_refs.first;
|
|
while (true) {
|
|
BChunk *chunk = cref->link;
|
|
chunk_size_step += chunk->data_len;
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, chunk_size_step);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
if (cref == cref_match_first) {
|
|
break;
|
|
}
|
|
else {
|
|
cref = cref->next;
|
|
}
|
|
}
|
|
/* happens when bytes are removed from the end of the array */
|
|
if (chunk_size_step == data_len_original) {
|
|
return chunk_list;
|
|
}
|
|
|
|
i_prev = chunk_size_step;
|
|
}
|
|
else {
|
|
i_prev = 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------
|
|
* Fast-Path for end chunks
|
|
*
|
|
* Check for trailing chunks
|
|
*/
|
|
|
|
/* In this case use 'chunk_list_reference_last' to define the last index
|
|
* index_match_last = -1 */
|
|
|
|
/* warning, from now on don't use len(data)
|
|
* since we want to ignore chunks already matched */
|
|
size_t data_len = data_len_original;
|
|
#define data_len_original invalid_usage
|
|
#ifdef data_len_original /* quiet warning */
|
|
#endif
|
|
|
|
const BChunkRef *chunk_list_reference_last = NULL;
|
|
|
|
#ifdef USE_FASTPATH_CHUNKS_LAST
|
|
if (!BLI_listbase_is_empty(&chunk_list_reference->chunk_refs)) {
|
|
const BChunkRef *cref = chunk_list_reference->chunk_refs.last;
|
|
while ((cref->prev != NULL) &&
|
|
(cref != cref_match_first) &&
|
|
(cref->link->data_len <= data_len - i_prev))
|
|
{
|
|
BChunk *chunk_test = cref->link;
|
|
size_t offset = data_len - chunk_test->data_len;
|
|
if (bchunk_data_compare(chunk_test, data, data_len, offset)) {
|
|
data_len = offset;
|
|
chunk_list_reference_last = cref;
|
|
chunk_list_reference_skip_len += 1;
|
|
chunk_list_reference_skip_bytes += cref->link->data_len;
|
|
cref = cref->prev;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* End Fast-Path (last)
|
|
* -------------------- */
|
|
#endif /* USE_FASTPATH_CHUNKS_LAST */
|
|
|
|
/* -----------------------------------------------------------------------
|
|
* Check for aligned chunks
|
|
*
|
|
* This saves a lot of searching, so use simple heuristics to detect aligned arrays.
|
|
* (may need to tweak exact method).
|
|
*/
|
|
|
|
bool use_aligned = false;
|
|
|
|
#ifdef USE_ALIGN_CHUNKS_TEST
|
|
if (chunk_list->total_size == chunk_list_reference->total_size) {
|
|
/* if we're already a quarter aligned */
|
|
if (data_len - i_prev <= chunk_list->total_size / 4) {
|
|
use_aligned = true;
|
|
}
|
|
else {
|
|
/* TODO, walk over chunks and check if some arbitrary amount align */
|
|
}
|
|
}
|
|
#endif /* USE_ALIGN_CHUNKS_TEST */
|
|
|
|
/* End Aligned Chunk Case
|
|
* ----------------------- */
|
|
|
|
if (use_aligned) {
|
|
/* Copy matching chunks, creates using the same 'layout' as the reference */
|
|
const BChunkRef *cref = cref_match_first ? cref_match_first->next : chunk_list_reference->chunk_refs.first;
|
|
while (i_prev != data_len) {
|
|
const size_t i = i_prev + cref->link->data_len;
|
|
BLI_assert(i != i_prev);
|
|
|
|
if ((cref != chunk_list_reference_last) &&
|
|
bchunk_data_compare(cref->link, data, data_len, i_prev))
|
|
{
|
|
bchunk_list_append(info, bs_mem, chunk_list, cref->link);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, i);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
}
|
|
else {
|
|
bchunk_list_append_data(info, bs_mem, chunk_list, &data[i_prev], i - i_prev);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, i);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
}
|
|
|
|
cref = cref->next;
|
|
|
|
i_prev = i;
|
|
}
|
|
}
|
|
else if ((data_len - i_prev >= info->chunk_byte_size) &&
|
|
(chunk_list_reference->chunk_refs_len >= chunk_list_reference_skip_len) &&
|
|
(chunk_list_reference->chunk_refs.first != NULL))
|
|
{
|
|
|
|
/* --------------------------------------------------------------------
|
|
* Non-Aligned Chunk De-Duplication */
|
|
|
|
/* only create a table if we have at least one chunk to search
|
|
* otherwise just make a new one.
|
|
*
|
|
* Support re-arranged chunks */
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
size_t i_table_start = i_prev;
|
|
const size_t table_hash_array_len = (data_len - i_prev) / info->chunk_stride;
|
|
hash_key *table_hash_array = MEM_mallocN(sizeof(*table_hash_array) * table_hash_array_len, __func__);
|
|
hash_array_from_data(info, &data[i_prev], data_len - i_prev, table_hash_array);
|
|
|
|
hash_accum(table_hash_array, table_hash_array_len, info->accum_steps);
|
|
#else
|
|
/* dummy vars */
|
|
uint i_table_start = 0;
|
|
hash_key *table_hash_array = NULL;
|
|
#endif
|
|
|
|
const uint chunk_list_reference_remaining_len =
|
|
(chunk_list_reference->chunk_refs_len - chunk_list_reference_skip_len) + 1;
|
|
BTableRef *table_ref_stack = MEM_mallocN(chunk_list_reference_remaining_len * sizeof(BTableRef), __func__);
|
|
uint table_ref_stack_n = 0;
|
|
|
|
const size_t table_len = chunk_list_reference_remaining_len * BCHUNK_HASH_TABLE_MUL;
|
|
BTableRef **table = MEM_callocN(table_len * sizeof(*table), __func__);
|
|
|
|
/* table_make - inline
|
|
* include one matching chunk, to allow for repeating values */
|
|
{
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
const size_t hash_store_len = info->accum_read_ahead_len;
|
|
hash_key *hash_store = MEM_mallocN(sizeof(hash_key) * hash_store_len, __func__);
|
|
#endif
|
|
|
|
const BChunkRef *cref;
|
|
size_t chunk_list_reference_bytes_remaining =
|
|
chunk_list_reference->total_size - chunk_list_reference_skip_bytes;
|
|
|
|
if (cref_match_first) {
|
|
cref = cref_match_first;
|
|
chunk_list_reference_bytes_remaining += cref->link->data_len;
|
|
}
|
|
else {
|
|
cref = chunk_list_reference->chunk_refs.first;
|
|
}
|
|
|
|
#ifdef USE_PARANOID_CHECKS
|
|
{
|
|
size_t test_bytes_len = 0;
|
|
const BChunkRef *cr = cref;
|
|
while (cr != chunk_list_reference_last) {
|
|
test_bytes_len += cr->link->data_len;
|
|
cr = cr->next;
|
|
}
|
|
BLI_assert(test_bytes_len == chunk_list_reference_bytes_remaining);
|
|
}
|
|
#endif
|
|
|
|
while ((cref != chunk_list_reference_last) &&
|
|
(chunk_list_reference_bytes_remaining >= info->accum_read_ahead_bytes))
|
|
{
|
|
hash_key key = key_from_chunk_ref(info, cref
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
, hash_store, hash_store_len
|
|
#endif
|
|
);
|
|
size_t key_index = (size_t)(key % (hash_key)table_len);
|
|
BTableRef *tref_prev = table[key_index];
|
|
BLI_assert(table_ref_stack_n < chunk_list_reference_remaining_len);
|
|
BTableRef *tref = &table_ref_stack[table_ref_stack_n++];
|
|
tref->cref = cref;
|
|
tref->next = tref_prev;
|
|
table[key_index] = tref;
|
|
|
|
chunk_list_reference_bytes_remaining -= cref->link->data_len;
|
|
cref = cref->next;
|
|
}
|
|
|
|
BLI_assert(table_ref_stack_n <= chunk_list_reference_remaining_len);
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
MEM_freeN(hash_store);
|
|
#endif
|
|
}
|
|
/* done making the table */
|
|
|
|
BLI_assert(i_prev <= data_len);
|
|
for (size_t i = i_prev; i < data_len; ) {
|
|
/* Assumes exiting chunk isnt a match! */
|
|
|
|
const BChunkRef *cref_found = table_lookup(
|
|
info,
|
|
table, table_len, i_table_start,
|
|
data, data_len, i, table_hash_array);
|
|
if (cref_found != NULL) {
|
|
BLI_assert(i < data_len);
|
|
if (i != i_prev) {
|
|
bchunk_list_append_data_n(info, bs_mem, chunk_list, &data[i_prev], i - i_prev);
|
|
i_prev = i;
|
|
}
|
|
|
|
/* now add the reference chunk */
|
|
{
|
|
BChunk *chunk_found = cref_found->link;
|
|
i += chunk_found->data_len;
|
|
bchunk_list_append(info, bs_mem, chunk_list, chunk_found);
|
|
}
|
|
i_prev = i;
|
|
BLI_assert(i_prev <= data_len);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, i_prev);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
|
|
/* its likely that the next chunk in the list will be a match, so check it! */
|
|
while ((cref_found->next != NULL) &&
|
|
(cref_found->next != chunk_list_reference_last))
|
|
{
|
|
cref_found = cref_found->next;
|
|
BChunk *chunk_found = cref_found->link;
|
|
|
|
if (bchunk_data_compare(chunk_found, data, data_len, i_prev)) {
|
|
/* may be useful to remove table data, assuming we dont have repeating memory
|
|
* where it would be useful to re-use chunks. */
|
|
i += chunk_found->data_len;
|
|
bchunk_list_append(info, bs_mem, chunk_list, chunk_found);
|
|
/* chunk_found may be freed! */
|
|
i_prev = i;
|
|
BLI_assert(i_prev <= data_len);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, i_prev);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
i = i + info->chunk_stride;
|
|
}
|
|
}
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
MEM_freeN(table_hash_array);
|
|
#endif
|
|
MEM_freeN(table);
|
|
MEM_freeN(table_ref_stack);
|
|
|
|
/* End Table Lookup
|
|
* ---------------- */
|
|
}
|
|
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, i_prev);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
|
|
/* -----------------------------------------------------------------------
|
|
* No Duplicates to copy, write new chunks
|
|
*
|
|
* Trailing chunks, no matches found in table lookup above.
|
|
* Write all new data. */
|
|
if (i_prev != data_len) {
|
|
bchunk_list_append_data_n(info, bs_mem, chunk_list, &data[i_prev], data_len - i_prev);
|
|
i_prev = data_len;
|
|
}
|
|
|
|
BLI_assert(i_prev == data_len);
|
|
|
|
#ifdef USE_FASTPATH_CHUNKS_LAST
|
|
if (chunk_list_reference_last != NULL) {
|
|
/* write chunk_list_reference_last since it hasn't been written yet */
|
|
const BChunkRef *cref = chunk_list_reference_last;
|
|
while (cref != NULL) {
|
|
BChunk *chunk = cref->link;
|
|
// BLI_assert(bchunk_data_compare(chunk, data, data_len, i_prev));
|
|
i_prev += chunk->data_len;
|
|
/* use simple since we assume the references chunks have already been sized correctly. */
|
|
bchunk_list_append_only(bs_mem, chunk_list, chunk);
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
cref = cref->next;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#undef data_len_original
|
|
|
|
BLI_assert(i_prev == data_len_original);
|
|
|
|
/* check we're the correct size and that we didn't accidentally modify the reference */
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list, data_len_original);
|
|
ASSERT_CHUNKLIST_SIZE(chunk_list_reference, chunk_list_reference->total_size);
|
|
|
|
ASSERT_CHUNKLIST_DATA(chunk_list, data);
|
|
|
|
return chunk_list;
|
|
}
|
|
/* end private API */
|
|
|
|
/** \} */
|
|
|
|
|
|
/** \name Main Array Storage API
|
|
* \{ */
|
|
|
|
|
|
/**
|
|
* Create a new array store, which can store any number of arrays
|
|
* as long as their stride matches.
|
|
*
|
|
* \param stride: ``sizeof()`` each element,
|
|
*
|
|
* \note while a stride of ``1`` will always work,
|
|
* its less efficient since duplicate chunks of memory will be searched
|
|
* at positions unaligned with the array data.
|
|
*
|
|
* \param chunk_count: Number of elements to split each chunk into.
|
|
* - A small value increases the ability to de-duplicate chunks,
|
|
* but adds overhead by increasing the number of chunks to look-up when searching for duplicates,
|
|
* as well as some overhead constructing the original array again, with more calls to ``memcpy``.
|
|
* - Larger values reduce the *book keeping* overhead,
|
|
* but increase the chance a small, isolated change will cause a larger amount of data to be duplicated.
|
|
*
|
|
* \return A new array store, to be freed with #BLI_array_store_destroy.
|
|
*/
|
|
BArrayStore *BLI_array_store_create(
|
|
uint stride,
|
|
uint chunk_count)
|
|
{
|
|
BArrayStore *bs = MEM_callocN(sizeof(BArrayStore), __func__);
|
|
|
|
bs->info.chunk_stride = stride;
|
|
// bs->info.chunk_count = chunk_count;
|
|
|
|
bs->info.chunk_byte_size = chunk_count * stride;
|
|
#ifdef USE_MERGE_CHUNKS
|
|
bs->info.chunk_byte_size_min = MAX2(1u, chunk_count / BCHUNK_SIZE_MIN_DIV) * stride;
|
|
bs->info.chunk_byte_size_max = (chunk_count * BCHUNK_SIZE_MAX_MUL) * stride;
|
|
#endif
|
|
|
|
#ifdef USE_HASH_TABLE_ACCUMULATE
|
|
bs->info.accum_steps = BCHUNK_HASH_TABLE_ACCUMULATE_STEPS - 1;
|
|
/* Triangle number, identifying now much read-ahead we need:
|
|
* https://en.wikipedia.org/wiki/Triangular_number (+ 1) */
|
|
bs->info.accum_read_ahead_len = (uint)((((bs->info.accum_steps * (bs->info.accum_steps + 1))) / 2) + 1);
|
|
bs->info.accum_read_ahead_bytes = bs->info.accum_read_ahead_len * stride;
|
|
#else
|
|
bs->info.accum_read_ahead_bytes = BCHUNK_HASH_LEN * stride;
|
|
#endif
|
|
|
|
bs->memory.chunk_list = BLI_mempool_create(sizeof(BChunkList), 0, 512, BLI_MEMPOOL_NOP);
|
|
bs->memory.chunk_ref = BLI_mempool_create(sizeof(BChunkRef), 0, 512, BLI_MEMPOOL_NOP);
|
|
/* allow iteration to simplify freeing, otherwise its not needed
|
|
* (we could loop over all states as an alternative). */
|
|
bs->memory.chunk = BLI_mempool_create(sizeof(BChunk), 0, 512, BLI_MEMPOOL_ALLOW_ITER);
|
|
|
|
return bs;
|
|
}
|
|
|
|
static void array_store_free_data(BArrayStore *bs)
|
|
{
|
|
/* free chunk data */
|
|
{
|
|
BLI_mempool_iter iter;
|
|
BChunk *chunk;
|
|
BLI_mempool_iternew(bs->memory.chunk, &iter);
|
|
while ((chunk = BLI_mempool_iterstep(&iter))) {
|
|
BLI_assert(chunk->users > 0);
|
|
MEM_freeN((void *)chunk->data);
|
|
}
|
|
}
|
|
|
|
/* free states */
|
|
for (BArrayState *state = bs->states.first, *state_next; state; state = state_next) {
|
|
state_next = state->next;
|
|
MEM_freeN(state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Free the #BArrayStore, including all states and chunks.
|
|
*/
|
|
void BLI_array_store_destroy(
|
|
BArrayStore *bs)
|
|
{
|
|
array_store_free_data(bs);
|
|
|
|
BLI_mempool_destroy(bs->memory.chunk_list);
|
|
BLI_mempool_destroy(bs->memory.chunk_ref);
|
|
BLI_mempool_destroy(bs->memory.chunk);
|
|
|
|
MEM_freeN(bs);
|
|
}
|
|
|
|
/**
|
|
* Clear all contents, allowing reuse of \a bs.
|
|
*/
|
|
void BLI_array_store_clear(
|
|
BArrayStore *bs)
|
|
{
|
|
array_store_free_data(bs);
|
|
|
|
BLI_listbase_clear(&bs->states);
|
|
|
|
BLI_mempool_clear(bs->memory.chunk_list);
|
|
BLI_mempool_clear(bs->memory.chunk_ref);
|
|
BLI_mempool_clear(bs->memory.chunk);
|
|
}
|
|
|
|
/** \name BArrayStore Statistics
|
|
* \{ */
|
|
|
|
/**
|
|
* \return the total amount of memory that would be used by getting the arrays for all states.
|
|
*/
|
|
size_t BLI_array_store_calc_size_expanded_get(
|
|
const BArrayStore *bs)
|
|
{
|
|
size_t size_accum = 0;
|
|
for (const BArrayState *state = bs->states.first; state; state = state->next) {
|
|
size_accum += state->chunk_list->total_size;
|
|
}
|
|
return size_accum;
|
|
}
|
|
|
|
/**
|
|
* \return the amount of memory used by all #BChunk.data
|
|
* (duplicate chunks are only counted once).
|
|
*/
|
|
size_t BLI_array_store_calc_size_compacted_get(
|
|
const BArrayStore *bs)
|
|
{
|
|
size_t size_total = 0;
|
|
BLI_mempool_iter iter;
|
|
BChunk *chunk;
|
|
BLI_mempool_iternew(bs->memory.chunk, &iter);
|
|
while ((chunk = BLI_mempool_iterstep(&iter))) {
|
|
BLI_assert(chunk->users > 0);
|
|
size_total += (size_t)chunk->data_len;
|
|
}
|
|
return size_total;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
|
|
/** \name BArrayState Access
|
|
* \{ */
|
|
|
|
/**
|
|
*
|
|
* \param data: Data used to create
|
|
* \param state_reference: The state to use as a reference when adding the new state,
|
|
* typically this is the previous state,
|
|
* however it can be any previously created state from this \a bs.
|
|
*
|
|
* \return The new state, which is used by the caller as a handle to get back the contents of \a data.
|
|
* This may be removed using #BLI_array_store_state_remove,
|
|
* otherwise it will be removed with #BLI_array_store_destroy.
|
|
*/
|
|
BArrayState *BLI_array_store_state_add(
|
|
BArrayStore *bs,
|
|
const void *data, const size_t data_len,
|
|
const BArrayState *state_reference)
|
|
{
|
|
/* ensure we're aligned to the stride */
|
|
BLI_assert((data_len % bs->info.chunk_stride) == 0);
|
|
|
|
#ifdef USE_PARANOID_CHECKS
|
|
if (state_reference) {
|
|
BLI_assert(BLI_findindex(&bs->states, state_reference) != -1);
|
|
}
|
|
#endif
|
|
|
|
BChunkList *chunk_list;
|
|
if (state_reference) {
|
|
chunk_list = bchunk_list_from_data_merge(
|
|
&bs->info, &bs->memory,
|
|
(const uchar *)data, data_len,
|
|
/* re-use reference chunks */
|
|
state_reference->chunk_list);
|
|
}
|
|
else {
|
|
chunk_list = bchunk_list_new(&bs->memory, data_len);
|
|
bchunk_list_fill_from_array(
|
|
&bs->info, &bs->memory,
|
|
chunk_list,
|
|
(const uchar *)data, data_len);
|
|
}
|
|
|
|
chunk_list->users += 1;
|
|
|
|
BArrayState *state = MEM_callocN(sizeof(BArrayState), __func__);
|
|
state->chunk_list = chunk_list;
|
|
|
|
BLI_addtail(&bs->states, state);
|
|
|
|
#ifdef USE_PARANOID_CHECKS
|
|
{
|
|
size_t data_test_len;
|
|
void *data_test = BLI_array_store_state_data_get_alloc(state, &data_test_len);
|
|
BLI_assert(data_test_len == data_len);
|
|
BLI_assert(memcmp(data_test, data, data_len) == 0);
|
|
MEM_freeN(data_test);
|
|
}
|
|
#endif
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Remove a state and free any unused #BChunk data.
|
|
*
|
|
* The states can be freed in any order.
|
|
*/
|
|
void BLI_array_store_state_remove(
|
|
BArrayStore *bs,
|
|
BArrayState *state)
|
|
{
|
|
#ifdef USE_PARANOID_CHECKS
|
|
BLI_assert(BLI_findindex(&bs->states, state) != -1);
|
|
#endif
|
|
|
|
bchunk_list_decref(&bs->memory, state->chunk_list);
|
|
BLI_remlink(&bs->states, state);
|
|
|
|
MEM_freeN(state);
|
|
}
|
|
|
|
/**
|
|
* \return the expanded size of the array,
|
|
* use this to know how much memory to allocate #BLI_array_store_state_data_get's argument.
|
|
*/
|
|
size_t BLI_array_store_state_size_get(
|
|
BArrayState *state)
|
|
{
|
|
return state->chunk_list->total_size;
|
|
}
|
|
|
|
/**
|
|
* Fill in existing allocated memory with the contents of \a state.
|
|
*/
|
|
void BLI_array_store_state_data_get(
|
|
BArrayState *state,
|
|
void *data)
|
|
{
|
|
#ifdef USE_PARANOID_CHECKS
|
|
size_t data_test_len = 0;
|
|
for (BChunkRef *cref = state->chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
data_test_len += cref->link->data_len;
|
|
}
|
|
BLI_assert(data_test_len == state->chunk_list->total_size);
|
|
#endif
|
|
|
|
uchar *data_step = (uchar *)data;
|
|
for (BChunkRef *cref = state->chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
BLI_assert(cref->link->users > 0);
|
|
memcpy(data_step, cref->link->data, cref->link->data_len);
|
|
data_step += cref->link->data_len;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allocate an array for \a state and return it.
|
|
*/
|
|
void *BLI_array_store_state_data_get_alloc(
|
|
BArrayState *state,
|
|
size_t *r_data_len)
|
|
{
|
|
void *data = MEM_mallocN(state->chunk_list->total_size, __func__);
|
|
BLI_array_store_state_data_get(state, data);
|
|
*r_data_len = state->chunk_list->total_size;
|
|
return data;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
|
|
/** \name Debugging API (for testing).
|
|
* \{ */
|
|
|
|
/* only for test validation */
|
|
static size_t bchunk_list_size(const BChunkList *chunk_list)
|
|
{
|
|
size_t total_size = 0;
|
|
for (BChunkRef *cref = chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
total_size += cref->link->data_len;
|
|
}
|
|
return total_size;
|
|
}
|
|
|
|
bool BLI_array_store_is_valid(
|
|
BArrayStore *bs)
|
|
{
|
|
bool ok = true;
|
|
|
|
/* Check Length
|
|
* ------------ */
|
|
|
|
for (BArrayState *state = bs->states.first; state; state = state->next) {
|
|
BChunkList *chunk_list = state->chunk_list;
|
|
if (!(bchunk_list_size(chunk_list) == chunk_list->total_size)) {
|
|
return false;
|
|
}
|
|
|
|
if (BLI_listbase_count(&chunk_list->chunk_refs) != (int)chunk_list->chunk_refs_len) {
|
|
return false;
|
|
}
|
|
|
|
#ifdef USE_MERGE_CHUNKS
|
|
/* ensure we merge all chunks that could be merged */
|
|
if (chunk_list->total_size > bs->info.chunk_byte_size_min) {
|
|
for (BChunkRef *cref = chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
if (cref->link->data_len < bs->info.chunk_byte_size_min) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
{
|
|
BLI_mempool_iter iter;
|
|
BChunk *chunk;
|
|
BLI_mempool_iternew(bs->memory.chunk, &iter);
|
|
while ((chunk = BLI_mempool_iterstep(&iter))) {
|
|
if (!(MEM_allocN_len(chunk->data) >= chunk->data_len)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Check User Count & Lost References
|
|
* ---------------------------------- */
|
|
{
|
|
GHashIterator gh_iter;
|
|
|
|
#define GHASH_PTR_ADD_USER(gh, pt) \
|
|
{ \
|
|
void **val; \
|
|
if (BLI_ghash_ensure_p((gh), (pt), &val)) { \
|
|
*((int *)val) += 1; \
|
|
} \
|
|
else { \
|
|
*((int *)val) = 1; \
|
|
} \
|
|
} ((void)0)
|
|
|
|
/* count chunk_list's */
|
|
GHash *chunk_list_map = BLI_ghash_ptr_new(__func__);
|
|
GHash *chunk_map = BLI_ghash_ptr_new(__func__);
|
|
|
|
int totrefs = 0;
|
|
for (BArrayState *state = bs->states.first; state; state = state->next) {
|
|
GHASH_PTR_ADD_USER(chunk_list_map, state->chunk_list);
|
|
}
|
|
GHASH_ITER (gh_iter, chunk_list_map) {
|
|
const struct BChunkList *chunk_list = BLI_ghashIterator_getKey(&gh_iter);
|
|
const int users = GET_INT_FROM_POINTER(BLI_ghashIterator_getValue(&gh_iter));
|
|
if (!(chunk_list->users == users)) {
|
|
ok = false;
|
|
goto user_finally;
|
|
}
|
|
}
|
|
if (!(BLI_mempool_count(bs->memory.chunk_list) == (int)BLI_ghash_size(chunk_list_map))) {
|
|
ok = false;
|
|
goto user_finally;
|
|
}
|
|
|
|
/* count chunk's */
|
|
GHASH_ITER (gh_iter, chunk_list_map) {
|
|
const struct BChunkList *chunk_list = BLI_ghashIterator_getKey(&gh_iter);
|
|
for (const BChunkRef *cref = chunk_list->chunk_refs.first; cref; cref = cref->next) {
|
|
GHASH_PTR_ADD_USER(chunk_map, cref->link);
|
|
totrefs += 1;
|
|
}
|
|
}
|
|
if (!(BLI_mempool_count(bs->memory.chunk) == (int)BLI_ghash_size(chunk_map))) {
|
|
ok = false;
|
|
goto user_finally;
|
|
}
|
|
if (!(BLI_mempool_count(bs->memory.chunk_ref) == totrefs)) {
|
|
ok = false;
|
|
goto user_finally;
|
|
}
|
|
|
|
GHASH_ITER (gh_iter, chunk_map) {
|
|
const struct BChunk *chunk = BLI_ghashIterator_getKey(&gh_iter);
|
|
const int users = GET_INT_FROM_POINTER(BLI_ghashIterator_getValue(&gh_iter));
|
|
if (!(chunk->users == users)) {
|
|
ok = false;
|
|
goto user_finally;
|
|
}
|
|
}
|
|
|
|
#undef GHASH_PTR_ADD_USER
|
|
|
|
user_finally:
|
|
BLI_ghash_free(chunk_list_map, NULL, NULL);
|
|
BLI_ghash_free(chunk_map, NULL, NULL);
|
|
}
|
|
|
|
|
|
return ok;
|
|
/* TODO, dangling pointer checks */
|
|
}
|
|
|
|
/** \} */
|