diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e2af0554..5dd69cbd 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -230,9 +230,20 @@ jobs: # whole run, and every job downstream of it, pending until GitHub's 6-hour # default fires. # - # 20 is ~5x the slowest observed leg (libjxl, 239s) and leaves room for a - # cold emsdk image pull. - timeout-minutes: 20 + # 50, not the 20 this started at. That 20 was set as ~5x the slowest leg + # then observed (libjxl, 239s), but libjxl is far more variable than one + # observation suggested: on #79 its Build step ran 18m42s on an ordinary + # hosted runner — 4.5x that figure, and a cache hit on dependencies, so the + # time went into the compile itself — and the job was cancelled at the + # 20-minute bound despite the PR changing nothing under packages/libjxl. + # The same leg had taken 4m18s on #93 twenty minutes earlier. + # + # A bound calibrated to a fast observation converts ordinary runner + # variance into a red check, and a cancelled leg costs a full CI cycle to + # tell apart from a real failure. 50 still catches the wedge this exists + # for (the unbounded leg above ran 80+ minutes) and leaves room for both a + # slow libjxl compile and a cold emsdk image pull. + timeout-minutes: 50 runs-on: ubuntu-latest container: image: emscripten/emsdk:3.1.74 diff --git a/packages/libjpeg-turbo-12bit/CMakeLists.txt b/packages/libjpeg-turbo-12bit/CMakeLists.txt index a1666b9a..5df376e0 100644 --- a/packages/libjpeg-turbo-12bit/CMakeLists.txt +++ b/packages/libjpeg-turbo-12bit/CMakeLists.txt @@ -31,11 +31,14 @@ if(NOT EXISTS "${PROJECT_SOURCE_DIR}/extern/libjpeg-turbo/CMakeLists.txt") message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.") endif() -option(ENABLE_SHARED "" OFF) -option(ENABLE_STATIC "" ON) - -# add the external library -add_subdirectory(extern/libjpeg-turbo EXCLUDE_FROM_ALL) +# libjpeg-turbo 3.x refuses add_subdirectory() integration and dropped the +# WITH_12BIT build flag (a single build is now multi-precision: 8/12/16-bit). +# build.sh builds it as a separate project and passes its build dir in +# LIBJPEG_TURBO_BUILD_DIR; src/CMakeLists.txt links the produced libjpeg.a and +# the decoder uses the 3.x jpeg12_* API for 12-bit samples. +if(EMSCRIPTEN AND NOT DEFINED LIBJPEG_TURBO_BUILD_DIR) + message(FATAL_ERROR "LIBJPEG_TURBO_BUILD_DIR not set — run build.sh, which builds libjpeg-turbo first and passes its build dir.") +endif() # add the js wrapper if(EMSCRIPTEN) diff --git a/packages/libjpeg-turbo-12bit/build.sh b/packages/libjpeg-turbo-12bit/build.sh index 46f47ccc..4f746531 100644 --- a/packages/libjpeg-turbo-12bit/build.sh +++ b/packages/libjpeg-turbo-12bit/build.sh @@ -1,16 +1,27 @@ #!/bin/sh # Disable exit on non 0 set +e -rm -rf dist -mkdir -p build -mkdir -p dist +rm -rf build build-libjpeg dist +mkdir -p build build-libjpeg dist -# DEBUG CONFIGURE -#(cd build && emcmake cmake -DCMAKE_BUILD_TYPE=Debug ..) && +# libjpeg-turbo 3.x forbids add_subdirectory() and dropped WITH_12BIT — a single +# build is now multi-precision (8/12/16-bit), exposing jpeg12_* APIs. So build +# libjpeg-turbo as a SEPARATE project first (Release, WITH_SIMD=0 for parity, +# WITH_SPNG=0 to avoid the new zlib/spng dep), then link its libjpeg.a; the +# 12-bit decoder uses jpeg12_read_scanlines. +echo "~~~ CONFIGURE libjpeg-turbo 3.x (standalone, multi-precision) ~~~" +(cd build-libjpeg && emcmake cmake -G"Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_SHARED=0 -DENABLE_STATIC=1 \ + -DWITH_SIMD=0 -DWITH_SPNG=0 \ + ../extern/libjpeg-turbo) +echo "~~~ MAKE libjpeg-turbo ~~~" +(cd build-libjpeg && emmake make VERBOSE=1 -j 16 jpeg-static) -echo "~~~ CONFIGURE ~~~" -(cd build && emcmake cmake -G"Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug -DWITH_12BIT=1 ..) -echo "~~~ MAKE ~~~" +echo "~~~ CONFIGURE wrapper ~~~" +LIBJPEG_TURBO_BUILD_DIR="$(cd build-libjpeg && pwd)" +(cd build && emcmake cmake -G"Unix Makefiles" -DLIBJPEG_TURBO_BUILD_DIR="$LIBJPEG_TURBO_BUILD_DIR" ..) +echo "~~~ MAKE wrapper ~~~" (cd build && emmake make VERBOSE=1 -j 16) echo "~~~ COPY ~~~ " cp ./build/src/libjpegturbo12wasm.js ./dist diff --git a/packages/libjpeg-turbo-12bit/extern/libjpeg-turbo b/packages/libjpeg-turbo-12bit/extern/libjpeg-turbo index dc4a93fa..c85e6b90 160000 --- a/packages/libjpeg-turbo-12bit/extern/libjpeg-turbo +++ b/packages/libjpeg-turbo-12bit/extern/libjpeg-turbo @@ -1 +1 @@ -Subproject commit dc4a93fab38b42d29b89a533409e012570180e28 +Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344 diff --git a/packages/libjpeg-turbo-12bit/src/CMakeLists.txt b/packages/libjpeg-turbo-12bit/src/CMakeLists.txt index 57e688c2..092bd625 100644 --- a/packages/libjpeg-turbo-12bit/src/CMakeLists.txt +++ b/packages/libjpeg-turbo-12bit/src/CMakeLists.txt @@ -1,6 +1,16 @@ - include_directories("../extern/libjpeg-turbo" "../build/extern/libjpeg-turbo") + # libjpeg-turbo 3.x moved public headers under src/; jconfig.h is generated + # into the standalone build dir. jpeg-static is consumed as a pre-built + # IMPORTED library (built separately by build.sh — 3.x forbids add_subdirectory). + include_directories( + "../extern/libjpeg-turbo/src" + "${LIBJPEG_TURBO_BUILD_DIR}" + "${LIBJPEG_TURBO_BUILD_DIR}/src") + + add_library(jpeg-static STATIC IMPORTED) + set_target_properties(jpeg-static PROPERTIES + IMPORTED_LOCATION "${LIBJPEG_TURBO_BUILD_DIR}/libjpeg.a") add_executable(libjpegturbo12js jslib.cpp) diff --git a/packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp b/packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp index 7687a528..56327447 100644 --- a/packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp +++ b/packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp @@ -135,68 +135,68 @@ class JPEGDecoder { } guard{cinfo}; jpeg_mem_src(&cinfo, encoded_.data(), encoded_.size()); - // Read file header, set default decompression parameters + // Read the header. In libjpeg-turbo 3.x this is precision-agnostic. jpeg_read_header(&cinfo, TRUE); - // Fail closed on multi-component images. This codec only supports - // single-component (grayscale) 12-bit JPEGs; forcing JCS_GRAYSCALE on a - // color image would make libjpeg silently discard the chroma channels - // and report componentCount=1, corrupting color data without any error. + + // This codec handles single-component (grayscale) 12-bit JPEGs only. Fail + // closed on color input: forcing JCS_GRAYSCALE on a multi-component image + // would silently drop chroma and mis-report componentCount=1. if (cinfo.num_components != 1) { throw std::runtime_error( "Unsupported 12-bit JPEG: expected 1 component (grayscale), got " + std::to_string(cinfo.num_components)); } - // Decode as single-component grayscale. This is a 12-bit-per-sample - // codec: each output value is a 16-bit-wide JSAMPLE (holding 0..4095), - // not an 8-bit RGBA quad. Previously this forced a 4-samples-per-pixel - // RGBA colorspace while the output buffer below was sized for 1 - // sample/pixel, causing libjpeg to write ~2x past the end of the - // allocated buffer (heap overflow). + // libjpeg-turbo 3.x is multi-precision in a single build, so a stream's + // precision is no longer implied by which library we linked; this codec + // only supports 12-bit samples. Reject others rather than mis-decode. + if (cinfo.data_precision != 12) { + throw std::runtime_error( + "Unsupported JPEG precision: expected 12-bit, got " + + std::to_string(cinfo.data_precision)); + } + + // Decode as single-component grayscale: one 12-bit sample per pixel held + // in a 16-bit-wide J12SAMPLE, not an 8-bit RGBA quad. This once forced a + // 4-samples-per-pixel RGBA colorspace while the output buffer below was + // sized for 1 sample/pixel, so libjpeg wrote ~2x past the end of it. cinfo.out_color_space = JCS_GRAYSCALE; jpeg_start_decompress(&cinfo); - frameInfo_.width = cinfo.output_width; frameInfo_.height = cinfo.output_height; frameInfo_.bitsPerSample = 12; frameInfo_.componentCount = 1; - // Prepare output buffer. One JSAMPLE (short, holding 0..4095) per pixel - // since output is single-component grayscale. - const int pixelFormat = 1; - - // Compute the output size (in samples) using a checked 64-bit multiply - // capped at 512 MiB so a malformed/adversarial header cannot overflow - // the size computation or force an unbounded allocation. - constexpr uint64_t kMaxOutputSamples = 512ull * 1024ull * 1024ull; // 512 MiB worth of samples + // One 12-bit sample per pixel, stored in a 16-bit-wide J12SAMPLE (short), + // so the sample count is just width * height. Overflow-checked and capped + // at 512 MiB of samples so a malformed or adversarial header cannot + // overflow the computation or force an unbounded allocation. + constexpr uint64_t kMaxOutputSamples = 512ull * 1024ull * 1024ull; const uint64_t width64 = static_cast(cinfo.output_width); const uint64_t height64 = static_cast(cinfo.output_height); - const uint64_t pixelFormat64 = static_cast(pixelFormat); - if (width64 == 0 || height64 == 0) { throw std::runtime_error("Invalid JPEG dimensions (zero width or height)"); } - uint64_t output_size64 = width64 * height64; - if (output_size64 / width64 != height64) { - // width * height overflowed - throw std::runtime_error("Overflow computing decoded buffer size"); - } - output_size64 *= pixelFormat64; - if (output_size64 == 0 || output_size64 > kMaxOutputSamples) { - throw std::runtime_error("Decoded buffer size exceeds allowed maximum or is invalid"); + if (output_size64 / width64 != height64 || output_size64 == 0 || + output_size64 > kMaxOutputSamples) { + throw std::runtime_error("Decoded buffer size out of range"); } - const size_t output_size = static_cast(output_size64); decoded_.resize(output_size); - - const size_t stride = static_cast(cinfo.output_width) * static_cast(pixelFormat); - - // Process data + const size_t stride = static_cast(cinfo.output_width); + + // 12-bit precision decodes through jpeg12_read_scanlines with a + // J12SAMPARRAY (short-based) — the libjpeg-turbo 3.x per-precision API. + // Under 2.x the library was compiled WITH_12BIT=1 and plain + // jpeg_read_scanlines *was* the 12-bit entry point; 3.x carries all + // precisions in one build, so the call has to name the precision. + // decoded_ is std::vector, matching J12SAMPLE. while (cinfo.output_scanline < cinfo.output_height) { - int16_t* output_data = &decoded_[stride * cinfo.output_scanline]; - (void)jpeg_read_scanlines(&cinfo, &output_data, 1); + J12SAMPROW output_data = + reinterpret_cast(&decoded_[stride * cinfo.output_scanline]); + (void)jpeg12_read_scanlines(&cinfo, &output_data, 1); } jpeg_finish_decompress(&cinfo); // DecompressGuard releases the decompress object -- a good deal of memory diff --git a/packages/libjpeg-turbo-8bit/CMakeLists.txt b/packages/libjpeg-turbo-8bit/CMakeLists.txt index a1666b9a..9cd7f2a6 100644 --- a/packages/libjpeg-turbo-8bit/CMakeLists.txt +++ b/packages/libjpeg-turbo-8bit/CMakeLists.txt @@ -31,11 +31,13 @@ if(NOT EXISTS "${PROJECT_SOURCE_DIR}/extern/libjpeg-turbo/CMakeLists.txt") message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.") endif() -option(ENABLE_SHARED "" OFF) -option(ENABLE_STATIC "" ON) - -# add the external library -add_subdirectory(extern/libjpeg-turbo EXCLUDE_FROM_ALL) +# NOTE: libjpeg-turbo 3.x refuses add_subdirectory() integration (it asserts it +# is the top-level project and errors out). It is now built as a separate +# project by build.sh, which passes its build dir in LIBJPEG_TURBO_BUILD_DIR; +# src/CMakeLists.txt links the produced libturbojpeg.a as an IMPORTED target. +if(EMSCRIPTEN AND NOT DEFINED LIBJPEG_TURBO_BUILD_DIR) + message(FATAL_ERROR "LIBJPEG_TURBO_BUILD_DIR not set — run build.sh, which builds libjpeg-turbo first and passes its build dir.") +endif() # add the js wrapper if(EMSCRIPTEN) diff --git a/packages/libjpeg-turbo-8bit/build.sh b/packages/libjpeg-turbo-8bit/build.sh index 185b1cec..2ab90bf4 100644 --- a/packages/libjpeg-turbo-8bit/build.sh +++ b/packages/libjpeg-turbo-8bit/build.sh @@ -1,18 +1,27 @@ #!/bin/sh # Disable exit on non 0 set +e -mkdir -p build -mkdir -p dist +rm -rf build build-libjpeg +mkdir -p build build-libjpeg dist -# DEBUG CONFIGURE -#(cd build && emcmake cmake -DCMAKE_BUILD_TYPE=Debug ..) && +# libjpeg-turbo 3.x forbids add_subdirectory() integration (its CMake asserts +# it is the top-level project). So we build it as a SEPARATE project first, +# then link the produced static lib (libturbojpeg.a) into our wasm wrapper. +# WITH_SIMD=0 keeps parity with the previous build; WITH_SPNG=0 avoids the new +# 3.x zlib/spng dependency (only used by the tj* tools, not our decode/encode). +echo "~~~ CONFIGURE libjpeg-turbo 3.x (standalone) ~~~" +(cd build-libjpeg && emcmake cmake -G"Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_SHARED=0 -DENABLE_STATIC=1 \ + -DWITH_SIMD=0 -DWITH_SPNG=0 -DWITH_TURBOJPEG=1 \ + ../extern/libjpeg-turbo) +echo "~~~ MAKE libjpeg-turbo ~~~" +(cd build-libjpeg && emmake make VERBOSE=1 -j 16 turbojpeg-static jpeg-static) -echo "~~~ CONFIGURE ~~~" -# Only include decoding to make it smaller -# see https://github.com/libjpeg-turbo/libjpeg-turbo/issues/431 -(cd build && emcmake cmake -G"Unix Makefiles" ..) -#(cd build && emcmake cmake -G"Unix Makefiles"..) -echo "~~~ MAKE ~~~" +echo "~~~ CONFIGURE wrapper ~~~" +LIBJPEG_TURBO_BUILD_DIR="$(cd build-libjpeg && pwd)" +(cd build && emcmake cmake -G"Unix Makefiles" -DLIBJPEG_TURBO_BUILD_DIR="$LIBJPEG_TURBO_BUILD_DIR" ..) +echo "~~~ MAKE wrapper ~~~" (cd build && emmake make VERBOSE=1 -j 16) echo "~~~ COPY ~~~ " cp ./build/src/libjpegturbowasm.js ./dist diff --git a/packages/libjpeg-turbo-8bit/extern/libjpeg-turbo b/packages/libjpeg-turbo-8bit/extern/libjpeg-turbo index dc4a93fa..c85e6b90 160000 --- a/packages/libjpeg-turbo-8bit/extern/libjpeg-turbo +++ b/packages/libjpeg-turbo-8bit/extern/libjpeg-turbo @@ -1 +1 @@ -Subproject commit dc4a93fab38b42d29b89a533409e012570180e28 +Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344 diff --git a/packages/libjpeg-turbo-8bit/src/CMakeLists.txt b/packages/libjpeg-turbo-8bit/src/CMakeLists.txt index 433ff18e..e6574954 100644 --- a/packages/libjpeg-turbo-8bit/src/CMakeLists.txt +++ b/packages/libjpeg-turbo-8bit/src/CMakeLists.txt @@ -1,6 +1,17 @@ - include_directories("../extern/libjpeg-turbo" "../build/extern/libjpeg-turbo") + # libjpeg-turbo 3.x moved its public headers under src/; jconfig.h is + # generated into the standalone build dir. turbojpeg-static is consumed as + # a pre-built IMPORTED library (built separately by build.sh — 3.x forbids + # add_subdirectory). + include_directories( + "../extern/libjpeg-turbo/src" + "${LIBJPEG_TURBO_BUILD_DIR}" + "${LIBJPEG_TURBO_BUILD_DIR}/src") + + add_library(turbojpeg-static STATIC IMPORTED) + set_target_properties(turbojpeg-static PROPERTIES + IMPORTED_LOCATION "${LIBJPEG_TURBO_BUILD_DIR}/libturbojpeg.a") add_executable(libjpegturbojs jslib.cpp) diff --git a/tools/dist-size/baseline.json b/tools/dist-size/baseline.json index 6331b39b..4aa5b887 100644 --- a/tools/dist-size/baseline.json +++ b/tools/dist-size/baseline.json @@ -33,42 +33,42 @@ }, "libjpeg-turbo-12bit": { "libjpegturbo12js.js": { - "raw": 2553272, - "gzip": 266948 + "raw": 599145, + "gzip": 146341 }, "libjpegturbo12wasm.js": { - "raw": 112153, - "gzip": 28570 + "raw": 55354, + "gzip": 15020 }, "libjpegturbo12wasm.wasm": { - "raw": 2238031, - "gzip": 760773 + "raw": 278203, + "gzip": 93433 } }, "libjpeg-turbo-8bit": { "libjpegturbojs.js": { - "raw": 838153, - "gzip": 160171 + "raw": 1076703, + "gzip": 212348 }, "libjpegturbojs_decode.js": { - "raw": 418292, - "gzip": 118256 + "raw": 639223, + "gzip": 157179 }, "libjpegturbowasm.js": { - "raw": 57440, - "gzip": 15274 + "raw": 57640, + "gzip": 15292 }, "libjpegturbowasm.wasm": { - "raw": 448955, - "gzip": 96494 + "raw": 555769, + "gzip": 136985 }, "libjpegturbowasm_decode.js": { - "raw": 56350, - "gzip": 15191 + "raw": 56736, + "gzip": 15229 }, "libjpegturbowasm_decode.wasm": { - "raw": 180512, - "gzip": 70074 + "raw": 299002, + "gzip": 99458 } }, "libjxl": {