1464286660
Not enabled by default, will test this in the future because it might optimize multithreaded query speed in worlds with sparse saves.
1066 lines
30 KiB
C++
1066 lines
30 KiB
C++
#include "voxel_stream_sqlite.h"
|
|
#include "../../thirdparty/sqlite/sqlite3.h"
|
|
#include "../../util/errors.h"
|
|
#include "../../util/godot/funcs.h"
|
|
#include "../../util/log.h"
|
|
#include "../../util/math/conv.h"
|
|
#include "../../util/profiling.h"
|
|
#include "../../util/string_funcs.h"
|
|
#include "../compressed_data.h"
|
|
|
|
#include <limits>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
|
|
namespace zylann::voxel {
|
|
|
|
struct BlockLocation {
|
|
int16_t x;
|
|
int16_t y;
|
|
int16_t z;
|
|
uint8_t lod;
|
|
|
|
static bool validate(const Vector3i pos, uint8_t lod) {
|
|
ZN_ASSERT_RETURN_V(can_convert_to_i16(pos), false);
|
|
ZN_ASSERT_RETURN_V(lod < constants::MAX_LOD, false);
|
|
return true;
|
|
}
|
|
|
|
uint64_t encode() const {
|
|
// 0l xx yy zz
|
|
// TODO Is this valid with negative numbers?
|
|
return ((static_cast<uint64_t>(lod) & 0xffff) << 48) | ((static_cast<uint64_t>(x) & 0xffff) << 32) |
|
|
((static_cast<uint64_t>(y) & 0xffff) << 16) | (static_cast<uint64_t>(z) & 0xffff);
|
|
}
|
|
|
|
static BlockLocation decode(uint64_t id) {
|
|
BlockLocation b;
|
|
b.z = (id & 0xffff);
|
|
b.y = ((id >> 16) & 0xffff);
|
|
b.x = ((id >> 32) & 0xffff);
|
|
b.lod = ((id >> 48) & 0xff);
|
|
return b;
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// One connection to the database, with our prepared statements
|
|
class VoxelStreamSQLiteInternal {
|
|
public:
|
|
static const int VERSION = 0;
|
|
|
|
struct Meta {
|
|
int version = -1;
|
|
int block_size_po2 = 0;
|
|
|
|
struct Channel {
|
|
VoxelBufferInternal::Depth depth;
|
|
bool used = false;
|
|
};
|
|
|
|
FixedArray<Channel, VoxelBufferInternal::MAX_CHANNELS> channels;
|
|
};
|
|
|
|
enum BlockType { //
|
|
VOXELS,
|
|
INSTANCES
|
|
};
|
|
|
|
VoxelStreamSQLiteInternal();
|
|
~VoxelStreamSQLiteInternal();
|
|
|
|
bool open(const char *fpath);
|
|
void close();
|
|
|
|
bool is_open() const {
|
|
return _db != nullptr;
|
|
}
|
|
|
|
// Returns the file path from SQLite
|
|
const char *get_file_path() const;
|
|
|
|
// Return the file path that was used to open the connection.
|
|
// You may use this one if you want determinism, as SQLite seems to globalize its path.
|
|
const char *get_opened_file_path() const {
|
|
return _opened_path.c_str();
|
|
}
|
|
|
|
bool begin_transaction();
|
|
bool end_transaction();
|
|
|
|
bool save_block(BlockLocation loc, const std::vector<uint8_t> &block_data, BlockType type);
|
|
VoxelStream::ResultCode load_block(BlockLocation loc, std::vector<uint8_t> &out_block_data, BlockType type);
|
|
|
|
bool load_all_blocks(void *callback_data,
|
|
void (*process_block_func)(void *callback_data, BlockLocation location, Span<const uint8_t> voxel_data,
|
|
Span<const uint8_t> instances_data));
|
|
|
|
bool load_all_block_keys(
|
|
void *callback_data, void (*process_block_func)(void *callback_data, BlockLocation location));
|
|
|
|
Meta load_meta();
|
|
void save_meta(Meta meta);
|
|
|
|
private:
|
|
struct TransactionScope {
|
|
VoxelStreamSQLiteInternal &db;
|
|
TransactionScope(VoxelStreamSQLiteInternal &p_db) : db(p_db) {
|
|
db.begin_transaction();
|
|
}
|
|
~TransactionScope() {
|
|
db.end_transaction();
|
|
}
|
|
};
|
|
|
|
static bool prepare(sqlite3 *db, sqlite3_stmt **s, const char *sql) {
|
|
const int rc = sqlite3_prepare_v2(db, sql, -1, s, nullptr);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(String("Preparing statement failed: {0}").format(varray(sqlite3_errmsg(db))));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void finalize(sqlite3_stmt *&s) {
|
|
if (s != nullptr) {
|
|
sqlite3_finalize(s);
|
|
s = nullptr;
|
|
}
|
|
}
|
|
|
|
std::string _opened_path;
|
|
sqlite3 *_db = nullptr;
|
|
sqlite3_stmt *_begin_statement = nullptr;
|
|
sqlite3_stmt *_end_statement = nullptr;
|
|
sqlite3_stmt *_update_voxel_block_statement = nullptr;
|
|
sqlite3_stmt *_get_voxel_block_statement = nullptr;
|
|
sqlite3_stmt *_update_instance_block_statement = nullptr;
|
|
sqlite3_stmt *_get_instance_block_statement = nullptr;
|
|
sqlite3_stmt *_load_meta_statement = nullptr;
|
|
sqlite3_stmt *_save_meta_statement = nullptr;
|
|
sqlite3_stmt *_load_channels_statement = nullptr;
|
|
sqlite3_stmt *_save_channel_statement = nullptr;
|
|
sqlite3_stmt *_load_all_blocks_statement = nullptr;
|
|
sqlite3_stmt *_load_all_block_keys_statement = nullptr;
|
|
};
|
|
|
|
VoxelStreamSQLiteInternal::VoxelStreamSQLiteInternal() {}
|
|
|
|
VoxelStreamSQLiteInternal::~VoxelStreamSQLiteInternal() {
|
|
close();
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::open(const char *fpath) {
|
|
ZN_PROFILE_SCOPE();
|
|
close();
|
|
|
|
int rc = sqlite3_open_v2(fpath, &_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
|
|
if (rc != 0) {
|
|
ERR_PRINT(String("Could not open database: {0}").format(varray(sqlite3_errmsg(_db))));
|
|
close();
|
|
return false;
|
|
}
|
|
|
|
sqlite3 *db = _db;
|
|
char *error_message = nullptr;
|
|
|
|
// Create tables if they dont exist
|
|
const char *tables[3] = { "CREATE TABLE IF NOT EXISTS meta (version INTEGER, block_size_po2 INTEGER)",
|
|
"CREATE TABLE IF NOT EXISTS blocks (loc INTEGER PRIMARY KEY, vb BLOB, instances BLOB)",
|
|
"CREATE TABLE IF NOT EXISTS channels (idx INTEGER PRIMARY KEY, depth INTEGER)" };
|
|
for (size_t i = 0; i < 3; ++i) {
|
|
rc = sqlite3_exec(db, tables[i], nullptr, nullptr, &error_message);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(String("Failed to create table: {0}").format(varray(error_message)));
|
|
sqlite3_free(error_message);
|
|
close();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Prepare statements
|
|
if (!prepare(db, &_update_voxel_block_statement,
|
|
"INSERT INTO blocks VALUES (:loc, :vb, null) "
|
|
"ON CONFLICT(loc) DO UPDATE SET vb=excluded.vb")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_get_voxel_block_statement, "SELECT vb FROM blocks WHERE loc=:loc")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_update_instance_block_statement,
|
|
"INSERT INTO blocks VALUES (:loc, null, :instances) "
|
|
"ON CONFLICT(loc) DO UPDATE SET instances=excluded.instances")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_get_instance_block_statement, "SELECT instances FROM blocks WHERE loc=:loc")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_begin_statement, "BEGIN")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_end_statement, "END")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_load_meta_statement, "SELECT * FROM meta")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_save_meta_statement, "INSERT INTO meta VALUES (:version, :block_size_po2)")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_load_channels_statement, "SELECT * FROM channels")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_save_channel_statement,
|
|
"INSERT INTO channels VALUES (:idx, :depth) "
|
|
"ON CONFLICT(idx) DO UPDATE SET depth=excluded.depth")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_load_all_blocks_statement, "SELECT * FROM blocks")) {
|
|
return false;
|
|
}
|
|
if (!prepare(db, &_load_all_block_keys_statement, "SELECT loc FROM blocks")) {
|
|
return false;
|
|
}
|
|
|
|
// Is the database setup?
|
|
Meta meta = load_meta();
|
|
if (meta.version == -1) {
|
|
// Setup database
|
|
meta.version = VERSION;
|
|
// Defaults
|
|
meta.block_size_po2 = constants::DEFAULT_BLOCK_SIZE_PO2;
|
|
for (unsigned int i = 0; i < meta.channels.size(); ++i) {
|
|
Meta::Channel &channel = meta.channels[i];
|
|
channel.used = true;
|
|
channel.depth = VoxelBufferInternal::DEPTH_16_BIT;
|
|
}
|
|
save_meta(meta);
|
|
}
|
|
|
|
_opened_path = fpath;
|
|
return true;
|
|
}
|
|
|
|
void VoxelStreamSQLiteInternal::close() {
|
|
if (_db == nullptr) {
|
|
return;
|
|
}
|
|
finalize(_begin_statement);
|
|
finalize(_end_statement);
|
|
finalize(_update_voxel_block_statement);
|
|
finalize(_get_voxel_block_statement);
|
|
finalize(_update_instance_block_statement);
|
|
finalize(_get_instance_block_statement);
|
|
finalize(_load_meta_statement);
|
|
finalize(_save_meta_statement);
|
|
finalize(_load_channels_statement);
|
|
finalize(_save_channel_statement);
|
|
finalize(_load_all_blocks_statement);
|
|
finalize(_load_all_block_keys_statement);
|
|
sqlite3_close(_db);
|
|
_db = nullptr;
|
|
_opened_path.clear();
|
|
}
|
|
|
|
const char *VoxelStreamSQLiteInternal::get_file_path() const {
|
|
if (_db == nullptr) {
|
|
return nullptr;
|
|
}
|
|
return sqlite3_db_filename(_db, nullptr);
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::begin_transaction() {
|
|
int rc = sqlite3_reset(_begin_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
rc = sqlite3_step(_begin_statement);
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::end_transaction() {
|
|
int rc = sqlite3_reset(_end_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
rc = sqlite3_step(_end_statement);
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::save_block(BlockLocation loc, const std::vector<uint8_t> &block_data, BlockType type) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
sqlite3 *db = _db;
|
|
|
|
sqlite3_stmt *update_block_statement;
|
|
switch (type) {
|
|
case VOXELS:
|
|
update_block_statement = _update_voxel_block_statement;
|
|
break;
|
|
case INSTANCES:
|
|
update_block_statement = _update_instance_block_statement;
|
|
break;
|
|
default:
|
|
CRASH_NOW();
|
|
}
|
|
|
|
int rc = sqlite3_reset(update_block_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
const uint64_t eloc = loc.encode();
|
|
|
|
rc = sqlite3_bind_int64(update_block_statement, 1, eloc);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
if (block_data.size() == 0) {
|
|
rc = sqlite3_bind_null(update_block_statement, 2);
|
|
} else {
|
|
// We use SQLITE_TRANSIENT so SQLite will make its own copy of the data
|
|
rc = sqlite3_bind_blob(update_block_statement, 2, block_data.data(), block_data.size(), SQLITE_TRANSIENT);
|
|
}
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
rc = sqlite3_step(update_block_statement);
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VoxelStream::ResultCode VoxelStreamSQLiteInternal::load_block(
|
|
BlockLocation loc, std::vector<uint8_t> &out_block_data, BlockType type) {
|
|
sqlite3 *db = _db;
|
|
|
|
sqlite3_stmt *get_block_statement;
|
|
switch (type) {
|
|
case VOXELS:
|
|
get_block_statement = _get_voxel_block_statement;
|
|
break;
|
|
case INSTANCES:
|
|
get_block_statement = _get_instance_block_statement;
|
|
break;
|
|
default:
|
|
CRASH_NOW();
|
|
}
|
|
|
|
int rc;
|
|
|
|
rc = sqlite3_reset(get_block_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return VoxelStream::RESULT_ERROR;
|
|
}
|
|
|
|
const uint64_t eloc = loc.encode();
|
|
|
|
rc = sqlite3_bind_int64(get_block_statement, 1, eloc);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return VoxelStream::RESULT_ERROR;
|
|
}
|
|
|
|
VoxelStream::ResultCode result = VoxelStream::RESULT_BLOCK_NOT_FOUND;
|
|
|
|
while (true) {
|
|
rc = sqlite3_step(get_block_statement);
|
|
if (rc == SQLITE_ROW) {
|
|
const void *blob = sqlite3_column_blob(get_block_statement, 0);
|
|
//const uint8_t *b = reinterpret_cast<const uint8_t *>(blob);
|
|
const size_t blob_size = sqlite3_column_bytes(get_block_statement, 0);
|
|
if (blob_size != 0) {
|
|
result = VoxelStream::RESULT_BLOCK_FOUND;
|
|
out_block_data.resize(blob_size);
|
|
memcpy(out_block_data.data(), blob, blob_size);
|
|
}
|
|
// The query is still ongoing, we'll need to step one more time to complete it
|
|
continue;
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return VoxelStream::RESULT_ERROR;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::load_all_blocks(void *callback_data,
|
|
void (*process_block_func)(void *callback_data, BlockLocation location, Span<const uint8_t> voxel_data,
|
|
Span<const uint8_t> instances_data)) {
|
|
ZN_PROFILE_SCOPE();
|
|
CRASH_COND(process_block_func == nullptr);
|
|
|
|
sqlite3 *db = _db;
|
|
sqlite3_stmt *load_all_blocks_statement = _load_all_blocks_statement;
|
|
|
|
int rc;
|
|
|
|
rc = sqlite3_reset(load_all_blocks_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
while (true) {
|
|
rc = sqlite3_step(load_all_blocks_statement);
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
ZN_PROFILE_SCOPE_NAMED("Row");
|
|
|
|
const uint64_t eloc = sqlite3_column_int64(load_all_blocks_statement, 0);
|
|
const BlockLocation loc = BlockLocation::decode(eloc);
|
|
|
|
const void *voxels_blob = sqlite3_column_blob(load_all_blocks_statement, 1);
|
|
const size_t voxels_blob_size = sqlite3_column_bytes(load_all_blocks_statement, 1);
|
|
|
|
const void *instances_blob = sqlite3_column_blob(load_all_blocks_statement, 2);
|
|
const size_t instances_blob_size = sqlite3_column_bytes(load_all_blocks_statement, 2);
|
|
|
|
// Using a function pointer because returning a big list of a copy of all the blobs can
|
|
// waste a lot of temporary memory
|
|
process_block_func(callback_data, loc,
|
|
Span<const uint8_t>(reinterpret_cast<const uint8_t *>(voxels_blob), voxels_blob_size),
|
|
Span<const uint8_t>(reinterpret_cast<const uint8_t *>(instances_blob), instances_blob_size));
|
|
|
|
} else if (rc == SQLITE_DONE) {
|
|
break;
|
|
|
|
} else {
|
|
ERR_PRINT(String("Unexpected SQLite return code: {0}; errmsg: {1}").format(rc, sqlite3_errmsg(db)));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VoxelStreamSQLiteInternal::load_all_block_keys(
|
|
void *callback_data, void (*process_block_func)(void *callback_data, BlockLocation location)) {
|
|
ZN_PROFILE_SCOPE();
|
|
ZN_ASSERT(process_block_func != nullptr);
|
|
|
|
sqlite3 *db = _db;
|
|
sqlite3_stmt *load_all_block_keys_statement = _load_all_block_keys_statement;
|
|
|
|
int rc;
|
|
|
|
rc = sqlite3_reset(load_all_block_keys_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return false;
|
|
}
|
|
|
|
while (true) {
|
|
rc = sqlite3_step(load_all_block_keys_statement);
|
|
|
|
if (rc == SQLITE_ROW) {
|
|
ZN_PROFILE_SCOPE_NAMED("Row");
|
|
|
|
const uint64_t eloc = sqlite3_column_int64(load_all_block_keys_statement, 0);
|
|
const BlockLocation loc = BlockLocation::decode(eloc);
|
|
|
|
// Using a function pointer because returning a big list of a copy of all the blobs can
|
|
// waste a lot of temporary memory
|
|
process_block_func(callback_data, loc);
|
|
|
|
} else if (rc == SQLITE_DONE) {
|
|
break;
|
|
|
|
} else {
|
|
ERR_PRINT(String("Unexpected SQLite return code: {0}; errmsg: {1}").format(rc, sqlite3_errmsg(db)));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
VoxelStreamSQLiteInternal::Meta VoxelStreamSQLiteInternal::load_meta() {
|
|
sqlite3 *db = _db;
|
|
sqlite3_stmt *load_meta_statement = _load_meta_statement;
|
|
sqlite3_stmt *load_channels_statement = _load_channels_statement;
|
|
|
|
int rc = sqlite3_reset(load_meta_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return Meta();
|
|
}
|
|
|
|
TransactionScope transaction(*this);
|
|
|
|
Meta meta;
|
|
|
|
rc = sqlite3_step(load_meta_statement);
|
|
if (rc == SQLITE_ROW) {
|
|
meta.version = sqlite3_column_int(load_meta_statement, 0);
|
|
meta.block_size_po2 = sqlite3_column_int(load_meta_statement, 1);
|
|
// The query is still ongoing, we'll need to step one more time to complete it
|
|
rc = sqlite3_step(load_meta_statement);
|
|
|
|
} else if (rc == SQLITE_DONE) {
|
|
// There was no row. This database is probably not setup.
|
|
return Meta();
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return Meta();
|
|
}
|
|
|
|
rc = sqlite3_reset(load_channels_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return Meta();
|
|
}
|
|
|
|
while (true) {
|
|
rc = sqlite3_step(load_channels_statement);
|
|
if (rc == SQLITE_ROW) {
|
|
const int index = sqlite3_column_int(load_channels_statement, 0);
|
|
const int depth = sqlite3_column_int(load_channels_statement, 1);
|
|
if (index < 0 || index >= static_cast<int>(meta.channels.size())) {
|
|
ERR_PRINT(String("Channel index {0} is invalid").format(varray(index)));
|
|
continue;
|
|
}
|
|
if (depth < 0 || depth >= VoxelBufferInternal::DEPTH_COUNT) {
|
|
ERR_PRINT(String("Depth {0} is invalid").format(varray(depth)));
|
|
continue;
|
|
}
|
|
Meta::Channel &channel = meta.channels[index];
|
|
channel.used = true;
|
|
channel.depth = static_cast<VoxelBufferInternal::Depth>(depth);
|
|
continue;
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return Meta();
|
|
}
|
|
break;
|
|
}
|
|
|
|
return meta;
|
|
}
|
|
|
|
void VoxelStreamSQLiteInternal::save_meta(Meta meta) {
|
|
sqlite3 *db = _db;
|
|
sqlite3_stmt *save_meta_statement = _save_meta_statement;
|
|
sqlite3_stmt *save_channel_statement = _save_channel_statement;
|
|
|
|
int rc = sqlite3_reset(save_meta_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
|
|
TransactionScope transaction(*this);
|
|
|
|
rc = sqlite3_bind_int(save_meta_statement, 1, meta.version);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
rc = sqlite3_bind_int(save_meta_statement, 2, meta.block_size_po2);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
|
|
rc = sqlite3_step(save_meta_statement);
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
|
|
for (unsigned int channel_index = 0; channel_index < meta.channels.size(); ++channel_index) {
|
|
const Meta::Channel &channel = meta.channels[channel_index];
|
|
if (!channel.used) {
|
|
// TODO Remove rows for unused channels? Or have a `used` column?
|
|
continue;
|
|
}
|
|
|
|
rc = sqlite3_reset(save_channel_statement);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
|
|
rc = sqlite3_bind_int(save_channel_statement, 1, channel_index);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
rc = sqlite3_bind_int(save_channel_statement, 2, channel.depth);
|
|
if (rc != SQLITE_OK) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
|
|
rc = sqlite3_step(save_channel_statement);
|
|
if (rc != SQLITE_DONE) {
|
|
ERR_PRINT(sqlite3_errmsg(db));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
namespace {
|
|
thread_local std::vector<uint8_t> tls_temp_block_data;
|
|
thread_local std::vector<uint8_t> tls_temp_compressed_block_data;
|
|
} // namespace
|
|
|
|
VoxelStreamSQLite::VoxelStreamSQLite() {}
|
|
|
|
VoxelStreamSQLite::~VoxelStreamSQLite() {
|
|
ZN_PRINT_VERBOSE("~VoxelStreamSQLite");
|
|
if (!_connection_path.is_empty() && _cache.get_indicative_block_count() > 0) {
|
|
ZN_PRINT_VERBOSE("~VoxelStreamSQLite flushy flushy");
|
|
flush_cache();
|
|
ZN_PRINT_VERBOSE("~VoxelStreamSQLite flushy done");
|
|
}
|
|
for (auto it = _connection_pool.begin(); it != _connection_pool.end(); ++it) {
|
|
delete *it;
|
|
}
|
|
_connection_pool.clear();
|
|
ZN_PRINT_VERBOSE("~VoxelStreamSQLite done");
|
|
}
|
|
|
|
void VoxelStreamSQLite::set_database_path(String path) {
|
|
MutexLock lock(_connection_mutex);
|
|
if (path == _connection_path) {
|
|
return;
|
|
}
|
|
if (!_connection_path.is_empty() && _cache.get_indicative_block_count() > 0) {
|
|
// Save cached data before changing the path.
|
|
// Not using get_connection() because it locks.
|
|
VoxelStreamSQLiteInternal con;
|
|
CharString cpath = path.utf8();
|
|
// Note, the path could be invalid,
|
|
// Since Godot helpfully sets the property for every character typed in the inspector.
|
|
// So there can be lots of errors in the editor if you type it.
|
|
if (con.open(cpath)) {
|
|
flush_cache(&con);
|
|
}
|
|
}
|
|
for (auto it = _connection_pool.begin(); it != _connection_pool.end(); ++it) {
|
|
delete *it;
|
|
}
|
|
_block_keys_cache.clear();
|
|
_connection_pool.clear();
|
|
_connection_path = path;
|
|
// Don't actually open anything here. We'll do it only when necessary
|
|
}
|
|
|
|
String VoxelStreamSQLite::get_database_path() const {
|
|
MutexLock lock(_connection_mutex);
|
|
return _connection_path;
|
|
}
|
|
|
|
void VoxelStreamSQLite::load_voxel_block(VoxelStream::VoxelQueryData &q) {
|
|
load_voxel_blocks(Span<VoxelStream::VoxelQueryData>(&q, 1));
|
|
}
|
|
|
|
void VoxelStreamSQLite::save_voxel_block(VoxelStream::VoxelQueryData &q) {
|
|
save_voxel_blocks(Span<VoxelStream::VoxelQueryData>(&q, 1));
|
|
}
|
|
|
|
void VoxelStreamSQLite::load_voxel_blocks(Span<VoxelStream::VoxelQueryData> p_blocks) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
// TODO Get block size from database
|
|
const int bs_po2 = constants::DEFAULT_BLOCK_SIZE_PO2;
|
|
|
|
// Check the cache first
|
|
std::vector<unsigned int> blocks_to_load;
|
|
for (unsigned int i = 0; i < p_blocks.size(); ++i) {
|
|
VoxelStream::VoxelQueryData &q = p_blocks[i];
|
|
const Vector3i pos = q.origin_in_voxels >> (bs_po2 + q.lod);
|
|
|
|
ZN_ASSERT_CONTINUE(can_convert_to_i16(pos));
|
|
|
|
if (_block_keys_cache_enabled && !_block_keys_cache.contains(to_vec3i16(pos), q.lod)) {
|
|
q.result = RESULT_BLOCK_NOT_FOUND;
|
|
continue;
|
|
}
|
|
|
|
if (_cache.load_voxel_block(pos, q.lod, q.voxel_buffer)) {
|
|
q.result = RESULT_BLOCK_FOUND;
|
|
|
|
} else {
|
|
blocks_to_load.push_back(i);
|
|
}
|
|
}
|
|
|
|
if (blocks_to_load.size() == 0) {
|
|
// Everything was cached, no need to query the database
|
|
return;
|
|
}
|
|
|
|
VoxelStreamSQLiteInternal *con = get_connection();
|
|
ERR_FAIL_COND(con == nullptr);
|
|
|
|
// TODO We should handle busy return codes
|
|
ERR_FAIL_COND(con->begin_transaction() == false);
|
|
|
|
for (unsigned int i = 0; i < blocks_to_load.size(); ++i) {
|
|
const unsigned int ri = blocks_to_load[i];
|
|
VoxelStream::VoxelQueryData &q = p_blocks[ri];
|
|
|
|
const unsigned int po2 = bs_po2 + q.lod;
|
|
|
|
BlockLocation loc;
|
|
loc.x = q.origin_in_voxels.x >> po2;
|
|
loc.y = q.origin_in_voxels.y >> po2;
|
|
loc.z = q.origin_in_voxels.z >> po2;
|
|
loc.lod = q.lod;
|
|
|
|
const ResultCode res = con->load_block(loc, tls_temp_block_data, VoxelStreamSQLiteInternal::VOXELS);
|
|
|
|
if (res == RESULT_BLOCK_FOUND) {
|
|
// TODO Not sure if we should actually expect non-null. There can be legit not found blocks.
|
|
BlockSerializer::decompress_and_deserialize(to_span_const(tls_temp_block_data), q.voxel_buffer);
|
|
}
|
|
|
|
q.result = res;
|
|
}
|
|
|
|
ERR_FAIL_COND(con->end_transaction() == false);
|
|
|
|
recycle_connection(con);
|
|
}
|
|
|
|
void VoxelStreamSQLite::save_voxel_blocks(Span<VoxelStream::VoxelQueryData> p_blocks) {
|
|
// TODO Get block size from database
|
|
const int bs_po2 = constants::DEFAULT_BLOCK_SIZE_PO2;
|
|
|
|
// First put in cache
|
|
for (unsigned int i = 0; i < p_blocks.size(); ++i) {
|
|
VoxelStream::VoxelQueryData &q = p_blocks[i];
|
|
const Vector3i pos = q.origin_in_voxels >> (bs_po2 + q.lod);
|
|
|
|
if (!BlockLocation::validate(pos, q.lod)) {
|
|
ERR_PRINT(String("Block position {0} is outside of supported range").format(varray(pos)));
|
|
continue;
|
|
}
|
|
|
|
_cache.save_voxel_block(pos, q.lod, q.voxel_buffer);
|
|
if (_block_keys_cache_enabled) {
|
|
_block_keys_cache.add(to_vec3i16(pos), q.lod);
|
|
}
|
|
}
|
|
|
|
// TODO We should consider using a serialized cache, and measure the threshold in bytes
|
|
if (_cache.get_indicative_block_count() >= CACHE_SIZE) {
|
|
flush_cache();
|
|
}
|
|
}
|
|
|
|
bool VoxelStreamSQLite::supports_instance_blocks() const {
|
|
return true;
|
|
}
|
|
|
|
void VoxelStreamSQLite::load_instance_blocks(Span<VoxelStream::InstancesQueryData> out_blocks) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
// TODO Get block size from database
|
|
//const int bs_po2 = constants::DEFAULT_BLOCK_SIZE_PO2;
|
|
|
|
// Check the cache first
|
|
std::vector<unsigned int> blocks_to_load;
|
|
for (size_t i = 0; i < out_blocks.size(); ++i) {
|
|
VoxelStream::InstancesQueryData &q = out_blocks[i];
|
|
|
|
if (_cache.load_instance_block(q.position, q.lod, q.data)) {
|
|
q.result = RESULT_BLOCK_FOUND;
|
|
|
|
} else {
|
|
blocks_to_load.push_back(i);
|
|
}
|
|
}
|
|
|
|
if (blocks_to_load.size() == 0) {
|
|
// Everything was cached, no need to query the database
|
|
return;
|
|
}
|
|
|
|
VoxelStreamSQLiteInternal *con = get_connection();
|
|
ERR_FAIL_COND(con == nullptr);
|
|
|
|
// TODO We should handle busy return codes
|
|
// TODO recycle on error
|
|
ERR_FAIL_COND(con->begin_transaction() == false);
|
|
|
|
for (unsigned int i = 0; i < blocks_to_load.size(); ++i) {
|
|
const unsigned int ri = blocks_to_load[i];
|
|
VoxelStream::InstancesQueryData &q = out_blocks[ri];
|
|
|
|
BlockLocation loc;
|
|
loc.x = q.position.x;
|
|
loc.y = q.position.y;
|
|
loc.z = q.position.z;
|
|
loc.lod = q.lod;
|
|
|
|
const ResultCode res =
|
|
con->load_block(loc, tls_temp_compressed_block_data, VoxelStreamSQLiteInternal::INSTANCES);
|
|
|
|
if (res == RESULT_BLOCK_FOUND) {
|
|
if (!CompressedData::decompress(to_span_const(tls_temp_compressed_block_data), tls_temp_block_data)) {
|
|
ERR_PRINT("Failed to decompress instance block");
|
|
q.result = RESULT_ERROR;
|
|
continue;
|
|
}
|
|
q.data = make_unique_instance<InstanceBlockData>();
|
|
if (!deserialize_instance_block_data(*q.data, to_span_const(tls_temp_block_data))) {
|
|
ERR_PRINT("Failed to deserialize instance block");
|
|
q.result = RESULT_ERROR;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
q.result = res;
|
|
}
|
|
|
|
ERR_FAIL_COND(con->end_transaction() == false);
|
|
|
|
recycle_connection(con);
|
|
}
|
|
|
|
void VoxelStreamSQLite::save_instance_blocks(Span<VoxelStream::InstancesQueryData> p_blocks) {
|
|
// TODO Get block size from database
|
|
//const int bs_po2 = constants::DEFAULT_BLOCK_SIZE_PO2;
|
|
|
|
// First put in cache
|
|
for (size_t i = 0; i < p_blocks.size(); ++i) {
|
|
VoxelStream::InstancesQueryData &q = p_blocks[i];
|
|
|
|
if (!BlockLocation::validate(q.position, q.lod)) {
|
|
ZN_PRINT_ERROR("Instance block position {} is outside of supported range", q.position);
|
|
continue;
|
|
}
|
|
|
|
_cache.save_instance_block(q.position, q.lod, std::move(q.data));
|
|
if (_block_keys_cache_enabled) {
|
|
_block_keys_cache.add(to_vec3i16(q.position), q.lod);
|
|
}
|
|
}
|
|
|
|
// TODO Optimization: we should consider using a serialized cache, and measure the threshold in bytes
|
|
if (_cache.get_indicative_block_count() >= CACHE_SIZE) {
|
|
flush_cache();
|
|
}
|
|
}
|
|
|
|
void VoxelStreamSQLite::load_all_blocks(FullLoadingResult &result) {
|
|
ZN_PROFILE_SCOPE();
|
|
|
|
VoxelStreamSQLiteInternal *con = get_connection();
|
|
ERR_FAIL_COND(con == nullptr);
|
|
|
|
struct Context {
|
|
FullLoadingResult &result;
|
|
};
|
|
|
|
// Using local function instead of a lambda for quite stupid reason admittedly:
|
|
// Godot's clang-format does not allow to write function parameters in column,
|
|
// which makes the lambda break line length.
|
|
struct L {
|
|
static void process_block_func(void *callback_data, const BlockLocation location,
|
|
Span<const uint8_t> voxel_data, Span<const uint8_t> instances_data) {
|
|
Context *ctx = reinterpret_cast<Context *>(callback_data);
|
|
|
|
if (voxel_data.size() == 0 && instances_data.size() == 0) {
|
|
ZN_PRINT_VERBOSE(format("Unexpected empty voxel data and instances data at {} lod {}",
|
|
Vector3i(location.x, location.y, location.z), location.lod));
|
|
return;
|
|
}
|
|
|
|
FullLoadingResult::Block result_block;
|
|
result_block.position = Vector3i(location.x, location.y, location.z);
|
|
result_block.lod = location.lod;
|
|
|
|
if (voxel_data.size() > 0) {
|
|
std::shared_ptr<VoxelBufferInternal> voxels = make_shared_instance<VoxelBufferInternal>();
|
|
ERR_FAIL_COND(!BlockSerializer::decompress_and_deserialize(voxel_data, *voxels));
|
|
result_block.voxels = voxels;
|
|
}
|
|
|
|
if (instances_data.size() > 0) {
|
|
std::vector<uint8_t> &temp_block_data = tls_temp_block_data;
|
|
if (!CompressedData::decompress(instances_data, temp_block_data)) {
|
|
ERR_PRINT("Failed to decompress instance block");
|
|
return;
|
|
}
|
|
result_block.instances_data = make_unique_instance<InstanceBlockData>();
|
|
if (!deserialize_instance_block_data(*result_block.instances_data, to_span_const(temp_block_data))) {
|
|
ERR_PRINT("Failed to deserialize instance block");
|
|
return;
|
|
}
|
|
}
|
|
|
|
ctx->result.blocks.push_back(std::move(result_block));
|
|
}
|
|
};
|
|
|
|
// Had to suffix `_outer`,
|
|
// because otherwise GCC thinks it shadows a variable inside the local function/captureless lambda
|
|
Context ctx_outer{ result };
|
|
const bool request_result = con->load_all_blocks(&ctx_outer, L::process_block_func);
|
|
ERR_FAIL_COND(request_result == false);
|
|
}
|
|
|
|
int VoxelStreamSQLite::get_used_channels_mask() const {
|
|
// Assuming all, since that stream can store anything.
|
|
return VoxelBufferInternal::ALL_CHANNELS_MASK;
|
|
}
|
|
|
|
void VoxelStreamSQLite::flush_cache() {
|
|
VoxelStreamSQLiteInternal *con = get_connection();
|
|
ERR_FAIL_COND(con == nullptr);
|
|
flush_cache(con);
|
|
recycle_connection(con);
|
|
}
|
|
|
|
// This function does not lock any mutex for internal use.
|
|
void VoxelStreamSQLite::flush_cache(VoxelStreamSQLiteInternal *con) {
|
|
ZN_PROFILE_SCOPE();
|
|
ZN_PRINT_VERBOSE(format("VoxelStreamSQLite: Flushing cache ({} elements)", _cache.get_indicative_block_count()));
|
|
|
|
ERR_FAIL_COND(con == nullptr);
|
|
ERR_FAIL_COND(con->begin_transaction() == false);
|
|
|
|
std::vector<uint8_t> &temp_data = tls_temp_block_data;
|
|
std::vector<uint8_t> &temp_compressed_data = tls_temp_compressed_block_data;
|
|
|
|
// TODO Needs better error rollback handling
|
|
_cache.flush([con, &temp_data, &temp_compressed_data](VoxelStreamCache::Block &block) {
|
|
ERR_FAIL_COND(!BlockLocation::validate(block.position, block.lod));
|
|
|
|
BlockLocation loc;
|
|
loc.x = block.position.x;
|
|
loc.y = block.position.y;
|
|
loc.z = block.position.z;
|
|
loc.lod = block.lod;
|
|
|
|
// Save voxels
|
|
if (block.has_voxels) {
|
|
if (block.voxels_deleted) {
|
|
const std::vector<uint8_t> empty;
|
|
con->save_block(loc, empty, VoxelStreamSQLiteInternal::VOXELS);
|
|
} else {
|
|
BlockSerializer::SerializeResult res = BlockSerializer::serialize_and_compress(block.voxels);
|
|
ERR_FAIL_COND(!res.success);
|
|
con->save_block(loc, res.data, VoxelStreamSQLiteInternal::VOXELS);
|
|
}
|
|
}
|
|
|
|
// Save instances
|
|
temp_compressed_data.clear();
|
|
if (block.instances != nullptr) {
|
|
temp_data.clear();
|
|
|
|
ERR_FAIL_COND(!serialize_instance_block_data(*block.instances, temp_data));
|
|
|
|
ERR_FAIL_COND(!CompressedData::compress(
|
|
to_span_const(temp_data), temp_compressed_data, CompressedData::COMPRESSION_NONE));
|
|
}
|
|
con->save_block(loc, temp_compressed_data, VoxelStreamSQLiteInternal::INSTANCES);
|
|
|
|
// TODO Optimization: add a version of the query that can update both at once
|
|
});
|
|
|
|
ERR_FAIL_COND(con->end_transaction() == false);
|
|
}
|
|
|
|
VoxelStreamSQLiteInternal *VoxelStreamSQLite::get_connection() {
|
|
_connection_mutex.lock();
|
|
if (_connection_path.is_empty()) {
|
|
_connection_mutex.unlock();
|
|
return nullptr;
|
|
}
|
|
if (_connection_pool.size() != 0) {
|
|
VoxelStreamSQLiteInternal *s = _connection_pool.back();
|
|
_connection_pool.pop_back();
|
|
_connection_mutex.unlock();
|
|
return s;
|
|
}
|
|
// First connection we get since we set the database path
|
|
|
|
String fpath = _connection_path;
|
|
_connection_mutex.unlock();
|
|
|
|
if (fpath.is_empty()) {
|
|
return nullptr;
|
|
}
|
|
VoxelStreamSQLiteInternal *con = new VoxelStreamSQLiteInternal();
|
|
CharString fpath_utf8 = fpath.utf8();
|
|
if (!con->open(fpath_utf8)) {
|
|
delete con;
|
|
con = nullptr;
|
|
}
|
|
if (_block_keys_cache_enabled) {
|
|
RWLockWrite wlock(_block_keys_cache.rw_lock);
|
|
con->load_all_block_keys(&_block_keys_cache, [](void *ctx, BlockLocation loc) {
|
|
BlockKeysCache *cache = static_cast<BlockKeysCache *>(ctx);
|
|
cache->add_no_lock({ loc.x, loc.y, loc.z }, loc.lod);
|
|
});
|
|
}
|
|
return con;
|
|
}
|
|
|
|
void VoxelStreamSQLite::recycle_connection(VoxelStreamSQLiteInternal *con) {
|
|
String con_path = con->get_opened_file_path();
|
|
_connection_mutex.lock();
|
|
// If path differs, delete this connection
|
|
if (_connection_path != con_path) {
|
|
_connection_mutex.unlock();
|
|
delete con;
|
|
} else {
|
|
_connection_pool.push_back(con);
|
|
_connection_mutex.unlock();
|
|
}
|
|
}
|
|
|
|
void VoxelStreamSQLite::set_key_cache_enabled(bool enable) {
|
|
_block_keys_cache_enabled = enable;
|
|
}
|
|
|
|
bool VoxelStreamSQLite::is_key_cache_enabled() const {
|
|
return _block_keys_cache_enabled;
|
|
}
|
|
|
|
void VoxelStreamSQLite::_bind_methods() {
|
|
ClassDB::bind_method(D_METHOD("set_database_path", "path"), &VoxelStreamSQLite::set_database_path);
|
|
ClassDB::bind_method(D_METHOD("get_database_path"), &VoxelStreamSQLite::get_database_path);
|
|
|
|
ClassDB::bind_method(D_METHOD("set_key_cache_enabled", "enabled"), &VoxelStreamSQLite::set_key_cache_enabled);
|
|
ClassDB::bind_method(D_METHOD("is_key_cache_enabled"), &VoxelStreamSQLite::is_key_cache_enabled);
|
|
|
|
ADD_PROPERTY(PropertyInfo(Variant::STRING, "database_path", PROPERTY_HINT_FILE), "set_database_path",
|
|
"get_database_path");
|
|
}
|
|
|
|
} // namespace zylann::voxel
|