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

Emscripten update and standalone support #925

Merged
2 changes: 1 addition & 1 deletion .github/workflows/emscripten.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
emscripten:
env:
EMSCRIPTEN_VERSION: 1.37.26
EMSCRIPTEN_VERSION: 3.1.43
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
Expand Down
136 changes: 100 additions & 36 deletions build-emscripten.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,116 @@
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

CORES=$(nproc --all)
CORES="${CORES:-`nproc --all`}"
ENABLE_LIBDE265="${ENABLE_LIBDE265:-1}"
LIBDE265_VERSION="${LIBDE265_VERSION:-1.0.12}"
ENABLE_AOM="${ENABLE_AOM:-0}"
AOM_VERSION="${AOM_VERSION:-3.6.1}"
STANDALONE="${STANDALONE:-0}"
DEBUG="${DEBUG:-0}"
USE_WASM="${USE_WASM:-1}"

echo "Build using ${CORES} CPU cores"

LIBDE265_VERSION=1.0.8
[ -s "libde265-${LIBDE265_VERSION}.tar.gz" ] || curl \
-L \
-o libde265-${LIBDE265_VERSION}.tar.gz \
https://github.com/strukturag/libde265/releases/download/v${LIBDE265_VERSION}/libde265-${LIBDE265_VERSION}.tar.gz
if [ ! -s "libde265-${LIBDE265_VERSION}/libde265/.libs/libde265.so" ]; then
tar xf libde265-${LIBDE265_VERSION}.tar.gz
cd libde265-${LIBDE265_VERSION}
[ -x configure ] || ./autogen.sh
emconfigure ./configure --disable-sse --disable-dec265 --disable-sherlock265
emmake make -j${CORES}
cd ..
LIBRARY_LINKER_FLAGS=""
LIBRARY_INCLUDE_FLAGS=""

CONFIGURE_ARGS_LIBDE265=""
if [ "$ENABLE_LIBDE265" = "1" ]; then
[ -s "libde265-${LIBDE265_VERSION}.tar.gz" ] || curl \
-L \
-o libde265-${LIBDE265_VERSION}.tar.gz \
https://github.com/strukturag/libde265/releases/download/v${LIBDE265_VERSION}/libde265-${LIBDE265_VERSION}.tar.gz
if [ ! -s "libde265-${LIBDE265_VERSION}/libde265/.libs/libde265.so" ]; then
tar xf libde265-${LIBDE265_VERSION}.tar.gz
cd libde265-${LIBDE265_VERSION}
[ -x configure ] || ./autogen.sh
emconfigure ./configure --disable-sse --disable-dec265 --disable-sherlock265
emmake make -j${CORES}
cd ..
fi
CONFIGURE_ARGS_LIBDE265="-DLIBDE265_INCLUDE_DIR=${DIR}/libde265-${LIBDE265_VERSION} -DLIBDE265_LIBRARY=-L${DIR}/libde265-${LIBDE265_VERSION}/libde265/.libs"
LIBRARY_LINKER_FLAGS="$LIBRARY_LINKER_FLAGS -lde265"
LIBRARY_INCLUDE_FLAGS="$LIBRARY_INCLUDE_FLAGS -L${DIR}/libde265-${LIBDE265_VERSION}/libde265/.libs"
fi

CONFIGURE_ARGS_AOM=""
if [ "$ENABLE_AOM" = "1" ]; then
[ -s "aom-${AOM_VERSION}.tar.gz" ] || curl \
-L \
-o aom-${AOM_VERSION}.tar.gz \
"https://aomedia.googlesource.com/aom/+archive/v${AOM_VERSION}.tar.gz"
if [ ! -s "aom-${AOM_VERSION}/libaom.a" ]; then
mkdir -p aom-${AOM_VERSION}/aom-source
tar xf aom-${AOM_VERSION}.tar.gz -C aom-${AOM_VERSION}/aom-source
cd aom-${AOM_VERSION}

emcmake cmake aom-source \
-DENABLE_CCACHE=1 \
-DAOM_TARGET_CPU=generic \
-DENABLE_DOCS=0 \
-DENABLE_TESTS=0 \
-DENABLE_EXAMPLES=0 \
-DENABLE_TESTDATA=0 \
-DENABLE_TOOLS=0 \
-DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 \
-DBUILD_SHARED_LIBS=1 \
-DCMAKE_BUILD_TYPE=Release

emmake make -j${CORES}

cd ..
fi

CONFIGURE_ARGS_AOM="-DAOM_INCLUDE_DIR=${DIR}/aom-${AOM_VERSION}/aom-source -DAOM_LIBRARY=-L${DIR}/aom-${AOM_VERSION}"
LIBRARY_LINKER_FLAGS="$LIBRARY_LINKER_FLAGS -laom"
LIBRARY_INCLUDE_FLAGS="$LIBRARY_INCLUDE_FLAGS -L${DIR}/aom-${AOM_VERSION}"
fi

CONFIGURE_ARGS="-DENABLE_MULTITHREADING_SUPPORT=OFF -DWITH_GDK_PIXBUF=OFF -DWITH_EXAMPLES=OFF -DBUILD_SHARED_LIBS=ON"
#export PKG_CONFIG_PATH="${DIR}/libde265-${LIBDE265_VERSION}"
EXTRA_EXE_LINKER_FLAGS="-lembind"
EXTRA_COMPILER_FLAGS=""
if [ "$STANDALONE" = "1" ]; then
EXTRA_EXE_LINKER_FLAGS=""
EXTRA_COMPILER_FLAGS="-D__EMSCRIPTEN_STANDALONE_WASM__=1"
fi

CONFIGURE_ARGS="-DENABLE_MULTITHREADING_SUPPORT=OFF -DWITH_GDK_PIXBUF=OFF -DWITH_EXAMPLES=OFF -DBUILD_SHARED_LIBS=ON -DENABLE_PLUGIN_LOADING=OFF"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added -DENABLE_PLUGIN_LOADING=OFF, I thought it would make sense.

emcmake cmake $CONFIGURE_ARGS \
-DLIBDE265_INCLUDE_DIR="${DIR}/libde265-${LIBDE265_VERSION}" \
-DLIBDE265_LIBRARY="-L${DIR}/libde265-${LIBDE265_VERSION}/libde265/.libs"
-DCMAKE_C_FLAGS="${EXTRA_COMPILER_FLAGS}" \
-DCMAKE_CXX_FLAGS="${EXTRA_COMPILER_FLAGS}" \
-DCMAKE_EXE_LINKER_FLAGS="${LIBRARY_LINKER_FLAGS} ${EXTRA_EXE_LINKER_FLAGS}" \
$CONFIGURE_ARGS_LIBDE265 \
$CONFIGURE_ARGS_AOM

emmake make -j${CORES}

export TOTAL_MEMORY=16777216
LIBHEIFA="libheif/libheif.a"
EXPORTED_FUNCTIONS=$($EMSDK/upstream/bin/llvm-nm $LIBHEIFA --format=just-symbols | grep "^heif_\|^de265_\|^aom_" | grep "[^:]$" | sed 's/^/_/' | paste -sd "," -)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is necessary to make the C API's be exposed, otherwise they won't be available exported the WASM/JS code.


echo "Running Emscripten..."
emcc libheif/libheif.so \
--bind \
--closure 0 \
-s NO_EXIT_RUNTIME=1 \
-s TOTAL_MEMORY=${TOTAL_MEMORY} \

BUILD_FLAGS="-lembind -o libheif.js --pre-js pre.js --post-js post.js -sWASM=$USE_WASM"
RELEASE_BUILD_FLAGS="-O3"

if [ "$STANDALONE" = "1" ]; then
echo "Building in standalone (non-web) build mode"
BUILD_FLAGS="$BUILD_FLAGS -s STANDALONE_WASM=1 -s WASM=1 -o libheif.wasm --no-entry"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@farindk I specifically did not extend BUILD_FLAGS in standalone build because if you do, it will include embind imports in the generated WASM.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. I'll undo that. I must admit that I didn't really understand the 'standalone' option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@farindk Standalone builds allow you to run the generated WASM outside the browser in WebAssembly runtimes such as Wasmer, WAVM, or wasmtime. Personally I'm using the standalone build in the Wazero runtime to be used in the go-libheif library, this allows for a go library that is fully contained (because the WASM is embedded in the library) and doesn't need external native libraries, it also allows the library to run fully sandboxed. The only downside is that it's slower than CGO, and multithreaded support is pretty bad right now, which causes the decoding to be around 3x as slow as CGO.

fi

if [ "$DEBUG" = "1" ]; then
echo "Building in debug mode"
RELEASE_BUILD_FLAGS="--profile -g"
fi

emcc "$LIBHEIFA" \
-s EXPORTED_FUNCTIONS="$EXPORTED_FUNCTIONS,_free,_malloc,_memcpy" \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s INVOKE_RUN=0 \
-s DOUBLE_MODE=0 \
-s PRECISE_F32=0 \
-s DISABLE_EXCEPTION_CATCHING=1 \
-s USE_CLOSURE_COMPILER=0 \
-s LEGACY_VM_SUPPORT=1 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed all the old flags here, I don't think they are neccesary.

Copy link
Contributor

Choose a reason for hiding this comment

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

When not building to WASM, but plain JS, --memory-init-file 0 avoids the generation of an extra file libheif.js.mem. Thus we may keep this to stay compatible to existing applications.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@farindk Ah I did not even know it was possible to only emit JS, was that what the previous version had as output?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, previously, it was a single libheif.js file. Thus, we have to still support this that other software that depends on this, continues to work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@farindk Shouldn't USE_WASM have a default value of 0 then?

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. But I think in this case, I would take the risk to change the default because WASM is clearly the future and it doesn't change anything for the end-user. Just of the ones building new libheif.js files.
I am currently thinking about even doing larger changes, so it will be worth for them having a look anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have notified the following repo of the change: catdad-experiments/libheif-emscripten#19

What kind of other changes do you want to make? I think it might be interesting to look at thread support (for the browser it's possible).

-s LLD_REPORT_UNDEFINED \
--memory-init-file 0 \
-O3 \
-std=c++11 \
-L${DIR}/libde265-${LIBDE265_VERSION}/libde265/.libs \
-lde265 \
--pre-js pre.js \
--post-js post.js \
-o libheif.js
$LIBRARY_INCLUDE_FLAGS \
$LIBRARY_LINKER_FLAGS \
$BUILD_FLAGS \
$RELEASE_BUILD_FLAGS
2 changes: 1 addition & 1 deletion libheif/heif.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
#include <set>
#include <limits>

#if defined(__EMSCRIPTEN__)
#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_STANDALONE_WASM__)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: __EMSCRIPTEN_STANDALONE_WASM__ isn't something that Emscripten provides, I add this using CMAKE_C_FLAGS / CMAKE_CXX_FLAGS. Embind is not supported on non-web runtimes since it heavily relies on JS, so we do not want the Embind things to be injected in the standalone build.

#include "heif_emscripten.h"
#endif

Expand Down
8 changes: 4 additions & 4 deletions libheif/heif_emscripten.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ static emscripten::val heif_js_decode_image(struct heif_image_handle* handle,
result.set("width", width);
int height = heif_image_get_height(image, heif_channel_Y);
result.set("height", height);
std::string data;
std::basic_string<unsigned char> data;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was needed because Embind was messing up the data transfer when using a normal std::string, however, the data transfer between strings looks very odd to me right now because I think what currently happens is:

  • WASM sends pointer to string to JS
  • JS reads binary data from pointer position
  • JS converts binary data into string
  • libheif converts string back to binary data (post.js)

I believe we can remove the string conversion, probably also speeds things up a bit. I'm currently talking to Emscripten what the best way to do that is in Embind.

Copy link
Contributor

Choose a reason for hiding this comment

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

Converning passing the data around: do you think this (answer item 3) might work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes! result.set("data", emscripten::val(emscripten::typed_memory_view(data.size(), data.data()))); seems to work perfectly! It will give you a Uint8Array directly in JS, however, I'm not completely sure who then becomes owner of the data buffer and who has to free it, would have to be looked into to prevent memory leaks.

result.set("chroma", heif_image_get_chroma_format(image));
result.set("colorspace", heif_image_get_colorspace(image));
switch (heif_image_get_colorspace(image)) {
Expand All @@ -127,7 +127,7 @@ static emscripten::val heif_js_decode_image(struct heif_image_handle* handle,
const uint8_t* plane_v = heif_image_get_plane_readonly(image,
heif_channel_Cr, &stride_v);
data.resize((width * height) + (2 * round_odd(width) * round_odd(height)));
char* dest = const_cast<char*>(data.data());
unsigned char* dest = const_cast<unsigned char*>(data.data());
strided_copy(dest, plane_y, width, height, stride_y);
strided_copy(dest + (width * height), plane_u,
round_odd(width), round_odd(height), stride_u);
Expand All @@ -142,7 +142,7 @@ static emscripten::val heif_js_decode_image(struct heif_image_handle* handle,
const uint8_t* plane_rgb = heif_image_get_plane_readonly(image,
heif_channel_interleaved, &stride_rgb);
data.resize(width * height * 3);
char* dest = const_cast<char*>(data.data());
unsigned char* dest = const_cast<unsigned char*>(data.data());
strided_copy(dest, plane_rgb, width * 3, height, stride_rgb);
}
break;
Expand All @@ -153,7 +153,7 @@ static emscripten::val heif_js_decode_image(struct heif_image_handle* handle,
const uint8_t* plane_grey = heif_image_get_plane_readonly(image,
heif_channel_Y, &stride_grey);
data.resize(width * height);
char* dest = const_cast<char*>(data.data());
unsigned char* dest = const_cast<unsigned char*>(data.data());
strided_copy(dest, plane_grey, width, height, stride_grey);
}
break;
Expand Down
34 changes: 0 additions & 34 deletions post.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,40 +175,6 @@ var libheif = {
}
};

var key;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has all moved to pre.js


// Expose enum values.
var enums = {
"heif_error_code": true,
"heif_suberror_code": true,
"heif_compression_format": true,
"heif_chroma": true,
"heif_colorspace": true,
"heif_channel": true
};
var e;
for (e in enums) {
if (!enums.hasOwnProperty(e)) {
continue;
}
for (key in Module[e]) {
if (!Module[e].hasOwnProperty(key) ||
key === "values") {
continue;
}

libheif[key] = Module[e][key];
}
}

// Expose internal C API.
for (key in Module) {
if (enums.hasOwnProperty(key) || key.indexOf("heif_") !== 0) {
continue;
}
libheif[key] = Module[key];
}

// don't pollute the global namespace
delete this['Module'];

Expand Down
44 changes: 43 additions & 1 deletion pre.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,47 @@ var Module = {
console.error(text);
},
canvas: {},
noInitialRun: true
noInitialRun: true,
onRuntimeInitialized: function() {
// Expose enum values.
var enums = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to move all this logic from post.js to pre.js inside the onRuntimeInitialized function. This is because these properties are not available yet in post.js since they are loaded later using embind, onRuntimeInitialized is triggered after loading embind. The interface still works the same because of this trick.

"heif_error_code": true,
"heif_suberror_code": true,
"heif_compression_format": true,
"heif_chroma": true,
"heif_colorspace": true,
"heif_channel": true
};
var e;
for (e in enums) {
if (!enums.hasOwnProperty(e)) {
continue;
}

for (key in this[e]) {
if (!this[e].hasOwnProperty(key) ||
key === "values") {
continue;
}

libheif[key] = this[e][key];
}
}

// Expose internal C API.
for (key in this) {
if (enums.hasOwnProperty(key.slice(1)) || key.indexOf("_heif_") !== 0) {
continue;
}
libheif[key.slice(1)] = this[key];
Copy link
Contributor Author

@jerbob92 jerbob92 Jul 18, 2023

Choose a reason for hiding this comment

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

In the LLVM build, C functions start with _, we strip the _ off here to create an alias, so that the implementations can stay the same.

}

// Expose embind API.
for (key in Module) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I alias the embind API after the C API because there are two functions that have the same name as in the C API (heif_context_read_from_memory and heif_get_version), we prefer the embind function over the C API function.

if (enums.hasOwnProperty(key) || key.indexOf("heif_") !== 0) {
continue;
}
libheif[key] = Module[key];
}
}
};
13 changes: 6 additions & 7 deletions scripts/install-emscripten.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ fi

LIBSTDC_BASE=http://de.archive.ubuntu.com/ubuntu/pool/main/g/gcc-5
EMSDK_DOWNLOAD=https://github.com/emscripten-core/emsdk.git
EMSDK_VERSION=3.1.29

CODENAME=$(/usr/bin/lsb_release --codename --short)
if [ "$CODENAME" = "trusty" ] && [ ! -e "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21" ]; then
Expand All @@ -33,13 +32,13 @@ if [ ! -d emsdk ]; then
fi

cd emsdk
echo "Updating SDK base to ${EMSDK_VERSION} ..."
echo "Updating SDK base to ${VERSION} ..."
Copy link
Contributor Author

@jerbob92 jerbob92 Jul 18, 2023

Choose a reason for hiding this comment

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

I have removed the EMSDK_VERSION and use VERSION now, the version that gets installed is always the same as the SDK.

git checkout main
git pull --verbose
git checkout ${EMSDK_VERSION}
git checkout ${VERSION}

echo "Installing SDK version ${VERSION} ..."
./emsdk install sdk-fastcomp-${VERSION}-64bit
echo "Installing SDK version latest ..."
./emsdk install ${VERSION}

echo "Activating SDK version ${VERSION} ..."
./emsdk activate sdk-fastcomp-${VERSION}-64bit
echo "Activating SDK version latest ..."
./emsdk activate ${VERSION}
Loading