Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zstd minetest.compress support #12515

Merged
merged 1 commit into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions builtin/game/features.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ core.features = {
get_sky_as_table = true,
get_light_data_buffer = true,
mod_storage_on_disk = true,
compress_zstd = true,
}

function core.has_feature(arg)
Expand Down
19 changes: 12 additions & 7 deletions doc/client_lua_api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -897,14 +897,19 @@ Call these functions only at load time!
* Compress a string of data.
* `method` is a string identifying the compression method to be used.
* Supported compression methods:
* Deflate (zlib): `"deflate"`
* `...` indicates method-specific arguments. Currently defined arguments are:
* Deflate: `level` - Compression level, `0`-`9` or `nil`.
* Deflate (zlib): `"deflate"`
* Zstandard: `"zstd"`
* `...` indicates method-specific arguments. Currently defined arguments
are:
* Deflate: `level` - Compression level, `0`-`9` or `nil`.
* Zstandard: `level` - Compression level. Integer or `nil`. Default `3`.
Note any supported Zstandard compression level could be used here,
Copy link
Contributor

@erlehmann erlehmann Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should probably mention that Zstd beats other algorithms not on how well data is compressed, but on the speed of decompression with a wide range of compression options. If this is not noted, mod and game developers may assume that using zstd compression is always better. With default compression, zstd yields ssize-savings similar to zlib/deflate (it is worse up until some boundary, then gets better).

For inputs up to at least 4 kilobytes and even (in my tests) up to several 100 kilobytes at default compression level, zstd is likely to compress textual and biinary data about 5% worse than zlib … unless a pre-trained dictionary is used, which this API does not seem to provide. This is a WONTFIX from the zstd devs, as they are not interested in small files, but mods developers often do want to compress small payloads (e.g. bitmaps, item meta, json)..

Modders should also be warned that while increasing the Zstd compression level can make the compressed output smaller, but can make the zstd encoder take over 20 times or – at the highest compression levels – over 100 times as long as the default level makes it take. You can try this out yourself: Compressing a 94 kilobyte HTML page took <0.01s at default compression level on my machine, but took >1s at one of the highest zstd compression levels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...Since when was lua_api.txt meant to contain a detailed in-depth comparison between compression formats?

but these are subject to change between Zstandard versions.
* `minetest.decompress(compressed_data, method, ...)`: returns data
* Decompress a string of data (using ZLib).
* See documentation on `minetest.compress()` for supported compression methods.
* currently supported.
* `...` indicates method-specific arguments. Currently, no methods use this.
* Decompress a string of data using the algorithm specified by `method`.
* See documentation on `minetest.compress()` for supported compression
methods.
* `...` indicates method-specific arguments. Currently, no methods use this
* `minetest.rgba(red, green, blue[, alpha])`: returns a string
* Each argument is a 8 Bit unsigned integer
* Returns the ColorString from rgb or rgba values
Expand Down
8 changes: 7 additions & 1 deletion doc/lua_api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4910,6 +4910,8 @@ Utilities
-- the amount of data in mod storage is not constrained by
-- the amount of RAM available. (5.7.0)
mod_storage_on_disk = true,
-- "zstd" method for compress/decompress (5.7.0)
compress_zstd = true,
}

* `minetest.has_feature(arg)`: returns `boolean, missing_features`
Expand Down Expand Up @@ -6370,11 +6372,15 @@ Misc.
* `method` is a string identifying the compression method to be used.
* Supported compression methods:
* Deflate (zlib): `"deflate"`
* Zstandard: `"zstd"`
* `...` indicates method-specific arguments. Currently defined arguments
are:
* Deflate: `level` - Compression level, `0`-`9` or `nil`.
* Zstandard: `level` - Compression level. Integer or `nil`. Default `3`.
Note any supported Zstandard compression level could be used here,
Copy link
Contributor

@erlehmann erlehmann Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should probably mention that Zstd beats other algorithms not on how well data is compressed, but on the speed of decompression with a wide range of compression options. If this is not noted, mod and game developers may assume that using zstd compression is always better. With default compression, zstd yields ssize-savings similar to zlib/deflate (it is worse up until some boundary, then gets better).

For inputs up to at least 4 kilobytes and even (in my tests) up to several 100 kilobytes at default compression level, zstd is likely to compress textual and biinary data about 5% worse than zlib … unless a pre-trained dictionary is used, which this API does not seem to provide. This is a WONTFIX from the zstd devs, as they are not interested in small files, but mods developers often do want to compress small payloads (e.g. bitmaps, item meta, json)..

Modders should also be warned that while increasing the Zstd compression level can make the compressed output smaller, but can make the zstd encoder take over 20 times or – at the highest compression levels – over 100 times as long as the default level makes it take. You can try this out yourself: Compressing a 94 kilobyte HTML page took <0.01s at default compression level on my machine, but took >1s at one of the highest zstd compression levels.

but these are subject to change between Zstandard versions.
* `minetest.decompress(compressed_data, method, ...)`: returns data
* Decompress a string of data (using ZLib).
* Decompress a string of data using the algorithm specified by `method`.
* See documentation on `minetest.compress()` for supported compression
methods.
* `...` indicates method-specific arguments. Currently, no methods use this
Expand Down
23 changes: 23 additions & 0 deletions games/devtest/mods/unittests/misc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,26 @@ unittests.register("test_punch_node", function(_, pos)
minetest.remove_node(pos)
-- currently failing: assert(on_punch_called)
end, {map=true})

local function test_compress()
-- This text should be compressible, to make sure the results are... normal
local text = "The\000 icey canoe couldn't move very well on the\128 lake. The\000 ice was too stiff and the icey canoe's paddles simply wouldn't punch through."
local methods = {
"deflate",
"zstd",
-- "noodle", -- for warning alarm test
}
local zstd_magic = string.char(0x28, 0xB5, 0x2F, 0xFD)
20kdc marked this conversation as resolved.
Show resolved Hide resolved
for _, method in ipairs(methods) do
local compressed = core.compress(text, method)
assert(core.decompress(compressed, method) == text, "input/output mismatch for compression method " .. method)
local has_zstd_magic = compressed:sub(1, 4) == zstd_magic
if method == "zstd" then
assert(has_zstd_magic, "zstd magic number not in zstd method")
else
assert(not has_zstd_magic, "zstd magic number in method " .. method .. " (which is not zstd)")
end
end
end
unittests.register("test_compress", test_compress)

57 changes: 52 additions & 5 deletions src/script/lua_api/l_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "cpp_api/s_async.h"
#include "serialization.h"
#include <json/json.h>
#include <zstd.h>
#include "cpp_api/s_security.h"
#include "porting.h"
#include "convert_json.h"
Expand Down Expand Up @@ -278,6 +279,34 @@ int ModApiUtil::l_get_user_path(lua_State *L)
return 1;
}

enum LuaCompressMethod
{
LUA_COMPRESS_METHOD_DEFLATE,
LUA_COMPRESS_METHOD_ZSTD,
};

static const struct EnumString es_LuaCompressMethod[] =
{
{LUA_COMPRESS_METHOD_DEFLATE, "deflate"},
{LUA_COMPRESS_METHOD_ZSTD, "zstd"},
{0, nullptr},
};

static LuaCompressMethod get_compress_method(lua_State *L, int index)
{
if (lua_isnoneornil(L, index))
return LUA_COMPRESS_METHOD_DEFLATE;
const char *name = luaL_checkstring(L, index);
int value;
if (!string_to_enum(es_LuaCompressMethod, value, name)) {
// Pretend it's deflate if we don't know, for compatibility reasons.
log_deprecated(L, "Unknown compression method \"" + std::string(name)
+ "\", defaulting to \"deflate\". You should pass a valid value.");
20kdc marked this conversation as resolved.
Show resolved Hide resolved
return LUA_COMPRESS_METHOD_DEFLATE;
}
sfan5 marked this conversation as resolved.
Show resolved Hide resolved
return (LuaCompressMethod) value;
}

// compress(data, method, level)
int ModApiUtil::l_compress(lua_State *L)
{
Expand All @@ -286,12 +315,23 @@ int ModApiUtil::l_compress(lua_State *L)
size_t size;
const char *data = luaL_checklstring(L, 1, &size);

int level = -1;
if (!lua_isnoneornil(L, 3))
level = readParam<int>(L, 3);
LuaCompressMethod method = get_compress_method(L, 2);

std::ostringstream os(std::ios_base::binary);
compressZlib(reinterpret_cast<const u8 *>(data), size, os, level);

if (method == LUA_COMPRESS_METHOD_DEFLATE) {
int level = -1;
if (!lua_isnoneornil(L, 3))
level = readParam<int>(L, 3);

compressZlib(reinterpret_cast<const u8 *>(data), size, os, level);
} else if (method == LUA_COMPRESS_METHOD_ZSTD) {
int level = ZSTD_CLEVEL_DEFAULT;
if (!lua_isnoneornil(L, 3))
level = readParam<int>(L, 3);

compressZstd(reinterpret_cast<const u8 *>(data), size, os, level);
}
SmallJoker marked this conversation as resolved.
Show resolved Hide resolved

std::string out = os.str();

Expand All @@ -307,9 +347,16 @@ int ModApiUtil::l_decompress(lua_State *L)
size_t size;
const char *data = luaL_checklstring(L, 1, &size);

LuaCompressMethod method = get_compress_method(L, 2);

std::istringstream is(std::string(data, size), std::ios_base::binary);
std::ostringstream os(std::ios_base::binary);
decompressZlib(is, os);

if (method == LUA_COMPRESS_METHOD_DEFLATE) {
decompressZlib(is, os);
} else if (method == LUA_COMPRESS_METHOD_ZSTD) {
decompressZstd(is, os);
}
SmallJoker marked this conversation as resolved.
Show resolved Hide resolved

std::string out = os.str();

Expand Down