Add support for Zstandard compression for .blend files

Compressing blendfiles can help save a lot of disk space, but the slowdown
while loading and saving is a major annoyance.
Currently Blender uses Zlib (aka gzip aka Deflate) for compression, but there
are now several more modern algorithms that outperform it in every way.

In this patch, I decided for Zstandard aka Zstd for several reasons:
- It is widely supported, both in other programs and libraries as well as in
  general-purpose compression utilities on Unix
- It is extremely flexible - spanning several orders of magnitude of
  compression speeds depending on the level setting.
- It is pretty much on the Pareto frontier for all of its configurations
  (meaning that no other algorithm is both faster and more efficient).

One downside of course is that older versions of Blender will not be able to
read these files, but one can always just re-save them without compression or
decompress the file manually with an external tool.

The implementation here saves additional metadata into the compressed file in
order to allow for efficient seeking when loading. This is standard-compliant
and will be ignored by other tools that support Zstd.
If the metadata is not present (e.g. because you manually compressed a .blend
file with another tool), Blender will fall back to sequential reading.

Saving is multithreaded to improve performance. Loading is currently not
multithreaded since it's not easy to predict the access patterns of the
loading code when seeking is supported.
In the future, we might want to look into making this more predictable or
disabling seeking for the main .blend file, which would then allow for
multiple background threads that decompress data ahead of time.

The compression level was chosen to get sizes comparable to previous versions
at much higher speeds. In the future, this could be exposed as an option.

Reviewed By: campbellbarton, brecht, mont29

Differential Revision: https://developer.blender.org/D5799
This commit is contained in:
Lukas Stockner 2021-08-21 03:15:31 +02:00
parent 2b170f16d6
commit 2ea66af742
14 changed files with 689 additions and 72 deletions

View File

@ -0,0 +1,66 @@
# - Find Zstd library
# Find the native Zstd includes and library
# This module defines
# ZSTD_INCLUDE_DIRS, where to find zstd.h, Set when
# ZSTD_INCLUDE_DIR is found.
# ZSTD_LIBRARIES, libraries to link against to use Zstd.
# ZSTD_ROOT_DIR, The base directory to search for Zstd.
# This can also be an environment variable.
# ZSTD_FOUND, If false, do not try to use Zstd.
#
# also defined, but not for general use are
# ZSTD_LIBRARY, where to find the Zstd library.
#=============================================================================
# Copyright 2019 Blender Foundation.
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# If ZSTD_ROOT_DIR was defined in the environment, use it.
IF(NOT ZSTD_ROOT_DIR AND NOT $ENV{ZSTD_ROOT_DIR} STREQUAL "")
SET(ZSTD_ROOT_DIR $ENV{ZSTD_ROOT_DIR})
ENDIF()
SET(_zstd_SEARCH_DIRS
${ZSTD_ROOT_DIR}
)
FIND_PATH(ZSTD_INCLUDE_DIR
NAMES
zstd.h
HINTS
${_zstd_SEARCH_DIRS}
PATH_SUFFIXES
include
)
FIND_LIBRARY(ZSTD_LIBRARY
NAMES
zstd
HINTS
${_zstd_SEARCH_DIRS}
PATH_SUFFIXES
lib64 lib
)
# handle the QUIETLY and REQUIRED arguments and set ZSTD_FOUND to TRUE if
# all listed variables are TRUE
INCLUDE(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Zstd DEFAULT_MSG
ZSTD_LIBRARY ZSTD_INCLUDE_DIR)
IF(ZSTD_FOUND)
SET(ZSTD_LIBRARIES ${ZSTD_LIBRARY})
SET(ZSTD_INCLUDE_DIRS ${ZSTD_INCLUDE_DIR})
ENDIF()
MARK_AS_ADVANCED(
ZSTD_INCLUDE_DIR
ZSTD_LIBRARY
)

View File

@ -441,6 +441,9 @@ if(WITH_HARU)
endif()
endif()
set(ZSTD_ROOT_DIR ${LIBDIR}/zstd)
find_package(Zstd REQUIRED)
if(EXISTS ${LIBDIR})
without_system_libs_end()
endif()

View File

@ -99,6 +99,7 @@ endif()
find_package_wrapper(JPEG REQUIRED)
find_package_wrapper(PNG REQUIRED)
find_package_wrapper(ZLIB REQUIRED)
find_package_wrapper(Zstd REQUIRED)
find_package_wrapper(Freetype REQUIRED)
if(WITH_PYTHON)

View File

@ -873,3 +873,6 @@ if(WITH_HARU)
set(WITH_HARU OFF)
endif()
endif()
set(ZSTD_INCLUDE_DIRS ${LIBDIR}/zstd/include)
set(ZSTD_LIBRARIES ${LIBDIR}/zstd/lib/zstd_static.lib)

View File

@ -168,6 +168,8 @@ size_t BLI_ungzip_file_to_mem_at_pos(void *buf, size_t len, FILE *file, size_t g
ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
bool BLI_file_magic_is_gzip(const char header[4]);
bool BLI_file_magic_is_zstd(const char header[4]);
size_t BLI_file_descriptor_size(int file) ATTR_WARN_UNUSED_RESULT;
size_t BLI_file_size(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();

View File

@ -61,7 +61,7 @@ typedef struct FileReader {
*
* If a FileReader is created, it has to be cleaned up and freed by calling
* its close() function unless another FileReader has taken ownership - for example,
* Gzip takes over the base FileReader and will clean it up when their clean() is called.
* Zstd and Gzip take over the base FileReader and will clean it up when their clean() is called.
*/
/* Create FileReader from raw file descriptor. */
@ -71,6 +71,8 @@ FileReader *BLI_filereader_new_mmap(int filedes) ATTR_WARN_UNUSED_RESULT;
/* Create FileReader from a region of memory. */
FileReader *BLI_filereader_new_memory(const void *data, size_t len) ATTR_WARN_UNUSED_RESULT
ATTR_NONNULL();
/* Create FileReader from applying Zstd decompression on an underlying file. */
FileReader *BLI_filereader_new_zstd(FileReader *base) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
/* Create FileReader from applying Gzip decompression on an underlying file. */
FileReader *BLI_filereader_new_gzip(FileReader *base) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();

View File

@ -31,6 +31,7 @@ set(INC
set(INC_SYS
${ZLIB_INCLUDE_DIRS}
${ZSTD_INCLUDE_DIRS}
${FREETYPE_INCLUDE_DIRS}
${GMP_INCLUDE_DIRS}
)
@ -78,6 +79,7 @@ set(SRC
intern/filereader_file.c
intern/filereader_gzip.c
intern/filereader_memory.c
intern/filereader_zstd.c
intern/fnmatch.c
intern/freetypefont.c
intern/gsqueue.c
@ -327,6 +329,7 @@ set(LIB
${FREETYPE_LIBRARY}
${ZLIB_LIBRARIES}
${ZSTD_LIBRARIES}
)
if(WITH_MEM_VALGRIND)

View File

@ -262,6 +262,33 @@ bool BLI_file_magic_is_gzip(const char header[4])
return header[0] == 0x1f && header[1] == 0x8b && header[2] == 0x08;
}
bool BLI_file_magic_is_zstd(const char header[4])
{
/* ZSTD files consist of concatenated frames, each either a Zstd frame or a skippable frame.
* Both types of frames start with a magic number: 0xFD2FB528 for Zstd frames and 0x184D2A5*
* for skippable frames, with the * being anything from 0 to F.
*
* To check whether a file is Zstd-compressed, we just check whether the first frame matches
* either. Seeking through the file until a Zstd frame is found would make things more
* complicated and the probability of a false positive is rather low anyways.
*
* Note that LZ4 uses a compatible format, so even though its compressed frames have a
* different magic number, a valid LZ4 file might also start with a skippable frame matching
* the second check here.
*
* For more details, see https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md
*/
uint32_t magic = *((uint32_t *)header);
if (magic == 0xFD2FB528) {
return true;
}
if ((magic >> 4) == 0x184D2A5) {
return true;
}
return false;
}
/**
* Returns true if the file with the specified name can be written.
* This implementation uses access(2), which makes the check according

View File

@ -0,0 +1,335 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2021 Blender Foundation
* All rights reserved.
*/
/** \file
* \ingroup bli
*/
#include <string.h>
#include <zstd.h>
#include "BLI_blenlib.h"
#include "BLI_endian_switch.h"
#include "BLI_filereader.h"
#include "BLI_math_base.h"
#include "MEM_guardedalloc.h"
typedef struct {
FileReader reader;
FileReader *base;
ZSTD_DCtx *ctx;
ZSTD_inBuffer in_buf;
size_t in_buf_max_size;
struct {
int num_frames;
size_t *compressed_ofs;
size_t *uncompressed_ofs;
char *cached_content;
int cached_frame;
} seek;
} ZstdReader;
static bool zstd_read_u32(FileReader *base, uint32_t *val)
{
if (base->read(base, val, sizeof(uint32_t)) != sizeof(uint32_t)) {
return false;
}
#ifdef __BIG_ENDIAN__
BLI_endian_switch_uint32(val);
#endif
return true;
}
static bool zstd_read_seek_table(ZstdReader *zstd)
{
FileReader *base = zstd->base;
/* The seek table frame is at the end of the file, so seek there
* and verify that there is enough data. */
if (base->seek(base, -4, SEEK_END) < 13) {
return false;
}
uint32_t magic;
if (!zstd_read_u32(base, &magic) || magic != 0x8F92EAB1) {
return false;
}
uint8_t flags;
if (base->seek(base, -5, SEEK_END) < 0 || base->read(base, &flags, 1) != 1) {
return false;
}
/* Bit 7 indicates checksums. Bits 5 and 6 must be zero. */
bool has_checksums = (flags & 0x80);
if (flags & 0x60) {
return false;
}
uint32_t num_frames;
if (base->seek(base, -9, SEEK_END) < 0 || !zstd_read_u32(base, &num_frames)) {
return false;
}
/* Each frame has either 2 or 3 uint32_t, and after that we have
* num_frames, flags and magic for another 9 bytes. */
uint32_t expected_frame_length = num_frames * (has_checksums ? 12 : 8) + 9;
/* The frame starts with another magic number and its length, but these
* two fields are not included when counting length. */
off64_t frame_start_ofs = 8 + expected_frame_length;
/* Sanity check: Before the start of the seek table frame,
* there must be num_frames frames, each of which at least 8 bytes long. */
off64_t seek_frame_start = base->seek(base, -frame_start_ofs, SEEK_END);
if (seek_frame_start < num_frames * 8) {
return false;
}
if (!zstd_read_u32(base, &magic) || magic != 0x184D2A5E) {
return false;
}
uint32_t frame_length;
if (!zstd_read_u32(base, &frame_length) || frame_length != expected_frame_length) {
return false;
}
zstd->seek.num_frames = num_frames;
zstd->seek.compressed_ofs = MEM_malloc_arrayN(num_frames + 1, sizeof(size_t), __func__);
zstd->seek.uncompressed_ofs = MEM_malloc_arrayN(num_frames + 1, sizeof(size_t), __func__);
size_t compressed_ofs = 0;
size_t uncompressed_ofs = 0;
for (int i = 0; i < num_frames; i++) {
uint32_t compressed_size, uncompressed_size;
if (!zstd_read_u32(base, &compressed_size) || !zstd_read_u32(base, &uncompressed_size)) {
break;
}
if (has_checksums && base->seek(base, 4, SEEK_CUR) < 0) {
break;
}
zstd->seek.compressed_ofs[i] = compressed_ofs;
zstd->seek.uncompressed_ofs[i] = uncompressed_ofs;
compressed_ofs += compressed_size;
uncompressed_ofs += uncompressed_size;
}
zstd->seek.compressed_ofs[num_frames] = compressed_ofs;
zstd->seek.uncompressed_ofs[num_frames] = uncompressed_ofs;
/* Seek to the end of the previous frame for the following BHead frame detection. */
if (seek_frame_start != compressed_ofs || base->seek(base, seek_frame_start, SEEK_SET) < 0) {
MEM_freeN(zstd->seek.compressed_ofs);
MEM_freeN(zstd->seek.uncompressed_ofs);
memset(&zstd->seek, 0, sizeof(zstd->seek));
return false;
}
zstd->seek.cached_frame = -1;
return true;
}
/* Find out which frame contains the given position in the uncompressed stream.
* Basically just bisection. */
static int zstd_frame_from_pos(ZstdReader *zstd, size_t pos)
{
int low = 0, high = zstd->seek.num_frames;
if (pos >= zstd->seek.uncompressed_ofs[zstd->seek.num_frames]) {
return -1;
}
while (low + 1 < high) {
int mid = low + ((high - low) >> 1);
if (zstd->seek.uncompressed_ofs[mid] <= pos) {
low = mid;
}
else {
high = mid;
}
}
return low;
}
/* Ensure that the currently loaded frame is the correct one. */
static const char *zstd_ensure_cache(ZstdReader *zstd, int frame)
{
if (zstd->seek.cached_frame == frame) {
/* Cached frame matches, so just return it. */
return zstd->seek.cached_content;
}
/* Cached frame doesn't match, so discard it and cache the wanted one onstead. */
MEM_SAFE_FREE(zstd->seek.cached_content);
size_t compressed_size = zstd->seek.compressed_ofs[frame + 1] - zstd->seek.compressed_ofs[frame];
size_t uncompressed_size = zstd->seek.uncompressed_ofs[frame + 1] -
zstd->seek.uncompressed_ofs[frame];
char *uncompressed_data = MEM_mallocN(uncompressed_size, __func__);
char *compressed_data = MEM_mallocN(compressed_size, __func__);
if (zstd->base->seek(zstd->base, zstd->seek.compressed_ofs[frame], SEEK_SET) < 0 ||
zstd->base->read(zstd->base, compressed_data, compressed_size) < compressed_size) {
MEM_freeN(compressed_data);
MEM_freeN(uncompressed_data);
return NULL;
}
size_t res = ZSTD_decompressDCtx(
zstd->ctx, uncompressed_data, uncompressed_size, compressed_data, compressed_size);
MEM_freeN(compressed_data);
if (ZSTD_isError(res) || res < uncompressed_size) {
MEM_freeN(uncompressed_data);
return NULL;
}
zstd->seek.cached_frame = frame;
zstd->seek.cached_content = uncompressed_data;
return uncompressed_data;
}
static ssize_t zstd_read_seekable(FileReader *reader, void *buffer, size_t size)
{
ZstdReader *zstd = (ZstdReader *)reader;
size_t end_offset = zstd->reader.offset + size, read_len = 0;
while (zstd->reader.offset < end_offset) {
int frame = zstd_frame_from_pos(zstd, zstd->reader.offset);
if (frame < 0) {
/* EOF is reached, so return as much as we can. */
break;
}
const char *framedata = zstd_ensure_cache(zstd, frame);
if (framedata == NULL) {
/* Error while reading the frame, so return as much as we can. */
break;
}
size_t frame_end_offset = min_zz(zstd->seek.uncompressed_ofs[frame + 1], end_offset);
size_t frame_read_len = frame_end_offset - zstd->reader.offset;
size_t offset_in_frame = zstd->reader.offset - zstd->seek.uncompressed_ofs[frame];
memcpy((char *)buffer + read_len, framedata + offset_in_frame, frame_read_len);
read_len += frame_read_len;
zstd->reader.offset = frame_end_offset;
}
return read_len;
}
static off64_t zstd_seek(FileReader *reader, off64_t offset, int whence)
{
ZstdReader *zstd = (ZstdReader *)reader;
off64_t new_pos;
if (whence == SEEK_SET) {
new_pos = offset;
}
else if (whence == SEEK_END) {
new_pos = zstd->seek.uncompressed_ofs[zstd->seek.num_frames] + offset;
}
else {
new_pos = zstd->reader.offset + offset;
}
if (new_pos < 0 || new_pos > zstd->seek.uncompressed_ofs[zstd->seek.num_frames]) {
return -1;
}
zstd->reader.offset = new_pos;
return zstd->reader.offset;
}
static ssize_t zstd_read(FileReader *reader, void *buffer, size_t size)
{
ZstdReader *zstd = (ZstdReader *)reader;
ZSTD_outBuffer output = {buffer, size, 0};
while (output.pos < output.size) {
if (zstd->in_buf.pos == zstd->in_buf.size) {
/* Ran out of buffered input data, read some more. */
zstd->in_buf.pos = 0;
ssize_t readsize = zstd->base->read(
zstd->base, (char *)zstd->in_buf.src, zstd->in_buf_max_size);
if (readsize > 0) {
/* We got some data, so mark the buffer as refilled. */
zstd->in_buf.size = readsize;
}
else {
/* The underlying file is EOF, so return as much as we can. */
break;
}
}
if (ZSTD_isError(ZSTD_decompressStream(zstd->ctx, &output, &zstd->in_buf))) {
break;
}
}
zstd->reader.offset += output.pos;
return output.pos;
}
static void zstd_close(FileReader *reader)
{
ZstdReader *zstd = (ZstdReader *)reader;
ZSTD_freeDCtx(zstd->ctx);
if (zstd->reader.seek) {
MEM_freeN(zstd->seek.uncompressed_ofs);
MEM_freeN(zstd->seek.compressed_ofs);
MEM_freeN(zstd->seek.cached_content);
}
else {
MEM_freeN((void *)zstd->in_buf.src);
}
zstd->base->close(zstd->base);
MEM_freeN(zstd);
}
FileReader *BLI_filereader_new_zstd(FileReader *base)
{
ZstdReader *zstd = MEM_callocN(sizeof(ZstdReader), __func__);
zstd->ctx = ZSTD_createDCtx();
zstd->base = base;
if (zstd_read_seek_table(zstd)) {
zstd->reader.read = zstd_read_seekable;
zstd->reader.seek = zstd_seek;
}
else {
zstd->reader.read = zstd_read;
zstd->reader.seek = NULL;
zstd->in_buf_max_size = ZSTD_DStreamInSize();
zstd->in_buf.src = MEM_mallocN(zstd->in_buf_max_size, "zstd in buf");
zstd->in_buf.size = zstd->in_buf_max_size;
/* This signals that the buffer has run out,
* which will make the read function refill it on the first call. */
zstd->in_buf.pos = zstd->in_buf_max_size;
}
zstd->reader.close = zstd_close;
return (FileReader *)zstd;
}

View File

@ -42,7 +42,7 @@ set(INC
)
set(INC_SYS
${ZLIB_INCLUDE_DIRS}
${ZSTD_INCLUDE_DIRS}
)
set(SRC

View File

@ -1233,6 +1233,12 @@ static FileData *blo_filedata_from_file_descriptor(const char *filepath,
rawfile = NULL; /* The Gzip FileReader takes ownership of `rawfile`. */
}
}
else if (BLI_file_magic_is_zstd(header)) {
file = BLI_filereader_new_zstd(rawfile);
if (file != NULL) {
rawfile = NULL; /* The Zstd FileReader takes ownership of `rawfile`. */
}
}
/* Clean up `rawfile` if it wasn't taken over. */
if (rawfile != NULL) {
@ -1309,6 +1315,9 @@ FileData *blo_filedata_from_memory(const void *mem, int memsize, BlendFileReadRe
if (BLI_file_magic_is_gzip(mem)) {
file = BLI_filereader_new_gzip(mem_file);
}
else if (BLI_file_magic_is_zstd(mem)) {
file = BLI_filereader_new_zstd(mem_file);
}
if (file == NULL) {
/* Compression initialization failed. */

View File

@ -101,7 +101,12 @@
#include "BLI_bitmap.h"
#include "BLI_blenlib.h"
#include "BLI_endian_defines.h"
#include "BLI_endian_switch.h"
#include "BLI_link_utils.h"
#include "BLI_linklist.h"
#include "BLI_math_base.h"
#include "BLI_mempool.h"
#include "BLI_threads.h"
#include "MEM_guardedalloc.h" /* MEM_freeN */
#include "BKE_blender_version.h"
@ -129,14 +134,21 @@
#include <errno.h>
#include <zstd.h>
/* Make preferences read-only. */
#define U (*((const UserDef *)&U))
/* ********* my write, buffered writing with minimum size chunks ************ */
/* Use optimal allocation since blocks of this size are kept in memory for undo. */
#define MYWRITE_BUFFER_SIZE (MEM_SIZE_OPTIMAL(1 << 17)) /* 128kb */
#define MYWRITE_MAX_CHUNK (MEM_SIZE_OPTIMAL(1 << 15)) /* ~32kb */
#define MEM_BUFFER_SIZE (MEM_SIZE_OPTIMAL(1 << 17)) /* 128kb */
#define MEM_CHUNK_SIZE (MEM_SIZE_OPTIMAL(1 << 15)) /* ~32kb */
#define ZSTD_BUFFER_SIZE (1 << 21) /* 2mb */
#define ZSTD_CHUNK_SIZE (1 << 20) /* 1mb */
#define ZSTD_COMPRESSION_LEVEL 3
/** Use if we want to store how many bytes have been written to the file. */
// #define USE_WRITE_DATA_LEN
@ -147,9 +159,16 @@
typedef enum {
WW_WRAP_NONE = 1,
WW_WRAP_ZLIB,
WW_WRAP_ZSTD,
} eWriteWrapType;
typedef struct ZstdFrame {
struct ZstdFrame *next, *prev;
uint32_t compressed_size;
uint32_t uncompressed_size;
} ZstdFrame;
typedef struct WriteWrap WriteWrap;
struct WriteWrap {
/* callbacks */
@ -161,15 +180,23 @@ struct WriteWrap {
bool use_buf;
/* internal */
union {
int file_handle;
gzFile gz_handle;
} _user_data;
int file_handle;
struct {
ListBase threadpool;
ListBase tasks;
ThreadMutex mutex;
ThreadCondition condition;
int next_frame;
int num_frames;
int level;
ListBase frames;
bool write_error;
} zstd;
};
/* none */
#define FILE_HANDLE(ww) (ww)->_user_data.file_handle
static bool ww_open_none(WriteWrap *ww, const char *filepath)
{
int file;
@ -177,7 +204,7 @@ static bool ww_open_none(WriteWrap *ww, const char *filepath)
file = BLI_open(filepath, O_BINARY + O_WRONLY + O_CREAT + O_TRUNC, 0666);
if (file != -1) {
FILE_HANDLE(ww) = file;
ww->file_handle = file;
return true;
}
@ -185,39 +212,170 @@ static bool ww_open_none(WriteWrap *ww, const char *filepath)
}
static bool ww_close_none(WriteWrap *ww)
{
return (close(FILE_HANDLE(ww)) != -1);
return (close(ww->file_handle) != -1);
}
static size_t ww_write_none(WriteWrap *ww, const char *buf, size_t buf_len)
{
return write(FILE_HANDLE(ww), buf, buf_len);
return write(ww->file_handle, buf, buf_len);
}
#undef FILE_HANDLE
/* zlib */
#define FILE_HANDLE(ww) (ww)->_user_data.gz_handle
/* zstd */
static bool ww_open_zlib(WriteWrap *ww, const char *filepath)
typedef struct {
struct ZstdWriteBlockTask *next, *prev;
void *data;
size_t size;
int frame_number;
WriteWrap *ww;
} ZstdWriteBlockTask;
static void *zstd_write_task(void *userdata)
{
gzFile file;
ZstdWriteBlockTask *task = userdata;
WriteWrap *ww = task->ww;
file = BLI_gzopen(filepath, "wb1");
size_t out_buf_len = ZSTD_compressBound(task->size);
void *out_buf = MEM_mallocN(out_buf_len, "Zstd out buffer");
size_t out_size = ZSTD_compress(
out_buf, out_buf_len, task->data, task->size, ZSTD_COMPRESSION_LEVEL);
if (file != Z_NULL) {
FILE_HANDLE(ww) = file;
return true;
MEM_freeN(task->data);
BLI_mutex_lock(&ww->zstd.mutex);
while (ww->zstd.next_frame != task->frame_number) {
BLI_condition_wait(&ww->zstd.condition, &ww->zstd.mutex);
}
return false;
if (ZSTD_isError(out_size)) {
ww->zstd.write_error = true;
}
else {
if (ww_write_none(ww, out_buf, out_size) == out_size) {
ZstdFrame *frameinfo = MEM_mallocN(sizeof(ZstdFrame), "zstd frameinfo");
frameinfo->uncompressed_size = task->size;
frameinfo->compressed_size = out_size;
BLI_addtail(&ww->zstd.frames, frameinfo);
}
else {
ww->zstd.write_error = true;
}
}
ww->zstd.next_frame++;
BLI_mutex_unlock(&ww->zstd.mutex);
BLI_condition_notify_all(&ww->zstd.condition);
MEM_freeN(out_buf);
return NULL;
}
static bool ww_close_zlib(WriteWrap *ww)
static bool ww_open_zstd(WriteWrap *ww, const char *filepath)
{
return (gzclose(FILE_HANDLE(ww)) == Z_OK);
if (!ww_open_none(ww, filepath)) {
return false;
}
/* Leave one thread open for the main writing logic, unless we only have one HW thread. */
int num_threads = max_ii(1, BLI_system_thread_count() - 1);
BLI_threadpool_init(&ww->zstd.threadpool, zstd_write_task, num_threads);
BLI_mutex_init(&ww->zstd.mutex);
BLI_condition_init(&ww->zstd.condition);
return true;
}
static size_t ww_write_zlib(WriteWrap *ww, const char *buf, size_t buf_len)
static void zstd_write_u32_le(WriteWrap *ww, uint32_t val)
{
return gzwrite(FILE_HANDLE(ww), buf, buf_len);
#ifdef __BIG_ENDIAN__
BLI_endian_switch_uint32(&val);
#endif
ww_write_none(ww, (char *)&val, sizeof(uint32_t));
}
/* In order to implement efficient seeking when reading the .blend, we append
* a skippable frame that encodes information about the other frames present
* in the file.
* The format here follows the upstream spec for seekable files:
* https://github.com/facebook/zstd/blob/master/contrib/seekable_format/zstd_seekable_compression_format.md
* If this information is not present in a file (e.g. if it was compressed
* with external tools), it can still be opened in Blender, but seeking will
* not be supported, so more memory might be needed. */
static void zstd_write_seekable_frames(WriteWrap *ww)
{
/* Write seek table header (magic number and frame size). */
zstd_write_u32_le(ww, 0x184D2A5E);
/* The actual frame number might not match ww->zstd.num_frames if there was a write error. */
const uint32_t num_frames = BLI_listbase_count(&ww->zstd.frames);
/* Each frame consists of two u32, so 8 bytes each.
* After the frames, a footer containing two u32 and one byte (9 bytes total) is written. */
const uint32_t frame_size = num_frames * 8 + 9;
zstd_write_u32_le(ww, frame_size);
/* Write seek table entries. */
LISTBASE_FOREACH (ZstdFrame *, frame, &ww->zstd.frames) {
zstd_write_u32_le(ww, frame->compressed_size);
zstd_write_u32_le(ww, frame->uncompressed_size);
}
/* Write seek table footer (number of frames, option flags and second magic number). */
zstd_write_u32_le(ww, num_frames);
const char flags = 0; /* We don't store checksums for each frame. */
ww_write_none(ww, &flags, 1);
zstd_write_u32_le(ww, 0x8F92EAB1);
}
static bool ww_close_zstd(WriteWrap *ww)
{
BLI_threadpool_end(&ww->zstd.threadpool);
BLI_freelistN(&ww->zstd.tasks);
BLI_mutex_end(&ww->zstd.mutex);
BLI_condition_end(&ww->zstd.condition);
zstd_write_seekable_frames(ww);
BLI_freelistN(&ww->zstd.frames);
return ww_close_none(ww) && !ww->zstd.write_error;
}
static size_t ww_write_zstd(WriteWrap *ww, const char *buf, size_t buf_len)
{
if (ww->zstd.write_error) {
return 0;
}
ZstdWriteBlockTask *task = MEM_mallocN(sizeof(ZstdWriteBlockTask), __func__);
task->data = MEM_mallocN(buf_len, __func__);
memcpy(task->data, buf, buf_len);
task->size = buf_len;
task->frame_number = ww->zstd.num_frames++;
task->ww = ww;
BLI_mutex_lock(&ww->zstd.mutex);
BLI_addtail(&ww->zstd.tasks, task);
/* If there's a free worker thread, just push the block into that thread.
* Otherwise, we wait for the earliest thread to finish.
* We look up the earliest thread while holding the mutex, but release it
* before joining the thread to prevent a deadlock. */
ZstdWriteBlockTask *first_task = ww->zstd.tasks.first;
BLI_mutex_unlock(&ww->zstd.mutex);
if (!BLI_available_threads(&ww->zstd.threadpool)) {
BLI_threadpool_remove(&ww->zstd.threadpool, first_task);
/* If the task list was empty before we pushed our task, there should
* always be a free thread. */
BLI_assert(first_task != task);
BLI_remlink(&ww->zstd.tasks, first_task);
MEM_freeN(first_task);
}
BLI_threadpool_insert(&ww->zstd.threadpool, task);
return buf_len;
}
#undef FILE_HANDLE
/* --- end compression types --- */
@ -226,11 +384,11 @@ static void ww_handle_init(eWriteWrapType ww_type, WriteWrap *r_ww)
memset(r_ww, 0, sizeof(*r_ww));
switch (ww_type) {
case WW_WRAP_ZLIB: {
r_ww->open = ww_open_zlib;
r_ww->close = ww_close_zlib;
r_ww->write = ww_write_zlib;
r_ww->use_buf = false;
case WW_WRAP_ZSTD: {
r_ww->open = ww_open_zstd;
r_ww->close = ww_close_zstd;
r_ww->write = ww_write_zstd;
r_ww->use_buf = true;
break;
}
default: {
@ -252,10 +410,17 @@ static void ww_handle_init(eWriteWrapType ww_type, WriteWrap *r_ww)
typedef struct {
const struct SDNA *sdna;
/** Use for file and memory writing (fixed size of #MYWRITE_BUFFER_SIZE). */
uchar *buf;
/** Number of bytes used in #WriteData.buf (flushed when exceeded). */
size_t buf_used_len;
struct {
/** Use for file and memory writing (size stored in max_size). */
uchar *buf;
/** Number of bytes used in #WriteData.buf (flushed when exceeded). */
size_t used_len;
/** Maximum size of the buffer. */
size_t max_size;
/** Threshold above which writes get their own chunk. */
size_t chunk_size;
} buffer;
#ifdef USE_WRITE_DATA_LEN
/** Total number of bytes written. */
@ -271,7 +436,7 @@ typedef struct {
bool use_memfile;
/**
* Wrap writing, so we can use zlib or
* Wrap writing, so we can use zstd or
* other compression types later, see: G_FILE_COMPRESS
* Will be NULL for UNDO.
*/
@ -291,7 +456,15 @@ static WriteData *writedata_new(WriteWrap *ww)
wd->ww = ww;
if ((ww == NULL) || (ww->use_buf)) {
wd->buf = MEM_mallocN(MYWRITE_BUFFER_SIZE, "wd->buf");
if (ww == NULL) {
wd->buffer.max_size = MEM_BUFFER_SIZE;
wd->buffer.chunk_size = MEM_CHUNK_SIZE;
}
else {
wd->buffer.max_size = ZSTD_BUFFER_SIZE;
wd->buffer.chunk_size = ZSTD_CHUNK_SIZE;
}
wd->buffer.buf = MEM_mallocN(wd->buffer.max_size, "wd->buffer.buf");
}
return wd;
@ -325,8 +498,8 @@ static void writedata_do_write(WriteData *wd, const void *mem, size_t memlen)
static void writedata_free(WriteData *wd)
{
if (wd->buf) {
MEM_freeN(wd->buf);
if (wd->buffer.buf) {
MEM_freeN(wd->buffer.buf);
}
MEM_freeN(wd);
}
@ -343,9 +516,9 @@ static void writedata_free(WriteData *wd)
*/
static void mywrite_flush(WriteData *wd)
{
if (wd->buf_used_len != 0) {
writedata_do_write(wd, wd->buf, wd->buf_used_len);
wd->buf_used_len = 0;
if (wd->buffer.used_len != 0) {
writedata_do_write(wd, wd->buffer.buf, wd->buffer.used_len);
wd->buffer.used_len = 0;
}
}
@ -369,20 +542,20 @@ static void mywrite(WriteData *wd, const void *adr, size_t len)
wd->write_len += len;
#endif
if (wd->buf == NULL) {
if (wd->buffer.buf == NULL) {
writedata_do_write(wd, adr, len);
}
else {
/* if we have a single big chunk, write existing data in
* buffer and write out big chunk in smaller pieces */
if (len > MYWRITE_MAX_CHUNK) {
if (wd->buf_used_len != 0) {
writedata_do_write(wd, wd->buf, wd->buf_used_len);
wd->buf_used_len = 0;
if (len > wd->buffer.chunk_size) {
if (wd->buffer.used_len != 0) {
writedata_do_write(wd, wd->buffer.buf, wd->buffer.used_len);
wd->buffer.used_len = 0;
}
do {
size_t writelen = MIN2(len, MYWRITE_MAX_CHUNK);
size_t writelen = MIN2(len, wd->buffer.chunk_size);
writedata_do_write(wd, adr, writelen);
adr = (const char *)adr + writelen;
len -= writelen;
@ -392,14 +565,14 @@ static void mywrite(WriteData *wd, const void *adr, size_t len)
}
/* if data would overflow buffer, write out the buffer */
if (len + wd->buf_used_len > MYWRITE_BUFFER_SIZE - 1) {
writedata_do_write(wd, wd->buf, wd->buf_used_len);
wd->buf_used_len = 0;
if (len + wd->buffer.used_len > wd->buffer.max_size - 1) {
writedata_do_write(wd, wd->buffer.buf, wd->buffer.used_len);
wd->buffer.used_len = 0;
}
/* append data at end of buffer */
memcpy(&wd->buf[wd->buf_used_len], adr, len);
wd->buf_used_len += len;
memcpy(&wd->buffer.buf[wd->buffer.used_len], adr, len);
wd->buffer.used_len += len;
}
}
@ -430,9 +603,9 @@ static WriteData *mywrite_begin(WriteWrap *ww, MemFile *compare, MemFile *curren
*/
static bool mywrite_end(WriteData *wd)
{
if (wd->buf_used_len != 0) {
writedata_do_write(wd, wd->buf, wd->buf_used_len);
wd->buf_used_len = 0;
if (wd->buffer.used_len != 0) {
writedata_do_write(wd, wd->buffer.buf, wd->buffer.used_len);
wd->buffer.used_len = 0;
}
if (wd->use_memfile) {
@ -1150,7 +1323,6 @@ bool BLO_write_file(Main *mainvar,
ReportList *reports)
{
char tempname[FILE_MAX + 1];
eWriteWrapType ww_type;
WriteWrap ww;
eBLO_WritePathRemap remap_mode = params->remap_mode;
@ -1172,14 +1344,7 @@ bool BLO_write_file(Main *mainvar,
/* open temporary file, so we preserve the original in case we crash */
BLI_snprintf(tempname, sizeof(tempname), "%s@", filepath);
if (write_flags & G_FILE_COMPRESS) {
ww_type = WW_WRAP_ZLIB;
}
else {
ww_type = WW_WRAP_NONE;
}
ww_handle_init(ww_type, &ww);
ww_handle_init((write_flags & G_FILE_COMPRESS) ? WW_WRAP_ZSTD : WW_WRAP_NONE, &ww);
if (ww.open(&ww, tempname) == false) {
BKE_reportf(

View File

@ -48,10 +48,6 @@ set(INC
${CMAKE_BINARY_DIR}/source/blender/makesdna/intern
)
set(INC_SYS
${ZLIB_INCLUDE_DIRS}
)
set(SRC
intern/wm.c
intern/wm_cursors.c

View File

@ -516,8 +516,13 @@ static int wm_read_exotic(const char *name)
/* check for compressed .blend */
FileReader *compressed_file = NULL;
if (BLI_file_magic_is_gzip(header)) {
/* In earlier versions of Blender (before 3.0), compressed files used Gzip instead of Zstd.
* While these files will no longer be written, there still needs to be reading support. */
compressed_file = BLI_filereader_new_gzip(rawfile);
}
else if (BLI_file_magic_is_zstd(header)) {
compressed_file = BLI_filereader_new_zstd(rawfile);
}
/* If a compression signature matches, try decompressing the start and check if it's a .blend */
if (compressed_file != NULL) {