diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index d137c2e1..8da187f9 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -625,6 +625,8 @@ DatasetSettings Convert(const org::openapitools::server::model::Dataset_settings ret.Compression(CompressionAlgorithm::BSHUF_ZSTD); else if (compr == "bszstd_rle") ret.Compression(CompressionAlgorithm::BSHUF_ZSTD_RLE); + else if (compr == "bszstd_rlehuf") + ret.Compression(CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF); else if (compr == "none") ret.Compression(CompressionAlgorithm::NO_COMPRESSION); else diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index 95845dfc..8e78be09 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -428,6 +428,7 @@ components: "bslz4", "bszstd", "bszstd_rle", + "bszstd_rlehuf", "none" ] default: "bslz4" diff --git a/common/DatasetSettings.cpp b/common/DatasetSettings.cpp index 2bcb144b..db185921 100644 --- a/common/DatasetSettings.cpp +++ b/common/DatasetSettings.cpp @@ -93,6 +93,7 @@ DatasetSettings &DatasetSettings::Compression(CompressionAlgorithm input) { case CompressionAlgorithm::BSHUF_LZ4: case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: compression = input; break; default: diff --git a/compression/CMakeLists.txt b/compression/CMakeLists.txt index aacc889e..73a875d3 100644 --- a/compression/CMakeLists.txt +++ b/compression/CMakeLists.txt @@ -8,6 +8,8 @@ ADD_LIBRARY(Compression STATIC bitshuffle_hperf/bitshuffle.h JFJochZstdCompressor.cpp JFJochZstdCompressor.h + JFJochZstdHuffCompressor.cpp + JFJochZstdHuffCompressor.h JFJochCompressor.cpp JFJochCompressor.h JFJochDecompress.h diff --git a/compression/CompressionAlgorithmEnum.h b/compression/CompressionAlgorithmEnum.h index 783ddb14..e6559140 100644 --- a/compression/CompressionAlgorithmEnum.h +++ b/compression/CompressionAlgorithmEnum.h @@ -3,4 +3,4 @@ #pragma once -enum class CompressionAlgorithm {BSHUF_LZ4, BSHUF_ZSTD, BSHUF_ZSTD_RLE, NO_COMPRESSION}; +enum class CompressionAlgorithm {BSHUF_LZ4, BSHUF_ZSTD, BSHUF_ZSTD_RLE, BSHUF_ZSTD_RLE_HUFF, NO_COMPRESSION}; diff --git a/compression/JFJochCompressor.cpp b/compression/JFJochCompressor.cpp index 3470e728..b837fa5b 100644 --- a/compression/JFJochCompressor.cpp +++ b/compression/JFJochCompressor.cpp @@ -31,6 +31,7 @@ static size_t MaxCompressedBlockSize(CompressionAlgorithm algorithm, size_t src_ return LZ4_compressBound(src_size) + 4; case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: return ZSTD_compressBound(src_size) + 4; default: return src_size + 4; @@ -69,6 +70,9 @@ size_t JFJochBitShuffleCompressor::CompressBlock(char *dest, const char *source, throw JFJochException(JFJochExceptionCategory::ZSTDCompressionError, e.what()); } break; + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: + compressed_size = huff_compressor.Compress(((uint8_t *) dest) + 4, (const uint64_t *) src_ptr, src_size); + break; default: throw JFJochException(JFJochExceptionCategory::Compression, "Algorithm not supported"); } diff --git a/compression/JFJochCompressor.h b/compression/JFJochCompressor.h index 644900d2..c4591c71 100644 --- a/compression/JFJochCompressor.h +++ b/compression/JFJochCompressor.h @@ -12,9 +12,11 @@ #include "MaxCompressedSize.h" #include "JFJochZstdCompressor.h" +#include "JFJochZstdHuffCompressor.h" class JFJochBitShuffleCompressor { JFJochZstdCompressor zstd_compressor; + JFJochZstdHuffCompressor huff_compressor; CompressionAlgorithm algorithm; std::vector tmp_space; std::vector scratch; diff --git a/compression/JFJochDecompress.h b/compression/JFJochDecompress.h index a2479d04..0e54a3d0 100644 --- a/compression/JFJochDecompress.h +++ b/compression/JFJochDecompress.h @@ -30,7 +30,8 @@ inline size_t JFJochDecompressHperfPtr(uint8_t *output, size_t block_size) { if ((algorithm != CompressionAlgorithm::BSHUF_LZ4) && (algorithm != CompressionAlgorithm::BSHUF_ZSTD) && - (algorithm != CompressionAlgorithm::BSHUF_ZSTD_RLE)) + (algorithm != CompressionAlgorithm::BSHUF_ZSTD_RLE) && + (algorithm != CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF)) throw JFJochException(JFJochExceptionCategory::Compression, "Algorithm not supported by hperf decompressor"); if ((block_size % BSHUF_BLOCKED_MULT) != 0) @@ -65,7 +66,8 @@ inline size_t JFJochDecompressHperfPtr(uint8_t *output, break; } case CompressionAlgorithm::BSHUF_ZSTD: - case CompressionAlgorithm::BSHUF_ZSTD_RLE: { + case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: { const size_t ret = ZSTD_decompress(decompressed_block.data(), expected_size, src_ptr, @@ -138,6 +140,7 @@ inline void JFJochDecompressPtr(uint8_t *output, } break; case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: case CompressionAlgorithm::BSHUF_ZSTD: if (use_hperf) { if (JFJochDecompressHperfPtr(output, algorithm, source + 12, source_size - 12, diff --git a/compression/JFJochZstdHuffCompressor.cpp b/compression/JFJochZstdHuffCompressor.cpp new file mode 100644 index 00000000..25c449d5 --- /dev/null +++ b/compression/JFJochZstdHuffCompressor.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +// This file hand-assembles a standard Zstandard frame so it can be decoded by stock +// ZSTD_decompress. Every structure below follows the Zstandard compression format spec: +// https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md +// Comments cite that document's section names so the framing can be checked clause by clause. +// Only the framing is hand-written; the Huffman tree + bitstream inside each Compressed_Block's +// Literals_Section are produced by zstd's own HUF_compress*X_repeat (so those bytes are trusted). + +#include "JFJochZstdHuffCompressor.h" + +#include +#include +#include + +extern "C" { +#include +size_t HUF_compress1X_repeat(void *dst, size_t dstSize, const void *src, size_t srcSize, + unsigned maxSymbolValue, unsigned tableLog, void *workSpace, size_t wkspSize, + HUF_CElt *hufTable, HUF_repeat *repeat, int flags); +} + +namespace { +// --- "Zstandard frames" / "Frame_Header" --- +constexpr uint32_t MAGIC_NUMBER = 0xFD2FB528u; +// Frame_Header_Descriptor: Frame_Content_Size_flag = 2 (4-byte size), Single_Segment_flag = 1 +// (=> no Window_Descriptor; Window_Size = Frame_Content_Size), no checksum, no dictionary. +constexpr uint8_t FRAME_DESCRIPTOR = 0xA0; + +// --- "Blocks": Block_Type field of Block_Header --- +constexpr uint32_t BLOCK_RAW = 0; +constexpr uint32_t BLOCK_RLE = 1; +constexpr uint32_t BLOCK_COMPRESSED = 2; + +// --- "Literals_Section_Header": Literals_Block_Type field --- +constexpr uint8_t LITERALS_COMPRESSED = 2; // carries a Huffman_Tree_Description +constexpr uint8_t LITERALS_TREELESS = 3; // reuses the tree from a previous Compressed block + +constexpr size_t CHUNK = 65536; // cap each block's regenerated size well under Block_Maximum_Size (128 KiB) +constexpr size_t RUN_MIN = 64; // runs shorter than this are absorbed into the literal stream +constexpr unsigned LIT_LOG = 11; // Huffman table-log limit for literals, per the format +} + +JFJochZstdHuffCompressor::JFJochZstdHuffCompressor() + : ctable(HUF_CTABLE_SIZE_ST(255)), entwksp(HUF_WORKSPACE_SIZE_U64) {} + +// append the low nbytes of v, little-endian (Zstd integer fields are little-endian) +void JFJochZstdHuffCompressor::put_le(uint64_t v, int nbytes) { + for (int i = 0; i < nbytes; i++) out.push_back((uint8_t)(v >> (8 * i))); +} + +// Block_Header (3 bytes, little-endian): [ Last_Block:1 | Block_Type:2 | Block_Size:21 ]. +// Last_Block (bit 0) is left 0 here and OR-ed onto the final block afterwards. Returns the +// offset of the header so that final block can be marked. +size_t JFJochZstdHuffCompressor::blk_hdr(uint32_t type, uint32_t size) { + size_t off = out.size(); + put_le((uint64_t)(type << 1) | ((uint64_t)size << 3), 3); + return off; +} + +// RLE_Block: Block_Size is the number of repeats (the regenerated size); Block_Content is the +// single repeated byte. Split into <=CHUNK pieces so Block_Size stays under Block_Maximum_Size. +void JFJochZstdHuffCompressor::emit_run(uint8_t value, size_t nbytes, size_t &last_off) { + for (size_t off = 0; off < nbytes; off += CHUNK) { + size_t c = std::min(CHUNK, nbytes - off); + last_off = blk_hdr(BLOCK_RLE, (uint32_t)c); + out.push_back(value); + } +} + +// Emit one Compressed_Block whose Literals_Section holds n Huffman-coded literal bytes and whose +// Sequences_Section is empty (Number_of_Sequences = 0). Falls back to a Raw_Block if Huffman does +// not help. The Huffman payload (tree + streams, or streams only when reusing) is produced by zstd. +void JFJochZstdHuffCompressor::emit_lit_chunk(const uint8_t *lits, size_t n, size_t &last_off) { + HUF_CElt *ct = reinterpret_cast(ctable.data()); + HUF_repeat rep = (HUF_repeat)repeat_state; + // Size_Format also selects the stream count: format 0 = 1 stream, formats 1/2/3 = 4 streams. + // Use a single stream only for small inputs (matches zstd's own ZSTD_compressLiterals heuristic). + const bool single = n < 256; + const size_t wkspSize = entwksp.size() * sizeof(uint64_t); + size_t hs = single + ? HUF_compress1X_repeat(hufbuf.data(), hufbuf.size(), lits, n, 255, LIT_LOG, entwksp.data(), wkspSize, ct, &rep, 0) + : HUF_compress4X_repeat(hufbuf.data(), hufbuf.size(), lits, n, 255, LIT_LOG, entwksp.data(), wkspSize, ct, &rep, 0); + + // Literals_Section_Header, read as a little-endian value, is laid out as + // [ Literals_Block_Type:2 | Size_Format:2 | Regenerated_Size | Compressed_Size ]. + // Size_Format (chosen from Regenerated_Size n) sets the header length and field widths: + // n < 1 KiB -> 3-byte header, 10-bit fields (format 0 single-stream, else 1) + // n < 16 KiB -> 4-byte header, 14-bit fields (format 2) + // else -> 5-byte header, 18-bit fields (format 3) + // Regenerated_Size starts at bit 4; Compressed_Size starts right after it, at bit (4 + width). + int size_format, lh_bytes, regen_width; + if (n < 1024) { size_format = single ? 0 : 1; lh_bytes = 3; regen_width = 10; } + else if (n < 16384) { size_format = 2; lh_bytes = 4; regen_width = 14; } + else { size_format = 3; lh_bytes = 5; regen_width = 18; } + + // hs == 1 is HUF's "single-symbol alphabet" signal (not a usable Huffman section); only keep + // the Huffman block when it is well-formed and actually smaller than a Raw_Block. + if (hs > 1 && !HUF_isError(hs) && (size_t)(lh_bytes + 1) + hs < n) { + repeat_state = HUF_repeat_check; // the table now in ct is valid -> reuse it next time + // HUF_compress*_repeat leaves *repeat != none only when it reused the previous table. + uint8_t lit_type = (rep != HUF_repeat_none) ? LITERALS_TREELESS : LITERALS_COMPRESSED; + uint64_t hdr = (uint64_t)lit_type | ((uint64_t)size_format << 2) + | ((uint64_t)n << 4) | ((uint64_t)hs << (4 + regen_width)); + last_off = blk_hdr(BLOCK_COMPRESSED, (uint32_t)(lh_bytes + hs + 1)); + put_le(hdr, lh_bytes); // Literals_Section_Header + out.insert(out.end(), hufbuf.data(), hufbuf.data() + hs); // Huffman tree + stream(s) + out.push_back(0x00); // Sequences_Section: Number_of_Sequences = 0 + } else { + repeat_state = HUF_repeat_none; // discard any table HUF rebuilt but we did not emit + last_off = blk_hdr(BLOCK_RAW, (uint32_t)n); + out.insert(out.end(), lits, lits + n); + } +} + +// src = one bitshuffled block (src_size bytes, a multiple of 8). Emits a complete Zstd frame. +size_t JFJochZstdHuffCompressor::Compress(uint8_t *dst, const uint64_t *src, size_t src_size) { + const size_t W = src_size / 8; + out.clear(); literals.clear(); segs.clear(); + repeat_state = HUF_repeat_none; + if (hufbuf.size() < src_size + 1024) hufbuf.resize(src_size + 1024); + + // Pass 1: classify 8-byte words into runs (0x00 / 0xFF) and literals. Runs >= RUN_MIN become + // RLE_Blocks; shorter runs are folded into the literal stream (where 0x00 costs ~1 Huffman bit). + size_t lit_start = 0; + auto close_lit = [&]() { if (literals.size() > lit_start) { + segs.push_back({2, literals.size() - lit_start, lit_start}); lit_start = literals.size(); } }; + for (size_t i = 0; i < W; ) { + if (src[i] == 0 || src[i] == UINT64_MAX) { + uint64_t val = src[i]; size_t j = i; while (j < W && src[j] == val) j++; + size_t bytes = (j - i) * 8; uint8_t v = val ? 0xFF : 0x00; + if (bytes >= RUN_MIN) { close_lit(); segs.push_back({(uint8_t)(val ? 1 : 0), bytes, 0}); } + else literals.insert(literals.end(), bytes, v); + i = j; + } else { + const uint8_t *wb = reinterpret_cast(&src[i]); + literals.insert(literals.end(), wb, wb + 8); + i++; + } + } + close_lit(); + + // Frame_Header: Magic_Number + Frame_Header_Descriptor + Frame_Content_Size (4 bytes). + put_le(MAGIC_NUMBER, 4); + out.push_back(FRAME_DESCRIPTOR); + put_le(src_size, 4); + + // Pass 2: emit the blocks in order. + size_t last_off = 0; bool emitted = false; + for (const Seg &s : segs) { + emitted = true; + if (s.type == 2) + for (size_t off = 0; off < s.bytes; off += CHUNK) + emit_lit_chunk(literals.data() + s.lit_off + off, std::min(CHUNK, s.bytes - off), last_off); + else + emit_run(s.type == 1 ? 0xFF : 0x00, s.bytes, last_off); + } + if (!emitted) last_off = blk_hdr(BLOCK_RAW, 0); // empty input -> a single empty block + out[last_off] |= 1; // set Last_Block on the final block + + memcpy(dst, out.data(), out.size()); + return out.size(); +} diff --git a/compression/JFJochZstdHuffCompressor.h b/compression/JFJochZstdHuffCompressor.h new file mode 100644 index 00000000..4d1ac84e --- /dev/null +++ b/compression/JFJochZstdHuffCompressor.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include + +// Produces a STANDARD Zstandard frame from bitshuffled data, decodable by stock ZSTD_decompress: +// - zero / 0xFF runs -> RLE_Blocks (cheap, like JFJochZstdCompressor) +// - literal regions -> Compressed_Blocks with Huffman literals (no sequences); short +// runs are absorbed into the literal stream +// - incompressible literals -> Raw_Blocks (bounded worst case) +// Faster than full ZSTD (no match search) and better ratio than the plain RLE compressor (it +// entropy-codes the literals). The Huffman table is built/reused per block from that block's own +// literals via zstd's HUF_compress*X_repeat, so it is robust on any input (random, Poisson, masks, +// zeros) with no trained tables. +class JFJochZstdHuffCompressor { + std::vector out; // assembled frame + std::vector literals; // literal bytes in stream order (incl. absorbed short runs) + std::vector hufbuf; // scratch for one Huffman-coded literal chunk + std::vector ctable; // HUF_CElt[] (size_t-aligned) + std::vector entwksp; // HUF compression workspace + unsigned repeat_state = 0; // HUF_repeat across literal blocks within the current frame + + struct Seg { uint8_t type; size_t bytes; size_t lit_off; }; // type 0=run0, 1=runFF, 2=literals + std::vector segs; + + void put_le(uint64_t v, int nbytes); + size_t blk_hdr(uint32_t type, uint32_t size); + void emit_run(uint8_t value, size_t nbytes, size_t &last_off); + void emit_lit_chunk(const uint8_t *lits, size_t n, size_t &last_off); +public: + JFJochZstdHuffCompressor(); + // src = bitshuffled block (src_size bytes, a multiple of 8). Writes one zstd frame to dst and + // returns its size. dst must hold at least ZSTD_compressBound(src_size) + 12 bytes. + size_t Compress(uint8_t *dst, const uint64_t *src, size_t src_size); +}; diff --git a/compression/MaxCompressedSize.cpp b/compression/MaxCompressedSize.cpp index 613a3480..1a28e0c8 100644 --- a/compression/MaxCompressedSize.cpp +++ b/compression/MaxCompressedSize.cpp @@ -12,6 +12,7 @@ int64_t MaxCompressedSize(CompressionAlgorithm algorithm, int64_t pixels_number, return bshuf_compress_lz4_bound(pixels_number, pixel_depth, JFJochBitShuffleCompressor::BlockSize(algorithm, pixel_depth)) + 12; case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: return bshuf_compress_zstd_bound(pixels_number, pixel_depth, JFJochBitShuffleCompressor::BlockSize(algorithm, pixel_depth)) + 12; default: return pixels_number * pixel_depth; diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index 0cf19abd..73cea1c7 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -68,6 +68,7 @@ void CBOR_ENC_COMPRESSED(CborEncoder &encoder, break; case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: cborErr(cbor_encode_text_stringz(&arrayEncoder, "bszstd")); break; default: diff --git a/tests/ZSTDCompressorTest.cpp b/tests/ZSTDCompressorTest.cpp index ba2fe8a4..29d8fb25 100644 --- a/tests/ZSTDCompressorTest.cpp +++ b/tests/ZSTDCompressorTest.cpp @@ -10,6 +10,7 @@ #include "../compression/JFJochCompressor.h" #include "../compression/JFJochDecompress.h" #include "../common/DiffractionExperiment.h" +#include "../writer/HDF5Objects.h" TEST_CASE("JFjochZstdCompressor_Raw_Block","[ZSTD]") { uint8_t src[128*8]; @@ -315,4 +316,64 @@ TEST_CASE("Bitshuffle_ZSTD","[ZSTD]") { REQUIRE(bshuf_decompress_zstd(compressed.data(), decompressed.data(), RAW_MODULE_SIZE, 4, 0) == out_size); REQUIRE(memcmp(image.data(), decompressed.data(), RAW_MODULE_SIZE*sizeof(uint32_t)) == 0); -} \ No newline at end of file +} +// ---- BSHUF_ZSTD_RLE_HUFF (RLE runs + adaptive Huffman literals, standard zstd frame) ---- +// Round-trips through JFJochDecompress (which calls stock ZSTD_decompress per block) and checks +// the output never exceeds the advertised worst-case size. Run on good and adversarial data to +// confirm the algorithm always produces valid output, even when the ratio is poor. +template +static void RequireHuffRoundTrip(const std::vector &image) { + JFJochBitShuffleCompressor compressor(CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF); + auto compressed = compressor.Compress(image); + + REQUIRE(compressed.size() <= (size_t) MaxCompressedSize(CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF, + image.size(), sizeof(T))); + std::vector output; + REQUIRE_NOTHROW(JFJochDecompress(output, CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF, + compressed, image.size())); + REQUIRE(output.size() == image.size()); + REQUIRE(memcmp(image.data(), output.data(), image.size() * sizeof(T)) == 0); +} + +TEST_CASE("ZstdHuff_PureZeros", "[ZSTD]") { + std::vector image(200000, 0); // all runs, no literals + RequireHuffRoundTrip(image); +} + +TEST_CASE("ZstdHuff_Poisson10", "[ZSTD]") { + std::vector image(200000); + std::mt19937 gen(12345); + std::poisson_distribution dist(10); + for (auto &v : image) v = (uint16_t) dist(gen); + RequireHuffRoundTrip(image); +} + +TEST_CASE("ZstdHuff_RandomIncompressible", "[ZSTD]") { + std::vector image(200000); // Mersenne-Twister noise: ~zero compression + std::mt19937 gen(98765); + for (auto &v : image) v = (uint16_t) gen(); + RequireHuffRoundTrip(image); // must still be valid and within bound +} + +TEST_CASE("ZstdHuff_MaskLike", "[ZSTD]") { + std::vector image(200000, 0); // extreme sparsity like a pixel_mask + std::mt19937 gen(555); + for (auto &v : image) { + uint32_t r = gen() % 100; + if (r < 5) v = 1; + else if (r == 5) v = 0x80000000u; // module-gap style flag + } + RequireHuffRoundTrip(image); +} + +TEST_CASE("ZstdHuff_LysoImage", "[ZSTD]") { + RegisterHDF5Filter(); // bitshuffle filter, needed to read the compressed benchmark dataset + HDF5ReadOnlyFile data("../../tests/test_data/compression_benchmark.h5"); + HDF5DataSet dataset(data, "/entry/data/data"); + HDF5DataSpace file_space(dataset); + auto dims = file_space.GetDimensions(); + std::vector image(dims[1] * dims[2]); + std::vector start = {0, 0, 0}, size = {1, dims[1], dims[2]}; + dataset.ReadVector(image, start, size); + RequireHuffRoundTrip(image); +} diff --git a/writer/HDF5Objects.cpp b/writer/HDF5Objects.cpp index dc8329ec..66aa2fe2 100644 --- a/writer/HDF5Objects.cpp +++ b/writer/HDF5Objects.cpp @@ -972,6 +972,7 @@ void HDF5Dcpl::SetCompression(CompressionAlgorithm c, size_t block_size) { break; case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: + case CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF: #ifdef USE_ZSTD params[0] = block_size; params[1] = BSHUF_H5_COMPRESS_ZSTD;