Extracted some functions from VoxelBuffer, optimized copy/paste a little

This commit is contained in:
Marc Gilleron 2021-05-24 23:23:50 +01:00
parent 0de2b1b9d0
commit 4cc47b9036
6 changed files with 348 additions and 160 deletions

View File

@ -63,6 +63,7 @@
Copies values from a channel's sub-region of another [VoxelBuffer] into the same channel for the current buffer, at a specific location. The depth formats must match.
If corners of the area represent a negative-size area, they will be sorted back.
If coordinates are entirely or partially out of bounds, they will be clipped automatically.
Copying across the same buffer to overlapping areas is not supported. You may use an intermediary buffer in this case.
</description>
</method>
<method name="copy_voxel_metadata_in_area">
@ -80,6 +81,7 @@
Copies per-voxel metadata from a sub-region of another [VoxelBuffer] into the the current buffer, at a specific location. Values will be a shallow copy.
If corners of the area represent a negative-size area, they will be sorted back.
If coordinates are entirely or partially out of bounds, they will be clipped automatically.
Copying across the same buffer to overlapping areas is not supported. You may use an intermediary buffer in this case.
</description>
</method>
<method name="create">

51
storage/funcs.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "funcs.h"
#include "../util/math/rect3i.h"
void copy_3d_region_zxy(
ArraySlice<uint8_t> dst, Vector3i dst_size, Vector3i dst_min,
ArraySlice<const uint8_t> src, Vector3i src_size, Vector3i src_min, Vector3i src_max,
size_t item_size) {
//
Vector3i::sort_min_max(src_min, src_max);
clip_copy_region(src_min, src_max, src_size, dst_min, dst_size);
const Vector3i area_size = src_max - src_min;
if (area_size.x <= 0 || area_size.y <= 0 || area_size.z <= 0) {
// Degenerate area, we'll not copy anything.
return;
}
#ifdef DEBUG_ENABLED
if (src.data() == dst.data()) {
ERR_FAIL_COND_MSG(
Rect3i::from_min_max(src_min, src_max).intersects(Rect3i::from_min_max(dst_min, dst_min + area_size)),
"Copy across the same buffer to an overlapping area is not supported");
}
ERR_FAIL_COND(area_size.volume() * item_size > dst.size());
ERR_FAIL_COND(area_size.volume() * item_size > src.size());
#endif
if (area_size == src_size && area_size == dst_size) {
// Copy everything
memcpy(dst.data(), src.data(), dst.size() * item_size);
} else {
// Copy area row by row:
// This offset is how much to move in order to advance by one row (row direction is Y),
// essentially doing y+1
const unsigned int src_row_offset = src_size.y * item_size;
const unsigned int dst_row_offset = dst_size.y * item_size;
Vector3i pos;
for (pos.z = 0; pos.z < area_size.z; ++pos.z) {
pos.x = 0;
unsigned int src_ri = Vector3i(src_min + pos).get_zxy_index(src_size) * item_size;
unsigned int dst_ri = Vector3i(dst_min + pos).get_zxy_index(dst_size) * item_size;
for (; pos.x < area_size.x; ++pos.x) {
// TODO Cast src and dst to `restrict` so the optimizer can assume adresses don't overlap,
// which might allow to write as a for loop (which may compile as a `memcpy`)?
memcpy(&dst[dst_ri], &src[src_ri], area_size.y * item_size);
src_ri += src_row_offset;
dst_ri += dst_row_offset;
}
}
}
}

139
storage/funcs.h Normal file
View File

@ -0,0 +1,139 @@
#ifndef VOXEL_STORAGE_FUNCS_H
#define VOXEL_STORAGE_FUNCS_H
#include "../util/array_slice.h"
#include "../util/math/vector3i.h"
inline void clip_copy_region_coord(int &src_min, int &src_max, const int src_size, int &dst_min, const int dst_size) {
// Clamp source and shrink destination for moved borders
if (src_min < 0) {
dst_min += -src_min;
src_min = 0;
}
if (src_max > src_size) {
src_max = src_size;
}
// Clamp destination and shrink source for moved borders
if (dst_min < 0) {
src_min += -dst_min;
dst_min = 0;
}
const int dst_w = src_max - src_min;
const int dst_max = dst_min + dst_w;
if (dst_max > dst_size) {
src_max -= dst_max - dst_size;
}
// It is possible the source has negative size at this point, which means there is nothing to copy.
// This must be checked by the caller.
}
// Clips coordinates that may be used to copy a sub-region of a 3D container into another 3D container.
// The result can have zero or negative size, so it must be checked before proceeding.
inline void clip_copy_region(
Vector3i &src_min, Vector3i &src_max, const Vector3i &src_size, Vector3i &dst_min, const Vector3i &dst_size) {
clip_copy_region_coord(src_min.x, src_max.x, src_size.x, dst_min.x, dst_size.x);
clip_copy_region_coord(src_min.y, src_max.y, src_size.y, dst_min.y, dst_size.y);
clip_copy_region_coord(src_min.z, src_max.z, src_size.z, dst_min.z, dst_size.z);
}
void copy_3d_region_zxy(
ArraySlice<uint8_t> dst, Vector3i dst_size, Vector3i dst_min,
ArraySlice<const uint8_t> src, Vector3i src_size, Vector3i src_min, Vector3i src_max,
size_t item_size);
template <typename T>
inline void copy_3d_region_zxy(
ArraySlice<T> dst, Vector3i dst_size, Vector3i dst_min,
ArraySlice<const T> src, Vector3i src_size, Vector3i src_min, Vector3i src_max) {
copy_3d_region_zxy(
dst.reinterpret_cast_to<uint8_t>(), dst_size, dst_min,
src.reinterpret_cast_to<const uint8_t>(), src_size, src_min, src_max,
sizeof(T));
}
template <typename T>
void fill_3d_region_zxy(ArraySlice<T> dst, Vector3i dst_size, Vector3i dst_min, Vector3i dst_max, const T value) {
Vector3i::sort_min_max(dst_min, dst_max);
dst_min.x = clamp(dst_min.x, 0, dst_size.x);
dst_min.y = clamp(dst_min.y, 0, dst_size.y);
dst_min.z = clamp(dst_min.z, 0, dst_size.z);
dst_max.x = clamp(dst_max.x, 0, dst_size.x);
dst_max.y = clamp(dst_max.y, 0, dst_size.y);
dst_max.z = clamp(dst_max.z, 0, dst_size.z);
const Vector3i area_size = src_max - src_min;
if (area_size.x <= 0 || area_size.y <= 0 || area_size.z <= 0) {
// Degenerate area, we'll not copy anything.
return;
}
#ifdef DEBUG_ENABLED
ERR_FAIL_COND(area_size.volume() > dst.size());
#endif
if (area_size == dst_size) {
for (unsigned int i = 0; i < dst.size(); ++i) {
dst[i] = value;
}
} else {
const unsigned int dst_row_offset = dst_size.y;
Vector3i pos;
for (pos.z = 0; pos.z < area_size.z; ++pos.z) {
unsigned int dst_ri = Vector3i(dst_min + pos).get_zxy_index(src_size);
for (pos.x = 0; pos.x < area_size.x; ++pos.x) {
// Fill row
for (pos.y = 0; pos.y < area_size.y; ++pos.y) {
dst[dst_ri + pos.y] = value;
}
dst_ri += dst_row_offset;
}
}
}
}
inline FixedArray<uint8_t, 4> decode_weights_from_packed_u16(uint16_t packed_weights) {
FixedArray<uint8_t, 4> weights;
// SIMDable?
// weights[0] = ((packed_weights >> 0) & 0x0f) << 4;
// weights[1] = ((packed_weights >> 4) & 0x0f) << 4;
// weights[2] = ((packed_weights >> 8) & 0x0f) << 4;
// weights[3] = ((packed_weights >> 12) & 0x0f) << 4;
// Reduced but not SIMDable
weights[0] = (packed_weights & 0x0f) << 4;
weights[1] = packed_weights & 0xf0;
weights[2] = (packed_weights >> 4) & 0xf0;
weights[3] = (packed_weights >> 8) & 0xf0;
return weights;
}
inline FixedArray<uint8_t, 4> decode_indices_from_packed_u16(uint16_t packed_indices) {
FixedArray<uint8_t, 4> indices;
// SIMDable?
indices[0] = (packed_indices >> 0) & 0x0f;
indices[1] = (packed_indices >> 4) & 0x0f;
indices[2] = (packed_indices >> 8) & 0x0f;
indices[3] = (packed_indices >> 12) & 0x0f;
return indices;
}
inline uint16_t encode_indices_to_packed_u16(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return (a & 0xf) | ((b & 0xf) << 4) | ((c & 0xf) << 8) | ((d & 0xf) << 12);
}
inline uint16_t encode_weights_to_packed_u16(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return (a >> 4) | ((b >> 4) << 4) | ((c >> 4) << 8) | ((d >> 4) << 12);
}
// Checks if there are no duplicate indices in any voxel
inline void debug_check_texture_indices(FixedArray<uint8_t, 4> indices) {
FixedArray<bool, 16> checked;
checked.fill(false);
for (int i = 0; i < indices.size(); ++i) {
unsigned int ti = indices[i];
CRASH_COND(checked[ti]);
checked[ti] = true;
}
}
#endif // VOXEL_STORAGE_FUNCS_H

View File

@ -33,6 +33,9 @@ inline void free_channel_data(uint8_t *data, uint32_t size) {
#endif
}
uint32_t g_depth_byte_counts[] = {
1, 2, 4, 8
};
uint32_t g_depth_bit_counts[] = {
8, 16, 32, 64
};
@ -48,6 +51,11 @@ inline uint32_t get_depth_bit_count(VoxelBuffer::Depth d) {
return g_depth_bit_counts[d];
}
inline uint32_t get_depth_byte_count(VoxelBuffer::Depth d) {
CRASH_COND(d < 0 || d >= VoxelBuffer::DEPTH_COUNT);
return g_depth_byte_counts[d];
}
inline uint64_t get_max_value_for_depth(VoxelBuffer::Depth d) {
CRASH_COND(d < 0 || d >= VoxelBuffer::DEPTH_COUNT);
return g_depth_max_values[d];
@ -345,11 +353,9 @@ void VoxelBuffer::fill_area(uint64_t defval, Vector3i min, Vector3i max, unsigne
ERR_FAIL_INDEX(channel_index, MAX_CHANNELS);
Vector3i::sort_min_max(min, max);
min.clamp_to(Vector3i(0, 0, 0), _size + Vector3i(1, 1, 1));
max.clamp_to(Vector3i(0, 0, 0), _size + Vector3i(1, 1, 1));
const Vector3i area_size = max - min;
if (area_size.x == 0 || area_size.y == 0 || area_size.z == 0) {
return;
}
@ -513,39 +519,9 @@ void VoxelBuffer::copy_from(const VoxelBuffer &other, unsigned int channel_index
channel.depth = other_channel.depth;
}
inline void clip_copy_region_coord(int &src_min, int &src_max, const int src_size, int &dst_min, const int dst_size) {
// Clamp source and shrink destination for moved borders
if (src_min < 0) {
dst_min += -src_min;
src_min = 0;
}
if (src_max > src_size) {
src_max = src_size;
}
// Clamp destination and shrink source for moved borders
if (dst_min < 0) {
src_min += -dst_min;
dst_min = 0;
}
const int dst_w = src_max - src_min;
const int dst_max = dst_min + dst_w;
if (dst_max > dst_size) {
src_max -= dst_max - dst_size;
}
// It is possible the source has negative size at this point, which means there is nothing to copy.
// This must be checked by the caller.
}
inline void clip_copy_region(
Vector3i &src_min, Vector3i &src_max, const Vector3i &src_size, Vector3i &dst_min, const Vector3i &dst_size) {
clip_copy_region_coord(src_min.x, src_max.x, src_size.x, dst_min.x, dst_size.x);
clip_copy_region_coord(src_min.y, src_max.y, src_size.y, dst_min.y, dst_size.y);
clip_copy_region_coord(src_min.z, src_max.z, src_size.z, dst_min.z, dst_size.z);
}
// TODO Disallow copying from overlapping areas of the same buffer
void VoxelBuffer::copy_from(const VoxelBuffer &other, Vector3i src_min, Vector3i src_max, Vector3i dst_min,
unsigned int channel_index) {
ERR_FAIL_INDEX(channel_index, MAX_CHANNELS);
Channel &channel = _channels[channel_index];
@ -558,74 +534,28 @@ void VoxelBuffer::copy_from(const VoxelBuffer &other, Vector3i src_min, Vector3i
return;
}
Vector3i::sort_min_max(src_min, src_max);
clip_copy_region(src_min, src_max, other._size, dst_min, _size);
const Vector3i area_size = src_max - src_min;
if (area_size.x <= 0 || area_size.y <= 0 || area_size.z <= 0) {
// Degenerate area, we'll not copy anything.
return;
}
if (area_size == _size && area_size == other._size) {
// Equivalent of full copy between two blocks of same size
copy_from(other, channel_index);
} else {
if (other_channel.data != nullptr) {
if (channel.data == nullptr) {
create_channel(channel_index, _size, channel.defval);
}
if (channel.depth == DEPTH_8_BIT) {
// Native format
// Copy row by row
Vector3i pos;
for (pos.z = 0; pos.z < area_size.z; ++pos.z) {
for (pos.x = 0; pos.x < area_size.x; ++pos.x) {
// Row direction is Y
const unsigned int src_ri =
other.get_index(pos.x + src_min.x, pos.y + src_min.y, pos.z + src_min.z);
const unsigned int dst_ri = get_index(pos.x + dst_min.x, pos.y + dst_min.y, pos.z + dst_min.z);
memcpy(&channel.data[dst_ri], &other_channel.data[src_ri], area_size.y * sizeof(uint8_t));
}
}
} else if (channel.depth == DEPTH_16_BIT) {
Vector3i pos;
for (pos.z = 0; pos.z < area_size.z; ++pos.z) {
for (pos.x = 0; pos.x < area_size.x; ++pos.x) {
const unsigned int src_ri =
2 * other.get_index(pos.x + src_min.x, pos.y + src_min.y, pos.z + src_min.z);
const unsigned int dst_ri =
2 * get_index(pos.x + dst_min.x, pos.y + dst_min.y, pos.z + dst_min.z);
memcpy(&channel.data[dst_ri], &other_channel.data[src_ri], area_size.y * sizeof(uint16_t));
}
}
} else {
VOXEL_PROFILE_SCOPE();
// TODO Optimized versions
Vector3i pos;
for (pos.z = 0; pos.z < area_size.z; ++pos.z) {
for (pos.x = 0; pos.x < area_size.x; ++pos.x) {
for (pos.y = 0; pos.y < area_size.y; ++pos.y) {
const uint64_t v = other.get_voxel(src_min + pos, channel_index);
set_voxel(v, dst_min + pos, channel_index);
}
}
}
}
} else if (channel.defval != other_channel.defval) {
if (channel.data == nullptr) {
create_channel(channel_index, _size, channel.defval);
}
fill_area(other_channel.defval, dst_min, dst_min + area_size, channel_index);
if (other_channel.data != nullptr) {
if (channel.data == nullptr) {
// Note, we do this even if the pasted data happens to be all the same value as our current channel.
// We assume that this case is not frequent enough to bother, and compression can happen later
create_channel(channel_index, _size, channel.defval);
}
const unsigned int item_size = get_depth_byte_count(channel.depth);
ArraySlice<const uint8_t> src(other_channel.data, other_channel.size_in_bytes);
ArraySlice<uint8_t> dst(channel.data, channel.size_in_bytes);
copy_3d_region_zxy(dst, _size, dst_min, src, other._size, src_min, src_max, item_size);
} else if (channel.defval != other_channel.defval) {
// This logic is still required due to how source and destination regions can be specified.
// The actual size of the destination area must be determined from the source area, after it has been clipped.
Vector3i::sort_min_max(src_min, src_max);
clip_copy_region(src_min, src_max, other._size, dst_min, _size);
const Vector3i area_size = src_max - src_min;
if (area_size.x <= 0 || area_size.y <= 0 || area_size.z <= 0) {
// Degenerate area, we'll not copy anything.
return;
}
fill_area(other_channel.defval, dst_min, dst_min + area_size, channel_index);
}
}

View File

@ -5,6 +5,7 @@
#include "../util/array_slice.h"
#include "../util/fixed_array.h"
#include "../util/math/rect3i.h"
#include "funcs.h"
#include <core/map.h>
#include <core/reference.h>
@ -74,6 +75,19 @@ public:
// Limit was made explicit for serialization reasons, and also because there must be a reasonable one
static const uint32_t MAX_SIZE = 65535;
struct Channel {
// Allocated when the channel is populated.
// Flat array, in order [z][x][y] because it allows faster vertical-wise access (the engine is Y-up).
uint8_t *data = nullptr;
// Default value when data is null
uint64_t defval = 0;
Depth depth = DEFAULT_CHANNEL_DEPTH;
uint32_t size_in_bytes = 0;
};
VoxelBuffer();
~VoxelBuffer();
@ -123,6 +137,55 @@ public:
void copy_from(const VoxelBuffer &other, Vector3i src_min, Vector3i src_max, Vector3i dst_min,
unsigned int channel_index);
// Copy a region from a box of values, passed as a raw array.
// `src_size` is the total 3D size of the source box.
// `src_min` and `src_max` are the sub-region of that box we want to copy.
// `dst_min` is the lower corner where we want the data to be copied into the destination.
template <typename T>
void copy_from(ArraySlice<const T> src, Vector3i src_size, Vector3i src_min, Vector3i src_max, Vector3i dst_min,
unsigned int channel_index) {
ERR_FAIL_INDEX(channel_index, MAX_CHANNELS);
const Channel &channel = _channels[channel_index];
#ifdef DEBUG_ENABLED
// Size of source and destination values must match
ERR_FAIL_COND(channel.depth != get_depth_from_size(sizeof(T)));
#endif
// This function always decompresses the destination.
// To keep it compressed, either check what you are about to copy,
// or schedule a recompression for later.
decompress_channel(channel_index);
ArraySlice<T> dst(static_cast<T *>(channel.data), channel.size_in_bytes / sizeof(T));
copy_3d_region_zxy<T>(dst, _size, dst_min, src, src_size, src_min, src_max);
}
// Copy a region of the data into a dense buffer.
// If the source is compressed, it is decompressed.
// `dst` is a raw array storing grid values in a box.
// `dst_size` is the total size of the box.
// `dst_min` is the lower corner of where we want the source data to be stored.
// `src_min` and `src_max` is the sub-region of the source we want to copy.
template <typename T>
void copy_to(ArraySlice<T> dst, Vector3i dst_size, Vector3i dst_min, Vector3i src_min, Vector3i src_max,
unsigned int channel_index) const {
ERR_FAIL_INDEX(channel_index, MAX_CHANNELS);
const Channel &channel = _channels[channel_index];
#ifdef DEBUG_ENABLED
// Size of source and destination values must match
ERR_FAIL_COND(channel.depth != get_depth_from_size(sizeof(T)));
#endif
if (channel.data == nullptr) {
fill_3d_region_zxy<T>(dst, dst_size, dst_min, dst_min + (src_max - src_min), channel.defval);
} else {
ArraySlice<const T> src(static_cast<const T *>(channel.data), channel.size_in_bytes / sizeof(T));
copy_3d_region_zxy<T>(dst, dst_size, dst_min, src, _size, src_min, src_max);
}
}
// Executes a read-write action on all cells of the provided box that intersect with this buffer.
// `action_func` receives a voxel value from the channel, and returns a modified value.
// if the returned value is different, it will be applied to the buffer.
@ -300,19 +363,6 @@ private:
void _b_copy_voxel_metadata_in_area(Ref<VoxelBuffer> src_buffer, Vector3 src_min_pos, Vector3 src_max_pos, Vector3 dst_pos);
private:
struct Channel {
// Allocated when the channel is populated.
// Flat array, in order [z][x][y] because it allows faster vertical-wise access (the engine is Y-up).
uint8_t *data = nullptr;
// Default value when data is null
uint64_t defval = 0;
Depth depth = DEFAULT_CHANNEL_DEPTH;
uint32_t size_in_bytes = 0;
};
// Each channel can store arbitary data.
// For example, you can decide to store colors (R, G, B, A), gameplay types (type, state, light) or both.
FixedArray<Channel, MAX_CHANNELS> _channels;
@ -326,53 +376,6 @@ private:
RWLock _rw_lock;
};
// TODO Maybe find a better place to put these functions other than global space
inline FixedArray<uint8_t, 4> decode_weights_from_packed_u16(uint16_t packed_weights) {
FixedArray<uint8_t, 4> weights;
// SIMDable?
// weights[0] = ((packed_weights >> 0) & 0x0f) << 4;
// weights[1] = ((packed_weights >> 4) & 0x0f) << 4;
// weights[2] = ((packed_weights >> 8) & 0x0f) << 4;
// weights[3] = ((packed_weights >> 12) & 0x0f) << 4;
// Reduced but not SIMDable
weights[0] = (packed_weights & 0x0f) << 4;
weights[1] = packed_weights & 0xf0;
weights[2] = (packed_weights >> 4) & 0xf0;
weights[3] = (packed_weights >> 8) & 0xf0;
return weights;
}
inline FixedArray<uint8_t, 4> decode_indices_from_packed_u16(uint16_t packed_indices) {
FixedArray<uint8_t, 4> indices;
// SIMDable?
indices[0] = (packed_indices >> 0) & 0x0f;
indices[1] = (packed_indices >> 4) & 0x0f;
indices[2] = (packed_indices >> 8) & 0x0f;
indices[3] = (packed_indices >> 12) & 0x0f;
return indices;
}
inline uint16_t encode_indices_to_packed_u16(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return (a & 0xf) | ((b & 0xf) << 4) | ((c & 0xf) << 8) | ((d & 0xf) << 12);
}
inline uint16_t encode_weights_to_packed_u16(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return (a >> 4) | ((b >> 4) << 4) | ((c >> 4) << 8) | ((d >> 4) << 12);
}
// Checks if there are no duplicate indices in any voxel
inline void debug_check_texture_indices(FixedArray<uint8_t, 4> indices) {
FixedArray<bool, 16> checked;
checked.fill(false);
for (int i = 0; i < indices.size(); ++i) {
unsigned int ti = indices[i];
CRASH_COND(checked[ti]);
checked[ti] = true;
}
}
inline void debug_check_texture_indices_packed_u16(const VoxelBuffer &voxels) {
for (int z = 0; z < voxels.get_size().z; ++z) {
for (int x = 0; x < voxels.get_size().x; ++x) {

View File

@ -208,6 +208,68 @@ void test_encode_weights_packed_u16() {
ERR_FAIL_COND(weights != decoded_weights);
}
void test_copy_3d_region_zxy() {
std::vector<uint16_t> src;
std::vector<uint16_t> dst;
const Vector3i src_size(8, 8, 8);
const Vector3i dst_size(3, 4, 5);
src.resize(src_size.volume(), 0);
dst.resize(src_size.volume(), 0);
for (unsigned int i = 0; i < src.size(); ++i) {
src[i] = i;
}
ArraySlice<const uint16_t> srcs = to_slice_const(src);
ArraySlice<uint16_t> dsts = to_slice(dst);
const Vector3i dst_min(0, 0, 0);
const Vector3i src_min(2, 1, 0);
const Vector3i src_max(5, 4, 3);
copy_3d_region_zxy(dsts, dst_size, dst_min, srcs, src_size, src_min, src_max);
/*for (pos.y = src_min.y; pos.y < src_max.y; ++pos.y) {
String s;
for (pos.x = src_min.x; pos.x < src_max.x; ++pos.x) {
const uint16_t v = srcs[pos.get_zxy_index(src_size)];
if (v < 10) {
s += String("{0} ").format(varray(v));
} else if (v < 100) {
s += String("{0} ").format(varray(v));
} else {
s += String("{0} ").format(varray(v));
}
}
print_line(s);
}
print_line("----");
const Vector3i dst_max = dst_min + (src_max - src_min);
pos = Vector3i();
for (pos.y = dst_min.y; pos.y < dst_max.y; ++pos.y) {
String s;
for (pos.x = dst_min.x; pos.x < dst_max.x; ++pos.x) {
const uint16_t v = dsts[pos.get_zxy_index(dst_size)];
if (v < 10) {
s += String("{0} ").format(varray(v));
} else if (v < 100) {
s += String("{0} ").format(varray(v));
} else {
s += String("{0} ").format(varray(v));
}
}
print_line(s);
}*/
Vector3i pos;
for (pos.z = src_min.z; pos.z < src_max.z; ++pos.z) {
for (pos.x = src_min.x; pos.x < src_max.x; ++pos.x) {
for (pos.y = src_min.y; pos.y < src_max.y; ++pos.y) {
const uint16_t srcv = srcs[pos.get_zxy_index(src_size)];
const uint16_t dstv = dsts[(pos - src_min + dst_min).get_zxy_index(dst_size)];
ERR_FAIL_COND(srcv != dstv);
}
}
}
}
#define VOXEL_TEST(fname) \
print_line(String("Running {0}").format(varray(#fname))); \
fname()
@ -220,6 +282,7 @@ void run_voxel_tests() {
VOXEL_TEST(test_voxel_data_map_paste_mask);
VOXEL_TEST(test_voxel_data_map_copy);
VOXEL_TEST(test_encode_weights_packed_u16);
VOXEL_TEST(test_copy_3d_region_zxy);
print_line("------------ Voxel tests end -------------");
}