diff options
-rw-r--r-- | build_files/cmake/Modules/FindZstd.cmake | 66 | ||||
-rw-r--r-- | build_files/cmake/platform/platform_apple.cmake | 3 | ||||
-rw-r--r-- | build_files/cmake/platform/platform_unix.cmake | 1 | ||||
-rw-r--r-- | build_files/cmake/platform/platform_win32.cmake | 3 | ||||
-rw-r--r-- | source/blender/blenlib/BLI_fileops.h | 2 | ||||
-rw-r--r-- | source/blender/blenlib/BLI_filereader.h | 4 | ||||
-rw-r--r-- | source/blender/blenlib/CMakeLists.txt | 3 | ||||
-rw-r--r-- | source/blender/blenlib/intern/fileops.c | 27 | ||||
-rw-r--r-- | source/blender/blenlib/intern/filereader_zstd.c | 335 | ||||
-rw-r--r-- | source/blender/blenloader/CMakeLists.txt | 2 | ||||
-rw-r--r-- | source/blender/blenloader/intern/readfile.c | 9 | ||||
-rw-r--r-- | source/blender/blenloader/intern/writefile.c | 297 | ||||
-rw-r--r-- | source/blender/windowmanager/CMakeLists.txt | 4 | ||||
-rw-r--r-- | source/blender/windowmanager/intern/wm_files.c | 5 |
14 files changed, 689 insertions, 72 deletions
diff --git a/build_files/cmake/Modules/FindZstd.cmake b/build_files/cmake/Modules/FindZstd.cmake new file mode 100644 index 00000000000..84606d01f44 --- /dev/null +++ b/build_files/cmake/Modules/FindZstd.cmake @@ -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 +) diff --git a/build_files/cmake/platform/platform_apple.cmake b/build_files/cmake/platform/platform_apple.cmake index dceafb236de..2bfdc84ec2a 100644 --- a/build_files/cmake/platform/platform_apple.cmake +++ b/build_files/cmake/platform/platform_apple.cmake @@ -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() diff --git a/build_files/cmake/platform/platform_unix.cmake b/build_files/cmake/platform/platform_unix.cmake index 7f62399ac4f..fc0c37e4c8b 100644 --- a/build_files/cmake/platform/platform_unix.cmake +++ b/build_files/cmake/platform/platform_unix.cmake @@ -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) diff --git a/build_files/cmake/platform/platform_win32.cmake b/build_files/cmake/platform/platform_win32.cmake index 3773aaaffed..d44ef691d1b 100644 --- a/build_files/cmake/platform/platform_win32.cmake +++ b/build_files/cmake/platform/platform_win32.cmake @@ -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) diff --git a/source/blender/blenlib/BLI_fileops.h b/source/blender/blenlib/BLI_fileops.h index 12fa73279c8..377b7bc3bc2 100644 --- a/source/blender/blenlib/BLI_fileops.h +++ b/source/blender/blenlib/BLI_fileops.h @@ -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(); diff --git a/source/blender/blenlib/BLI_filereader.h b/source/blender/blenlib/BLI_filereader.h index dbcaa7cb1b6..8d1fa3d1596 100644 --- a/source/blender/blenlib/BLI_filereader.h +++ b/source/blender/blenlib/BLI_filereader.h @@ -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(); diff --git a/source/blender/blenlib/CMakeLists.txt b/source/blender/blenlib/CMakeLists.txt index d2ba9e74c90..f98d15ad08b 100644 --- a/source/blender/blenlib/CMakeLists.txt +++ b/source/blender/blenlib/CMakeLists.txt @@ -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) diff --git a/source/blender/blenlib/intern/fileops.c b/source/blender/blenlib/intern/fileops.c index 6fc2222241b..31825c69737 100644 --- a/source/blender/blenlib/intern/fileops.c +++ b/source/blender/blenlib/intern/fileops.c @@ -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 diff --git a/source/blender/blenlib/intern/filereader_zstd.c b/source/blender/blenlib/intern/filereader_zstd.c new file mode 100644 index 00000000000..785a40cd1a1 --- /dev/null +++ b/source/blender/blenlib/intern/filereader_zstd.c @@ -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; +} diff --git a/source/blender/blenloader/CMakeLists.txt b/source/blender/blenloader/CMakeLists.txt index f5baf0dcb83..89631588ed0 100644 --- a/source/blender/blenloader/CMakeLists.txt +++ b/source/blender/blenloader/CMakeLists.txt @@ -42,7 +42,7 @@ set(INC ) set(INC_SYS - ${ZLIB_INCLUDE_DIRS} + ${ZSTD_INCLUDE_DIRS} ) set(SRC diff --git a/source/blender/blenloader/intern/readfile.c b/source/blender/blenloader/intern/readfile.c index 82fbb2083df..49c3497f996 100644 --- a/source/blender/blenloader/intern/readfile.c +++ b/source/blender/blenloader/intern/readfile.c @@ -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. */ diff --git a/source/blender/blenloader/intern/writefile.c b/source/blender/blenloader/intern/writefile.c index 4de8e35f402..6f43fbf1fa0 100644 --- a/source/blender/blenloader/intern/writefile.c +++ b/source/blender/blenloader/intern/writefile.c @@ -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 */ + +typedef struct { + struct ZstdWriteBlockTask *next, *prev; + void *data; + size_t size; + int frame_number; + WriteWrap *ww; +} ZstdWriteBlockTask; -static bool ww_open_zlib(WriteWrap *ww, const char *filepath) +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_open_zstd(WriteWrap *ww, const char *filepath) +{ + 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 bool ww_close_zlib(WriteWrap *ww) + +static void zstd_write_u32_le(WriteWrap *ww, uint32_t val) +{ +#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) { - return (gzclose(FILE_HANDLE(ww)) == Z_OK); + 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_zlib(WriteWrap *ww, const char *buf, size_t buf_len) + +static size_t ww_write_zstd(WriteWrap *ww, const char *buf, size_t buf_len) { - return gzwrite(FILE_HANDLE(ww), buf, 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( diff --git a/source/blender/windowmanager/CMakeLists.txt b/source/blender/windowmanager/CMakeLists.txt index b7fbb9bb82b..4d65726fe2b 100644 --- a/source/blender/windowmanager/CMakeLists.txt +++ b/source/blender/windowmanager/CMakeLists.txt @@ -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 diff --git a/source/blender/windowmanager/intern/wm_files.c b/source/blender/windowmanager/intern/wm_files.c index c3789abf2dd..f83511e76f0 100644 --- a/source/blender/windowmanager/intern/wm_files.c +++ b/source/blender/windowmanager/intern/wm_files.c @@ -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) { |