commit ff09371267315c39fe0220082943c5834db04ab9 Author: jpegxl-bot Date: Fri Dec 27 18:12:34 2019 +0100 Update JPEG-XL with latest changes. This includes all changes up to 2019-12-27 17:38:53 +0100. diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..a61b61c56 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: Google +IncludeCategories: + - Regex: '^ +# instead . +# - modernize-return-braced-init-list: this often doesn't improve readability. +# - modernize-use-auto: is too aggressive towards using auto. +# - modernize-use-default-member-init: with a mix of constructors and default +# member initialization this can be confusing if enforced. +# - modernize-use-using: typedefs are ok. +# +# - readability-else-after-return: It doesn't always improve readability. +# - readability-static-accessed-through-instance +# It is often more useful and readable to access a constant of a passed +# variable (like d.N) instead of using the type of the variable that could be +# long and complex. + +Checks: >- + clang-*, + -clang-diagnostic-unused-command-line-argument, + google-*, + modernize-*, + performance-*, + readability-*, + -google-readability-todo, + -modernize-deprecated-headers, + -modernize-return-braced-init-list, + -modernize-use-auto, + -modernize-use-default-member-init, + -modernize-use-using, + -readability-else-after-return, + -readability-static-accessed-through-instance, + + +WarningsAsErrors: >- + clang-*, + clang-analyzer-*, + -clang-diagnostic-unused-command-line-argument, + google-build-using-namespace, + google-explicit-constructor, + google-readability-namespace-comments + +# We are only interested in the headers from this projects, excluding +# third_party/ and build/. +HeaderFilterRegex: '^.*/(jxl|fuif|tools)/.*\.h$' + +CheckOptions: + - key: readability-braces-around-statements.ShortStatementLines + value: '2' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '2' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: '1' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: '1' diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..cc4295c07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Build output directories +/build +/build* +/docker/*.log + +# The downloaded corpora files for benchmark. +/third_party/corpora diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..ea5cff7aa --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,548 @@ +# Copyright (c) the JPEG XL Project +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +# We define only two stages for development. The "build" stage only compiles the +# code and doesn't run any target code. The "test" stage runs the code compiled +# by the previous stage. Stages form dependencies in a block, all the build +# targets in one stage need to be completed before any of the next stage can +# start. +stages: + - build + - test + - deploy + +variables: + # We need to fetch the third_party dependencies together with the code unless + # otherwise noted. + GIT_SUBMODULE_STRATEGY: recursive + +# A template for running in the generic cloud builders. These are tagged with +# "linux" and run on shared VMs. +.linux_builder_template: &linux_builder_template + image: &jpegxl-builder gcr.io/jpegxl/jpegxl-builder@sha256:b4744eed15a00e88d6faf54798c8d8bca8b58a581bfdda374cfa8c668ec6c1b2 + tags: + - linux + # By default all the workflows run on master and on request. This can be + # override by users of this template. + only: + - master + - tags + - schedules + # Retry on system failures. This can be typically caused by the runner or VM + # being evicted. + retry: + max: 2 + when: + - unknown_failure + - api_failure + - stuck_or_timeout_failure + - runner_system_failure + +# A template for building on x86_64 images on the cloud builders. +.linux_x86_64_template: &linux_x86_64_template + <<: *linux_builder_template + variables: + CC: clang-6.0 + CXX: clang++-6.0 + +.linux_i686_template: &linux_i686_template + <<: *linux_builder_template + variables: + CC: clang-6.0 + CXX: clang++-6.0 + # This makes cross-compiling for i686 the default when calling ci.sh. + BUILD_TARGET: i686-linux-gnu + BUILD_DIR: build + +# A template for building on aarch64 images on the cloud builders. These may not +# run on actual aarch64 hardware. +.linux_aarch64_template: &linux_aarch64_template + <<: *linux_builder_template + variables: + CC: clang-6.0 + CXX: clang++-6.0 + # This makes cross-compiling for aarch64 the default when calling ci.sh. + BUILD_TARGET: aarch64-linux-gnu + BUILD_DIR: build + # We need to fetch the third_party dependencies together with the code. + GIT_SUBMODULE_STRATEGY: recursive + +# A template for cross-compiling for arm-linux-gnueabihf. +.linux_armhf_template: &linux_armhf_template + <<: *linux_builder_template + variables: + CC: clang-6.0 + CXX: clang++-6.0 + # This makes cross-compiling for armhf the default when calling ci.sh. + BUILD_TARGET: arm-linux-gnueabihf + BUILD_DIR: build + # We need to fetch the third_party dependencies together with the code. + GIT_SUBMODULE_STRATEGY: recursive + +# Common artifacts for the build stage +.default_build_paths: &default_build_paths + - build/libjpegxl.so* + - build/tools/cjpegxl + - build/tools/djpegxl + - build/tools/benchmark_xl + - build/tools/decode_and_encode + - build/tools/comparison_viewer/compare_codecs + - build/tools/comparison_viewer/compare_images + - build/tools/viewer/viewer + # All the test binaries in the build/ directory and .cmake files used by + # ctest, compressed into this file together to save space when uploading. + - build/tests.tar.xz + # The coverage data produced at compile time. + - build/gcno.tar.xz + +.linux_x86_64_build_template: &linux_x86_64_build_template + <<: *linux_x86_64_template + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: *default_build_paths + +.linux_i686_build_template: &linux_i686_build_template + <<: *linux_i686_template + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: *default_build_paths + +.linux_aarch64_build_template: &linux_aarch64_build_template + <<: *linux_aarch64_template + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: *default_build_paths + +.linux_armhf_build_template: &linux_armhf_build_template + <<: *linux_armhf_template + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: *default_build_paths + +# The default Release build runs always. +build:x86_64:clang:release: + <<: *linux_x86_64_build_template + stage: build + only: + - master + - merge_requests + - schedules + - tags + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh release -DCMAKE_INSTALL_PREFIX=`pwd`/prefix + # Test that the package can be installed. + - ninja -C build install + +test:x86_64:clang:release: + <<: *linux_x86_64_template + stage: test + only: + - master + - merge_requests + - schedules + - tags + dependencies: + - build:x86_64:clang:release + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +# x86_84 test coverage build +build:x86_64:clang:coverage: + <<: *linux_x86_64_build_template + stage: build + only: + - master + - schedules + - tags + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh coverage + +test:x86_64:clang:coverage: + <<: *linux_x86_64_template + stage: test + only: + - master + - schedules + - tags + dependencies: + - build:x86_64:clang:coverage + script: + - ./ci.sh test + - ./ci.sh coverage_report + variables: + CC: clang-6.0 + CXX: clang++-6.0 + # Headers from submodules might be needed when generating the HTML reports. + GIT_SUBMODULE_STRATEGY: recursive + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/coverage.txt + - build/coverage*.html + - build/coverage.xml + +# Updates the "Pages" section in GitLab with the latest coverage report from +# master. +pages: + <<: *linux_x86_64_template + stage: deploy + only: + - master + dependencies: + - test:x86_64:clang:coverage + script: + - mkdir public + - mv build/coverage* public/ + - mv public/coverage.html public/index.html + - sed -E "s,GCC Code Coverage Report,Coverage Report for ${CI_COMMIT_SHORT_SHA}," -i public/index.html + artifacts: + paths: + - public + + +# x86_64 Scalar-only release runs only on master and on request. +build:x86_64:clang:scalar: + <<: *linux_x86_64_build_template + stage: build + only: + - master + - merge_requests + - schedules + - tags + script: + - SKIP_TEST=1 PACK_TEST=1 CMAKE_CXX_FLAGS="-DHWY_STATIC_TARGETS=0 -DHWY_RUNTIME_TARGETS=0" ./ci.sh release + +test:x86_64:clang:scalar: + <<: *linux_x86_64_template + stage: test + dependencies: + - build:x86_64:clang:scalar + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +# i686 (x86 32-bit) builders +build:i686:clang:release: + <<: *linux_i686_build_template + stage: build + only: + - master + - merge_requests + - schedules + - tags + script: + # AVX2 on 32-bit binaries hits clang compiler bugs. + - CMAKE_FLAGS="-DHWY_DISABLE_AVX2" SKIP_TEST=1 PACK_TEST=1 + ./ci.sh release -DJPEGXL_ENABLE_TCMALLOC=OFF -DJPEGXL_ENABLE_FUZZERS=OFF + +test:i686:clang:release: + <<: *linux_i686_template + stage: test + dependencies: + - build:i686:clang:release + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +# aarch64 release runs only on master and on request. +build:aarch64:clang:release: + <<: *linux_aarch64_build_template + stage: build + only: + - master + - merge_requests + - schedules + - tags + - web + script: + - CMAKE_CXX_FLAGS="-DJXL_DISABLE_SLOW_TESTS" SKIP_TEST=1 PACK_TEST=1 + ./ci.sh release + +test:aarch64:clang:release: + <<: *linux_aarch64_template + stage: test + dependencies: + - build:aarch64:clang:release + script: + # LeakSanitizer doesn't work when running under qemu-arm. + - ASAN_OPTIONS=detect_leaks=0 ./ci.sh test + +# arm release +build:armhf:clang:release: + <<: *linux_armhf_build_template + stage: build + only: + - master + - merge_requests + - schedules + - tags + - web + script: + - CMAKE_CXX_FLAGS="-DJXL_DISABLE_SLOW_TESTS" SKIP_TEST=1 PACK_TEST=1 + ./ci.sh release + +test:armhf:clang:release: + <<: *linux_armhf_template + stage: test + dependencies: + - build:armhf:clang:release + script: + - ./ci.sh test + +# arm debug +build:armhf:clang:debug: + <<: *linux_armhf_build_template + stage: build + only: + - merge_requests + script: + - SKIP_TEST=1 ./ci.sh debug + +# "debug" build enabled for merge_requests. This is here to check that +# debug-only statements still compile. +build:x86_64:clang:debug: + <<: *linux_x86_64_build_template + stage: build + only: + - merge_requests + script: + - SKIP_TEST=1 ./ci.sh debug + +# "asan", "tsan" and "msan" builds only run on master, tags and when requested +# from the web. Only supported in x86_64 for now. +build:x86_64:clang:asan: + <<: *linux_x86_64_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh asan + +test:x86_64:clang:asan: + <<: *linux_x86_64_template + stage: test + dependencies: + - build:x86_64:clang:asan + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +# aarch64 asan build and test. +build:aarch64:clang:asan: + <<: *linux_aarch64_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh asan + +test:aarch64:clang:asan: + <<: *linux_aarch64_template + stage: test + dependencies: + - build:aarch64:clang:asan + # Disable asan test on arm since they timeout. + only: + - tags + script: + - ./ci.sh test + +build:x86_64:clang:tsan: + <<: *linux_x86_64_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh tsan + +test:x86_64:clang:tsan: + <<: *linux_x86_64_template + stage: test + dependencies: + - build:x86_64:clang:tsan + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +build:x86_64:clang:msan: + <<: *linux_x86_64_build_template + stage: build + script: + - SKIP_TEST=1 PACK_TEST=1 ./ci.sh msan + +test:x86_64:clang:msan: + <<: *linux_x86_64_template + stage: test + dependencies: + - build:x86_64:clang:msan + script: + - ./ci.sh test + variables: + GIT_SUBMODULE_STRATEGY: none + +# Merge request linter checks. +build:lint: + <<: *linux_x86_64_template + stage: build + variables: + GIT_SUBMODULE_STRATEGY: none + only: + - merge_requests + script: + - LINT_OUTPUT=clang-format.patch ./ci.sh lint + allow_failure: true + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 month + when: on_failure + paths: + - clang-format.patch + +# Most clang-tidy checks are currently informational only since these are +# currently broken, however several are marked as errors. +build:tidy: + <<: *linux_x86_64_template + stage: build + only: + - merge_requests + script: + - CMAKE_BUILD_TYPE="Release" ./ci.sh tidy + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 month + when: always + paths: + - build/clang-tidy.txt + +# Faster benchmark over a smaller set of images. +.benchmark_x86_64_template: &benchmark_x86_64_template + <<: *linux_x86_64_template + stage: test + variables: + GIT_SUBMODULE_STRATEGY: none + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + +test:fast_benchmark:release: + <<: *benchmark_x86_64_template + only: + - master + - merge_requests + - schedules + - tags + dependencies: + - build:x86_64:clang:release + script: + - STORE_IMAGES=0 ./ci.sh fast_benchmark + +test:fast_benchmark:asan: + <<: *benchmark_x86_64_template + dependencies: + - build:x86_64:clang:asan + script: + - STORE_IMAGES=0 ./ci.sh fast_benchmark + +test:fast_benchmark.msan: + <<: *benchmark_x86_64_template + dependencies: + - build:x86_64:clang:msan + script: + - STORE_IMAGES=0 ./ci.sh fast_benchmark + +# This template runs on actual aarch64 hardware. +.benchmark_aarch64_template: &benchmark_aarch64_template + <<: *linux_builder_template + image: gcr.io/jpegxl/jpegxl-builder-run-aarch64@sha256:4ff7fe5e3e1d19a8c4604b72a41916173d83ec95d335f7fabd599ad92c438df2 + stage: test + tags: + - aarch64 + variables: + GIT_SUBMODULE_STRATEGY: none + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + +test:aarch64:fast_benchmark:release: + <<: *benchmark_aarch64_template + dependencies: + - build:aarch64:clang:release + script: + # Bind to the big CPUs only. + - ./ci.sh cpuset "${RUNNER_CPU_BIG}" + - STORE_IMAGES=0 ./ci.sh fast_benchmark + +# Benchmark test that runs separately on the big and little CPUs of the runner. +test:aarch64:arm_benchmark:release: + <<: *benchmark_aarch64_template + only: + - web + - master + dependencies: + - build:aarch64:clang:release + script: + - ./ci.sh arm_benchmark + +# Benchmark ToT on nightly builds. These are scheduled at midnight UTC. +test:benchmark:release: + image: *jpegxl-builder + stage: test + only: + - schedules + variables: + GIT_SUBMODULE_STRATEGY: none + dependencies: + - build:x86_64:clang:release + script: + - STORE_IMAGES=0 ./ci.sh benchmark + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + paths: + - build/benchmark_results/ + +build:tidy:all: + <<: *linux_x86_64_template + stage: build + only: + - master + script: + - CMAKE_BUILD_TYPE="Release" ./ci.sh tidy all + allow_failure: true + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 year + when: always + paths: + - build/clang-tidy.txt + +# Emscripten (WASM) build. +build:ems:all: + <<: *linux_x86_64_template + stage: build + variables: + CC: emcc + CXX: em++ + EMSDK: /opt/emsdk + EM_CONFIG: /opt/emsdk/.emscripten + CMAKE_TOOLCHAIN_FILE: /opt/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" + expire_in: 1 week + paths: + - build/jpegxl_emcc.* + script: + - source "${EMSDK}/emsdk_env.sh" + - SKIP_TEST=1 ./ci.sh release diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..8c9e574a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,24 @@ +[submodule "third_party/brotli"] + path = third_party/brotli + url = https://github.com/google/brotli +[submodule "third_party/lodepng"] + path = third_party/lodepng + url = https://github.com/lvandeve/lodepng +[submodule "third_party/lcms"] + path = third_party/lcms + url = https://github.com/mm2/Little-CMS +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest +[submodule "third_party/gperftools"] + path = third_party/gperftools + url = https://github.com/gperftools/gperftools +[submodule "third_party/brunsli"] + path = third_party/brunsli + url = https://github.com/google/brunsli.git +[submodule "third_party/sjpeg"] + path = third_party/sjpeg + url = https://github.com/webmproject/sjpeg.git +[submodule "third_party/skcms"] + path = third_party/skcms + url = https://skia.googlesource.com/skcms diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..94d8717bc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,234 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Ubuntu bionic ships with cmake 3.10. +cmake_minimum_required(VERSION 3.10) + +# Set compilers to default to Clang is not set. +if ("$ENV{CC}" STREQUAL "") + set(ENV{CC} clang) +endif() +if ("$ENV{CXX}" STREQUAL "") + set(ENV{CXX} clang++) +endif() + +# Honor VISIBILITY_INLINES_HIDDEN on all types of targets. +if(POLICY CMP0063) + cmake_policy(SET CMP0063 NEW) +endif() +# Pass CMAKE_EXE_LINKER_FLAGS to CC and CXX compilers when testing if they work. +if(POLICY CMP0065) + cmake_policy(SET CMP0065 NEW) +endif() + +project(JPEGXL LANGUAGES C CXX) + +include(CheckCXXSourceCompiles) +check_cxx_source_compiles( + "int main() { + #if !defined(__EMSCRIPTEN__) + static_assert(false, \"__EMSCRIPTEN__ is not defined\"); + #endif + return 0; + }" + JPEGXL_EMSCRIPTEN +) +if(JPEGXL_EMSCRIPTEN) + message("-- Compiler is EMSCRIPTEN.") + set(JPEGXL_NOT_EMSCRIPTEN false) + set(CMAKE_STATIC_LIBRARY_SUFFIX ".bc") + set(CMAKE_EXECUTABLE_SUFFIX ".html") + add_definitions(-DPROFILER_ENABLED=0) +else() + message("-- Compiler is not EMSCRIPTEN.") + set(JPEGXL_NOT_EMSCRIPTEN true) +endif() + +### Project build options: +if(NOT APPLE AND NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64" + AND NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") + # Enabled by default except on arm64 and Apple builds. + set(ENABLE_FUZZERS_DEFAULT true) +endif() +if(NOT APPLE AND NOT JPEGXL_EMSCRIPTEN + AND NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64" + AND NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") + # Enabled by default except on arm64 and Apple builds. + set(ENABLE_TCMALLOC_DEFAULT true) +endif() + +set(JPEGXL_ENABLE_FUZZERS ${ENABLE_FUZZERS_DEFAULT} CACHE BOOL + "Build JPEGXL fuzzer targets") +set(JPEGXL_ENABLE_BENCHMARK ${JPEGXL_NOT_EMSCRIPTEN} CACHE BOOL + "Build JPEGXL benchmark tools") +set(JPEGXL_ENABLE_SJPEG true CACHE BOOL + "Build JPEGXL with support for encoding with sjpeg") +set(JPEGXL_ENABLE_SKCMS false CACHE BOOL + "Where possible, use skcms instead of lcms2") +set(JPEGXL_ENABLE_VIEWERS ${JPEGXL_NOT_EMSCRIPTEN} CACHE BOOL + "Build JPEGXL viewer tools for evaluation") +set(JPEGXL_ENABLE_TCMALLOC ${ENABLE_TCMALLOC_DEFAULT} CACHE BOOL + "Build JPEGXL using gperftools (tcmalloc) allocator.") +set(JPEGXL_ENABLE_PLUGINS true CACHE BOOL + "Build third-party plugings to support JPEG XL in other applications.") +set(JPEGXL_ENABLE_COVERAGE false CACHE BOOL + "Enable code coverage tracking for libjpegxl. This also enables debug and disables optimizations.") + +# Require clang for building. +if(NOT ${CMAKE_C_COMPILER_ID} MATCHES "Clang" OR + NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") + message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} compiler is not supported.\n" + "Use clang instead:\n CC=clang CXX=clang++ cmake ..") +endif() +if (${CMAKE_C_COMPILER_VERSION} VERSION_LESS 6 OR + ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 6) + message(FATAL_ERROR + "Minimum Clang version required is Clang 6, please update.") +endif() + +# CMAKE_EXPORT_COMPILE_COMMANDS is used to generate the compilation database +# used by clang-tidy. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Global compiler flags for all targets here and in subdirectories. +add_definitions( + # Avoid changing the binary based on the current time and date. + -D__DATE__="redacted" + -D__TIMESTAMP__="redacted" + -D__TIME__="redacted" +) + +# In CMake before 3.12 it is problematic to pass repeated flags like -Xclang. +# For this reason we place them in CMAKE_CXX_FLAGS instead. +# See https://gitlab.kitware.com/cmake/cmake/issues/15826 + +if (NOT "${JPEGXL_EMSCRIPTEN}") + +# Pretty colorful messages within reasonable limits. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -Xclang -ferror-limit -Xclang 19 -Xclang -fmessage-length -Xclang 0 \ + -Xclang -fdiagnostics-show-option -fcolor-diagnostics") + +# Machine flags. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -Xclang -mrelax-all \ + -Xclang -mrelocation-model -Xclang pic \ + -Xclang -pic-level -Xclang 2 \ + -Xclang -mconstructor-aliases \ + -Xclang -mpie-copy-relocations \ + -Xclang -munwind-tables") + +if(WIN32) +# Not supported by clang-cl, but frame pointers are default on Windows +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer") +endif() + +# CPU flags +if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -mavx2 \ + -mfma \ + -Xclang -mprefer-vector-width=128 \ + -Xclang -target-cpu -Xclang haswell \ + -Xclang -target-feature -Xclang +avx2") +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "AMD64") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /arch:AVX2") +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -Xclang -mprefer-vector-width=128 \ + -Xclang -target-cpu -Xclang cortex-a72") +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ + -mcpu=cortex-a7 -mfpu=neon-vfpv4") +endif() + +endif() # JPEGXL_EMSCRIPTEN + +# Force build with optimizations in release mode. +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") + +add_compile_options( + # Ignore this to allow redefining __DATE__ and others. + -Wno-builtin-macro-redefined + + # Global warning settings. + -Wall +) + +if (NOT WIN32) +add_compile_options(-Werror) +endif () + +include(GNUInstallDirs) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +add_subdirectory(third_party) + +set(THREADS_PREFER_PTHREAD_FLAG YES) +find_package(Threads REQUIRED) + +# Enable tests regardless of where are they defined. +enable_testing() +include(CTest) + +# The jpegxl library definition. This also defines the internal list of flags we +# use in our project (library, tests and tools): JPEGXL_INTERNAL_FLAGS. +include(jpegxl.cmake) + +if (NOT "${JPEGXL_EMSCRIPTEN}") + +# Other libraries outside the core jpegxl library. +include(jpegxl_extras.cmake) +include(jpegxl_threads.cmake) + +# Plugins for third-party software +if (${JPEGXL_ENABLE_PLUGINS}) +add_subdirectory(plugins) +endif () + +# Binary tools +add_subdirectory(tools) + +# Unittests +cmake_policy(SET CMP0057 NEW) # https://gitlab.kitware.com/cmake/cmake/issues/18198 +include(GoogleTest) + +# Tests for the jpegxl library. +include(jpegxltests.cmake) + +else() # JPEGXL_EMSCRIPTEN + +# WASM API facade. +add_executable(jpegxl_emcc jxl/jpegxl_emcc.cc) +target_link_libraries(jpegxl_emcc jpegxl-static) +set_target_properties(jpegxl_emcc PROPERTIES LINK_FLAGS "\ + -O3\ + -s TOTAL_MEMORY=256MB\ + -s EXPORTED_FUNCTIONS='[\ + \"_decompress\",\ + \"_free\",\ + \"_freeImage\",\ + \"_getImageWidth\",\ + \"_getImageHeight\",\ + \"_getImagePixels\",\ + \"_malloc\"\ + ]'\ +") + +endif () # JPEGXL_EMSCRIPTEN + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3e3c9f968 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to Contribute + +We are currently unable to accept patches to this project, but we'd very much +appreciate if you open a Gitlab issue for any bug reports/feature requests. +We will be happy to investigate and hopefully fix any issues. + +# Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 000000000..848096f92 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,23 @@ +# This files lists individuals who made significant contributions to the JPEG XL +# code base, such as design, adding features, performing experiments, ... +# Small changes such as a small bugfix or fixing spelling errors are not +# included. If you'd like to be included in this file thanks to a significant +# contribution, feel free to send a pull request changing this file. +Alex Deymo +Alexander Rhatushnyak +Evgenii Kliuchnikov +Iulia-Maria Comșa +Jan Wassenberg +Jon Sneyers +Jyrki Alakuijala +Krzysztof Potempa +Lode Vandevenne +Luca Versari +Martin Bruse +Moritz Firsching +Renata Khasanova +Robert Obryk +Sami Boukortt +Sebastian Gomez-Gonzalez +Thomas Fischbacher +Zoltan Szabadka diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..6b0b1270f --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.OSX.md b/README.OSX.md new file mode 100644 index 000000000..a2179848b --- /dev/null +++ b/README.OSX.md @@ -0,0 +1,39 @@ +## Disclaimer + +OSX builds have "best effort" support, i.e. build might not work at all, some +tests may fail and some sub-projects are excluded from build. + +This manual outlines OSX specific setup. For general building and testing +instructions see "[README](README.md)" and +"[Building and Testing changes](doc/building_and_testing.md)". + +## Dependencies + +[Homebrew](https://brew.sh/) is a popular package manager that can be used for +installing dependencies. + +Make sure that `brew doctor` does not report serious problems and up-to-date +version of XCode is installed. + +Installing (actually, building) `clang` might take a couple hours. + +```shell +brew install llvm +``` + +```shell +brew install clang-format coreutils cmake giflib libjpeg ninja parallel +``` + +If `git-clang-format` command is not accessible, run + +```shell +brew link --overwrite clang-format +``` + +Before building the project check that `which clang` is +`/usr/local/opt/llvm/bin/clang`, not the one provided by XCode. If not, update +`PATH` environment variable. + +Also, `export CMAKE_PREFIX_PATH=/usr/local/opt/giflib` might be necessary for +correct include paths resolving. diff --git a/README.md b/README.md new file mode 100644 index 000000000..21e959d6c --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# JPEG XL reference implementation + +This repository contains a reference implementation of JPEG XL (encoder and +decoder). As [previously announced](https://jpeg.org/items/20190803_press.html), +it is available under a royalty-free and open source license (Apache 2). + +**NOTE** + +JPEG XL is in the final stages of standardization, but minor changes to the +codestream are still likely. WARNING: until further notice, do not depend on +future decoders being able to decode the output of a current encoder. + +## Checking out the code + +This repository uses git submodules to handle some third party dependencies +under `third_party/`. To check out these dependencies as well clone the +repository with `--recursive`: + +```shell +git clone git@gitlab.com:wg1/jpeg-xl.git --recursive +``` + +If you didn't check out recursively, and after any update run the following +command to check out the git submodules. + +```shell +git submodule update --init --recursive +``` + +## Minimum build dependencies + +Apart from the dependencies in third_party, some of the tools use external +dependencies that need to be installed in your system first. For a Debian/Ubuntu +based Linux distribution install: + +```shell +sudo apt install cmake clang-6.0 g++-8 qtbase5-dev libqt5x11extras5-dev \ + extra-cmake-modules libgif-dev libjpeg-dev ninja-build +``` + +For developing changes in JPEG XL, take a look at the +[Building and Testing changes](doc/building_and_testing.md) guide. + +Make sure your default "clang" compiler is at least version 6 running + +```bash +clang --version +``` + +If it still shows an old version despite having a clang-6.0 installed, you need +to update the default clang compiler. In Debian-based systems run: + +```bash +sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-6.0 100 +sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-6.0 100 +``` + +## Building + +The project builds with cmake. We currently support Linux (tested with Debian). +To build the "release" version, you can use the following helper command: + +```shell +./ci.sh release +``` + +This will build and test the project, and leave the binaries in the `build/` +directory. Check out the `tools` subdirectory for command-line tools that +interact with the library. You can set the environment variable SKIP_TEST=1 to +skip the test stage. + +There are other build versions with more debug information useful when +developing. You can read more about it in the +[Building and Testing changes](doc/building_and_testing.md) guide. + +## Documentation + +* [Developing in GitLab](doc/developing_in_gitlab.md) +* [XL Overview](doc/xl_overview.md) +* [Building and Testing changes](doc/building_and_testing.md) +* [Software Contribution Guidelines](doc/guidelines.md) +* [JPEG XL committee draft](https://arxiv.org/abs/1908.03565) +* [Introductory paper](https://www.spiedigitallibrary.org/proceedings/Download?fullDOI=10.1117%2F12.2529237) diff --git a/ci.sh b/ci.sh new file mode 100755 index 000000000..00e03adb6 --- /dev/null +++ b/ci.sh @@ -0,0 +1,956 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Continuous integration helper module. This module is meant to be called from +# the .gitlab-ci.yml file during the continuous integration build, as well as +# from the command line for developers. + +set -eu + +OS=`uname -s` + +MYDIR=$(dirname $(realpath "$0")) + +### Environment parameters: +CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-RelWithDebInfo} +CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH:-} +SKIP_TEST="${SKIP_TEST:-0}" +BUILD_TARGET="${BUILD_TARGET:-}" +if [[ -n "${BUILD_TARGET}" ]]; then + BUILD_DIR="${BUILD_DIR:-${MYDIR}/build-${BUILD_TARGET%%-*}}" +else + BUILD_DIR="${BUILD_DIR:-${MYDIR}/build}" +fi +# Whether we should post a message in the MR when the build fails. +POST_MESSAGE_ON_ERROR="${POST_MESSAGE_ON_ERROR:-1}" + +# Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS +CMAKE_FLAGS=${CMAKE_FLAGS:-} +CMAKE_C_FLAGS=${CMAKE_C_FLAGS:-${CMAKE_FLAGS}} +CMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS:-${CMAKE_FLAGS}} +CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} +CMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS:-} +CMAKE_SHARED_LINKER_FLAGS=${CMAKE_SHARED_LINKER_FLAGS:-} +CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE:-} + +# Version inferred from the CI variables. +CI_COMMIT_SHA=${CI_COMMIT_SHA:-} +JPEGXL_VERSION=${JPEGXL_VERSION:-${CI_COMMIT_SHA:0:8}} + +# Benchmark parameters +STORE_IMAGES=${STORE_IMAGES:-1} +BENCHMARK_CORPORA="${MYDIR}/third_party/corpora" + +# Local flags passed to sanitizers. +UBSAN_FLAGS=( + -fsanitize=alignment + -fsanitize=bool + -fsanitize=bounds + -fsanitize=builtin + -fsanitize=enum + -fsanitize=float-cast-overflow + -fsanitize=float-divide-by-zero + -fsanitize=integer-divide-by-zero + -fsanitize=null + -fsanitize=object-size + -fsanitize=pointer-overflow + -fsanitize=return + -fsanitize=returns-nonnull-attribute + -fsanitize=shift-base + -fsanitize=shift-exponent + -fsanitize=unreachable + -fsanitize=vla-bound + + -fno-sanitize-recover=undefined + # Brunsli uses unaligned accesses to uint32_t, so alignment is just a warning. + -fsanitize-recover=alignment +) +# -fsanitize=function doesn't work on aarch64 and arm. +if [[ "${BUILD_TARGET%%-*}" != "aarch64" && + "${BUILD_TARGET%%-*}" != "arm" ]]; then + UBSAN_FLAGS+=( + -fsanitize=function + ) +fi +if [[ "${BUILD_TARGET%%-*}" != "arm" ]]; then + UBSAN_FLAGS+=( + -fsanitize=signed-integer-overflow + ) +fi + +CLANG_TIDY_BIN=$(which clang-tidy-6.0 clang-tidy-7 clang-tidy-8 | head -n 1) +# Default to "cat" if "colordiff" is not installed or if stdout is not a tty. +if [[ -t 1 ]]; then + COLORDIFF_BIN=$(which colordiff cat | head -n 1) +else + COLORDIFF_BIN="cat" +fi + +CLANG_VERSION="${CLANG_VERSION:-}" +# Detect the clang version suffix and store it in CLANG_VERSION. For example, +# "6.0" for clang 6 or "7" for clang 7. +detect_clang_version() { + if [[ -n "${CLANG_VERSION}" ]]; then + return 0 + fi + local clang_version=$("${CC:-clang}" --version | head -n1) + local llvm_tag + case "${clang_version}" in + "clang version 6."*) + CLANG_VERSION="6.0" + ;; + "clang version 7."*) + CLANG_VERSION="7" + ;; + "clang version 8."*) + CLANG_VERSION="8" + ;; + "clang version 9."*) + CLANG_VERSION="9" + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac +} + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} + +# Executed on exit. +on_exit() { + local retcode="$1" + # Always cleanup the CLEANUP_FILES. + cleanup + + # Post a message in the MR when requested with POST_MESSAGE_ON_ERROR but only + # if the run failed and we are not running from a MR pipeline. + if [[ ${retcode} -ne 0 && -n "${CI_BUILD_NAME:-}" && + -n "${POST_MESSAGE_ON_ERROR}" && -z "${CI_MERGE_REQUEST_ID:-}" && + "${CI_BUILD_REF_NAME}" = "master" ]]; then + load_mr_vars_from_commit + { set +xeu; } 2>/dev/null + local message="**Run ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} failed.** + +Check the output of the job at ${CI_JOB_URL:-} to see if this was your problem. +If it was, please rollback this change or fix the problem ASAP, broken builds +slow down development. Check if the error already existed in the previous build +as well. + +Pipeline: ${CI_PIPELINE_URL} + +Previous build commit: ${CI_COMMIT_BEFORE_SHA} +" + cmd_post_mr_comment "${message}" + fi +} + +trap 'retcode=$?; { set +x; } 2>/dev/null; on_exit ${retcode}' INT TERM EXIT + + +# These variables are populated when calling merge_request_commits(). + +# The current hash at the top of the current branch or merge request branch (if +# running from a merge request pipeline). +MR_HEAD_SHA="" +# The common ancestor between the current commit and the tracked branch, such +# as master. This includes a list +MR_ANCESTOR_SHA="" + +# Populate MR_HEAD_SHA and MR_ANCESTOR_SHA. +merge_request_commits() { + { set +x; } 2>/dev/null + # CI_BUILD_REF is the reference currently being build in the CI workflow. + MR_HEAD_SHA=$(git -C "${MYDIR}" rev-parse -q "${CI_BUILD_REF:-HEAD}") + if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then + # We are in a local branch, not a merge request. + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true) + else + # Merge request pipeline in CI. In this case the upstream is called "origin" + # but it refers to the forked project that's the source of the merge + # request. We need to get the target of the merge request, for which we need + # to query that repository using our CI_JOB_TOKEN. + echo "machine gitlab.com login gitlab-ci-token password ${CI_JOB_TOKEN}" \ + >> "${HOME}/.netrc" + git -C "${MYDIR}" fetch "${CI_MERGE_REQUEST_PROJECT_URL}" \ + "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD) + fi + if [[ -z "${MR_ANCESTOR_SHA}" ]]; then + echo "Warning, not tracking any branch, using the last commit in HEAD.">&2 + # This prints the return value with just HEAD. + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^") + else + MR_ANCESTOR_SHA=$(git -C "${MYDIR}" merge-base --all \ + "${MR_ANCESTOR_SHA}" "${MR_HEAD_SHA}") + fi + set -x +} + +# Load the MR iid from the landed commit message when running not from a +# merge request workflow. This is useful to post back results at the merge +# request when running pipelines from master. +load_mr_vars_from_commit() { + { set +x; } 2>/dev/null + if [[ -z "${CI_MERGE_REQUEST_IID:-}" ]]; then + local mr_iid=$(git rev-list --format=%B --max-count=1 HEAD | + grep -F "${CI_PROJECT_URL}/merge_requests" | head -n 1) + # mr_iid contains a string like this if it matched: + # Part-of: + if [[ -n "${mr_iid}" ]]; then + mr_iid=$(echo "${mr_iid}" | + sed -E 's,^.*merge_requests/([0-9]+)>.*$,\1,') + CI_MERGE_REQUEST_IID="${mr_iid}" + CI_MERGE_REQUEST_PROJECT_ID=${CI_PROJECT_ID} + fi + fi + set -x +} + +# Posts a comment to the current merge request. +cmd_post_mr_comment() { + { set +x; } 2>/dev/null + local comment="$1" + if [[ -n "${BOT_TOKEN:-}" && -n "${CI_MERGE_REQUEST_IID:-}" ]]; then + local url="${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" + curl -X POST -g \ + -H "PRIVATE-TOKEN: ${BOT_TOKEN}" \ + --data-urlencode "body=${comment}" \ + --output /dev/null \ + "${url}" + fi + set -x +} + +cmake_configure() { + local args=( + -B"${BUILD_DIR}" -H"${MYDIR}" + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" + -G Ninja + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" + -DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}" + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" + -DCMAKE_MODULE_LINKER_FLAGS="${CMAKE_MODULE_LINKER_FLAGS}" + -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" + -DCMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" + -DJPEGXL_VERSION="${JPEGXL_VERSION}" + ) + if [[ -n "${BUILD_TARGET}" ]]; then + # If set, BUILD_TARGET must be the target triplet such as + # x86_64-unknown-linux-gnu. + args+=( + -DCMAKE_C_COMPILER_TARGET="${BUILD_TARGET}" + -DCMAKE_CXX_COMPILER_TARGET="${BUILD_TARGET}" + # Only the first element of the target triplet. + -DCMAKE_SYSTEM_PROCESSOR="${BUILD_TARGET%%-*}" + -DCMAKE_SYSTEM_NAME=Linux + # These are needed to make googletest work when cross-compiling. + -DCMAKE_CROSSCOMPILING=1 + -DHAVE_STD_REGEX=0 + -DHAVE_POSIX_REGEX=0 + -DHAVE_GNU_POSIX_REGEX=0 + -DHAVE_STEADY_CLOCK=0 + -DHAVE_THREAD_SAFETY_ATTRIBUTES=0 + ) + # Use pkg-config for the target: + local pkg_config=$(which "${BUILD_TARGET}-pkg-config" || true) + if [[ -n "${pkg_config}" ]]; then + args+=(-DPKG_CONFIG_EXECUTABLE="${pkg_config}") + fi + fi + cmake "${args[@]}" "$@" +} + +cmake_build_and_test() { + # gtest_discover_tests() runs the test binaries to discover the list of tests + # at build time, which fails under qemu. + ASAN_OPTIONS=detect_leaks=0 cmake --build "${BUILD_DIR}" + # Pack test binaries if requested. + if [[ "${PACK_TEST:-}" == "1" ]]; then + (cd "${BUILD_DIR}" + find -name '*.cmake' -a '!' -path '*CMakeFiles*' + find -type d -name tests -a '!' -path '*CMakeFiles*' + ) | tar -C "${BUILD_DIR}" -cf "${BUILD_DIR}/tests.tar.xz" -T - \ + --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" + du -h "${BUILD_DIR}/tests.tar.xz" + # Pack coverage data if also available. + touch "${BUILD_DIR}/gcno.sentinel" + (cd "${BUILD_DIR}"; echo gcno.sentinel; find -name '*gcno') | \ + tar -C "${BUILD_DIR}" -cvf "${BUILD_DIR}/gcno.tar.xz" -T - \ + --use-compress-program="xz --threads=$(nproc --all || echo 1) -6" + fi + + if [[ "${SKIP_TEST}" -ne "1" ]]; then + (cd "${BUILD_DIR}" + export UBSAN_OPTIONS=print_stacktrace=1 + ctest -j $(nproc --all || echo 1) --output-on-failure) + fi +} + +# Configure the build to strip unused functions. This considerably reduces the +# output size, specially for tests which only use a small part of the whole +# library. +strip_dead_code() { + # Emscripten does tree shaking without any extra flags. + if [[ "${CMAKE_TOOLCHAIN_FILE##*/}" == "Emscripten.cmake" ]]; then + return 0 + fi + # -ffunction-sections, -fdata-sections and -Wl,--gc-sections effectively + # discard all unreachable code, reducing the code size. For this to work, we + # need to also pass --no-export-dynamic to prevent it from exporting all the + # internal symbols (like functions) making them all reachable and thus not a + # candidate for removal. + CMAKE_CXX_FLAGS+=" -ffunction-sections -fdata-sections" + CMAKE_C_FLAGS+=" -ffunction-sections -fdata-sections" + if [[ "${OS}" == "Darwin" ]]; then + CMAKE_EXE_LINKER_FLAGS+=" -dead_strip" + CMAKE_SHARED_LINKER_FLAGS+=" -dead_strip" + else + CMAKE_EXE_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" + CMAKE_SHARED_LINKER_FLAGS+=" -Wl,--gc-sections -Wl,--no-export-dynamic" + fi +} + +### Externally visible commands + +cmd_debug() { + CMAKE_BUILD_TYPE="Debug" + cmake_configure "$@" + cmake_build_and_test +} + +cmd_release() { + CMAKE_BUILD_TYPE="Release" + strip_dead_code + cmake_configure "$@" + cmake_build_and_test +} + +cmd_opt() { + CMAKE_BUILD_TYPE="RelWithDebInfo" + CMAKE_CXX_FLAGS+=" -DJXL_DEBUG_WARNING -DJXL_DEBUG_ON_ERROR" + cmake_configure "$@" + cmake_build_and_test +} + +cmd_coverage() { + cmd_release -DJPEGXL_ENABLE_COVERAGE=ON "$@" + + if [[ "${SKIP_TEST}" -ne "1" ]]; then + # If we didn't run the test we also don't print a coverage report. + cmd_coverage_report + fi +} + +cmd_coverage_report() { + detect_clang_version + LLVM_COV=$(which "llvm-cov-${CLANG_VERSION}") + local real_build_dir=$(realpath ${BUILD_DIR}) + local gcovr_args=( + -r "${real_build_dir}" + --gcov-executable "${LLVM_COV} gcov" + # Only print coverage information for the jxl and fuif directories. The rest + # is not part of the code under test. + --filter '.*jxl/.*' + --exclude '.*_test.cc' + --object-directory "${real_build_dir}" + ) + + ( + cd "${real_build_dir}" + gcovr "${gcovr_args[@]}" --html --html-details \ + --output="${real_build_dir}/coverage.html" + gcovr "${gcovr_args[@]}" --print-summary | + tee "${real_build_dir}/coverage.txt" + gcovr "${gcovr_args[@]}" --xml --output="${real_build_dir}/coverage.xml" + ) +} + +cmd_test() { + # Unpack tests if needed. + if [[ -e "${BUILD_DIR}/tests.tar.xz" && ! -d "${BUILD_DIR}/tests" ]]; then + tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/tests.tar.xz" + fi + if [[ -e "${BUILD_DIR}/gcno.tar.xz" && ! -d "${BUILD_DIR}/gcno.sentinel" ]]; then + tar -C "${BUILD_DIR}" -Jxvf "${BUILD_DIR}/gcno.tar.xz" + fi + (cd "${BUILD_DIR}" + export UBSAN_OPTIONS=print_stacktrace=1 + ctest -j $(nproc --all || echo 1) --output-on-failure) +} + +cmd_asan() { + detect_clang_version + LLVM_SYMBOLIZER=$(which llvm-symbolizer "llvm-symbolizer-${CLANG_VERSION}" | \ + head -n1) + LLVM_SYMBOLIZER="$(realpath "${LLVM_SYMBOLIZER}" || true)" + + CMAKE_C_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ + -fsanitize=address ${UBSAN_FLAGS[@]}" + CMAKE_CXX_FLAGS+=" -DJXL_ENABLE_ASSERT=1 -g -DADDRESS_SANITIZER \ + -fsanitize=address ${UBSAN_FLAGS[@]}" + strip_dead_code + cmake_configure "$@" + export ASAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + export UBSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + cmake_build_and_test +} + +cmd_tsan() { + local tsan_args=( + -DJXL_ENABLE_ASSERT=1 + -g + -DTHREAD_SANITIZER + ${UBSAN_FLAGS[@]} + -fsanitize=thread + ) + CMAKE_C_FLAGS+=" ${tsan_args[@]}" + CMAKE_CXX_FLAGS+=" ${tsan_args[@]}" + + CMAKE_BUILD_TYPE="RelWithDebInfo" + cmake_configure "$@" -DJPEGXL_ENABLE_TCMALLOC=OFF + cmake_build_and_test +} + +cmd_msan() { + detect_clang_version + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + if [[ ! -d "${msan_prefix}" || -e "${msan_prefix}/lib/libc++abi.a" ]]; then + # Install msan libraries for this version if needed or if an older version + # with libc++abi was installed. + cmd_msan_install + fi + + local msan_c_flags=( + -fsanitize=memory + -fno-omit-frame-pointer + -fsanitize-memory-track-origins + + -DJXL_ENABLE_ASSERT=1 + -g + -DMEMORY_SANITIZER + + # Force gtest to not use the cxxbai. + -DGTEST_HAS_CXXABI_H_=0 + ) + local msan_cxx_flags=( + "${msan_c_flags[@]}" + + # Some C++ sources don't use the std at all, so the -stdlib=libc++ is unused + # in those cases. Ignore the warning. + -Wno-unused-command-line-argument + -stdlib=libc++ + + # We include the libc++ from the msan directory instead, so we don't want + # the std includes. + -nostdinc++ + -cxx-isystem"${msan_prefix}/include/c++/v1" + ) + + local msan_linker_flags=( + -L"${msan_prefix}"/lib + -Wl,-rpath -Wl,"${msan_prefix}"/lib/ + ) + + LLVM_SYMBOLIZER=$(which llvm-symbolizer "llvm-symbolizer-${CLANG_VERSION}" | \ + head -n1) + LLVM_SYMBOLIZER="$(realpath "${LLVM_SYMBOLIZER}" || true)" + + CMAKE_C_FLAGS+=" ${msan_c_flags[@]} ${UBSAN_FLAGS[@]}" + CMAKE_CXX_FLAGS+=" ${msan_cxx_flags[@]} ${UBSAN_FLAGS[@]}" + CMAKE_EXE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + CMAKE_MODULE_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + CMAKE_SHARED_LINKER_FLAGS+=" ${msan_linker_flags[@]}" + strip_dead_code + cmake_configure "$@" \ + -DCMAKE_CROSSCOMPILING=1 -DRUN_HAVE_STD_REGEX=0 -DRUN_HAVE_POSIX_REGEX=0 + export MSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + export UBSAN_SYMBOLIZER_PATH="${LLVM_SYMBOLIZER}" + cmake_build_and_test +} + +# Install libc++ libraries compiled with msan in the msan_prefix for the current +# compiler version. +cmd_msan_install() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + # Detect the llvm to install: + export CC="${CC:-clang}" + export CXX="${CXX:-clang++}" + detect_clang_version + local llvm_tag + case "${CLANG_VERSION}" in + "6.0") + llvm_tag="llvmorg-6.0.1" + ;; + "7") + llvm_tag="llvmorg-7.0.1" + ;; + "8") + llvm_tag="llvmorg-8.0.0" + ;; + "9") + llvm_tag="llvmorg-9.0.0" + ;; + *) + echo "Unknown clang version: ${CLANG_VERSION}" >&2 + return 1 + esac + local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" + curl -L --show-error -o "${llvm_targz}" \ + "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" + tar -C "${tmpdir}" -zxf "${llvm_targz}" + local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + rm -rf "${msan_prefix}" + + declare -A CMAKE_EXTRAS + CMAKE_EXTRAS[libcxx]="\ + -DLIBCXX_CXX_ABI=libstdc++ \ + -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" + + for project in libcxx; do + local proj_build="${tmpdir}/build-${project}" + local proj_dir="${llvm_root}/${project}" + mkdir -p "${proj_build}" + cmake -B"${proj_build}" -H"${proj_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_USE_SANITIZER=Memory \ + -DLLVM_PATH="${llvm_root}/llvm" \ + -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ + head -n1)" \ + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ + -DCMAKE_SHARED_LINKER_FLAGS="${CMAKE_SHARED_LINKER_FLAGS}" \ + -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ + ${CMAKE_EXTRAS[${project}]} + cmake --build "${proj_build}" + ninja -C "${proj_build}" install + done +} + +cmd_fast_benchmark() { + local small_corpus_tar="${BENCHMARK_CORPORA}/jyrki-full.tar" + mkdir -p "${BENCHMARK_CORPORA}" + curl --show-error -o "${small_corpus_tar}" -z "${small_corpus_tar}" \ + "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/jyrki-full.tar" + + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + tar -xf "${small_corpus_tar}" -C "${tmpdir}" + + run_benchmark "${tmpdir}" 1048576 +} + +cmd_benchmark() { + local nikon_corpus_tar="${BENCHMARK_CORPORA}/nikon-subset.tar" + mkdir -p "${BENCHMARK_CORPORA}" + curl --show-error -o "${nikon_corpus_tar}" -z "${nikon_corpus_tar}" \ + "https://storage.googleapis.com/artifacts.jpegxl.appspot.com/corpora/nikon-subset.tar" + + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + tar -xvf "${nikon_corpus_tar}" -C "${tmpdir}" + + local sem_id="jpegxl_benchmark-$$" + local nprocs=$(nproc --all || echo 1) + images=() + local filename + while IFS= read -r filename; do + # This removes the './' + filename="${filename:2}" + local mode + if [[ "${filename:0:4}" == "srgb" ]]; then + mode="RGB_D65_SRG_Rel_SRG" + elif [[ "${filename:0:5}" == "adobe" ]]; then + mode="RGB_D65_Ado_Rel_Ado" + else + echo "Unknown image colorspace: ${filename}" >&2 + exit 1 + fi + png_filename="${filename%.ppm}.png" + png_filename=$(echo "${png_filename}" | tr '/' '_') + sem --bg --id "${sem_id}" -j"${nprocs}" -- \ + "${BUILD_DIR}/tools/decode_and_encode" \ + "${tmpdir}/${filename}" "${mode}" "${tmpdir}/${png_filename}" + images+=( "${png_filename}" ) + done < <(cd "${tmpdir}"; find . -name '*.ppm' -type f) + sem --id "${sem_id}" --wait + + # We need about 10 GiB per thread on these images. + run_benchmark "${tmpdir}" 10485760 +} + +get_mem_available() { + if [[ "${OS}" == "Darwin" ]]; then + echo $(vm_stat | grep -F 'Pages free:' | awk '{print $3 * 4}') + else + echo $(grep -F MemAvailable: /proc/meminfo | awk '{print $2}') + fi +} + +run_benchmark() { + local src_img_dir="$1" + local mem_per_thread="${2:-10485760}" + + local output_dir="${BUILD_DIR}/benchmark_results" + mkdir -p "${output_dir}" + + # The memory available at the beginning of the benchmark run in kB. The number + # of threads depends on the available memory, and the passed memory per + # thread. We also add a 2 GiB of constant memory. + local mem_available="$(get_mem_available)" + # Check that we actually have a MemAvailable value. + [[ -n "${mem_available}" ]] + local num_threads=$(( (${mem_available} - 1048576) / ${mem_per_thread} )) + if [[ ${num_threads} -le 0 ]]; then + num_threads=1 + fi + + local benchmark_args=( + --input "${src_img_dir}/*.png" + --codec=jpeg:yuv420:q85,webp:q80,jxl:fast:d1,jxl:fast:d1:downsampling=8,jxl:fast:d4,jxl:fast:d4:downsampling=8,jxl:mg:nl,jxl:mg,jxl:mg:P7,jxl:mg:q80 + --output_dir "${output_dir}" + --noprofiler --show_progress + --num_threads="${num_threads}" + ) + if [[ "${STORE_IMAGES}" == "1" ]]; then + benchmark_args+=(--save_decompressed --save_compressed) + fi + "${BUILD_DIR}/tools/benchmark_xl" "${benchmark_args[@]}" | \ + tee "${output_dir}/results.txt" + # Check error code for benckmark_xl command. This will exit if not. + [[ "${PIPESTATUS[0]}" == 0 ]] + + if [[ -n "${CI_BUILD_NAME:-}" ]]; then + { set +x; } 2>/dev/null + local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): + +$(cat "${output_dir}/results.txt") +" + cmd_post_mr_comment "${message}" + set -x + fi +} + +# Helper function to wait for the CPU temperature to cool down on ARM. +wait_for_temp() { + { set +x; } 2>/dev/null + local temp_limit=${1:-37000} + if [[ -z "${THERMAL_FILE:-}" ]]; then + echo "Must define the THERMAL_FILE with the thermal_zoneX/temp file" \ + "to read the temperature from. This is normally set in the runner." >&2 + exit 1 + fi + local org_temp=$(cat "${THERMAL_FILE}") + if [[ "${org_temp}" -ge "${temp_limit}" ]]; then + echo -n "Waiting for temp to get down from ${org_temp}... " + fi + local temp="${org_temp}" + while [[ "${temp}" -ge "${temp_limit}" ]]; do + sleep 1 + temp=$(cat "${THERMAL_FILE}") + done + if [[ "${org_temp}" -ge "${temp_limit}" ]]; then + echo "Done, temp=${temp}" + fi + set -x +} + +# Helper function to set the cpuset restriction of the current process. +cmd_cpuset() { + local newset="$1" + local mycpuset=$(cat /proc/self/cpuset) + mycpuset="/dev/cpuset${mycpuset}" + # Check that the directory exists: + [[ -d "${mycpuset}" ]] + echo "${newset}" >"${mycpuset}/cpuset.cpus" +} + +# Run benchmarks on ARM for the big and little CPUs. +cmd_arm_benchmark() { + local benchmarks=( + # Lossy options: + "--adaptive_reconstruction=1 --distance=1.0 --speed=fast" + "--adaptive_reconstruction=0 --distance=1.0 --speed=fast" + "--adaptive_reconstruction=1 --distance=8.0 --speed=fast" + "--adaptive_reconstruction=0 --distance=8.0 --speed=fast" + "--modular-group -Q 90" + "--modular-group -Q 50" + # Lossless options: + "--modular-group" + "--modular-group -E 0 -I 0 -A" + "--modular-group -P 5 -A" + "--modular-group --responsive=1" + # Near-lossless options: + "--adaptive_reconstruction=0 --distance=0.3 --speed=fast" + "--modular-group -N 3 -B 11" + "--modular-group -Q 97" + ) + + local images=( + "third_party/testdata/imagecompression.info/flower_foveon.png" + ) + + local cpu_confs=( + "${RUNNER_CPU_LITTLE}" + "${RUNNER_CPU_BIG}" + # The CPU description is something like 3-7, so these configurations only + # take the first CPU of the group. + "${RUNNER_CPU_LITTLE%%-*}" + "${RUNNER_CPU_BIG%%-*}" + ) + local output_dir="${BUILD_DIR}/benchmark_results" + mkdir -p "${output_dir}" + local runs_file="${output_dir}/runs.txt" + + if [[ ! -e "${runs_file}" ]]; then + echo -e "flags\tsrc_img\tsrc size\tsrc pixels\tcpuset\tenc size (B)\tdec speed (MP/s)" | + tee -a "${runs_file}" + fi + + local flags + local src_img + for src_img in "${images[@]}"; do + local src_img_hash=$(sha1sum "${src_img}" | cut -f 1 -d ' ') + for flags in "${benchmarks[@]}"; do + cmd_cpuset "${RUNNER_CPU_ALL}" + # Encoding step. + local enc_file_hash="$flags || ${src_img} || ${src_img_hash}" + enc_file_hash=$(echo "${enc_file_hash}" | sha1sum | cut -f 1 -d ' ') + + local enc_file="${BUILD_DIR}/${enc_file_hash}.bin" + if [[ ! -f "${enc_file}" ]]; then + "${BUILD_DIR}/tools/cjpegxl" ${flags} "${src_img}" "${enc_file}.tmp" + mv "${enc_file}.tmp" "${enc_file}" + fi + local enc_size=$(stat -c "%s" "${enc_file}") + + for cpu_conf in "${cpu_confs[@]}"; do + cmd_cpuset "${cpu_conf}" + echo "Decoding with: img=${src_img} cpus=${cpu_conf} enc_flags=${flags}" + + local dec_output + wait_for_temp + dec_output=$("${BUILD_DIR}/tools/djpegxl" "${enc_file}" \ + --num_reps=5 2>&1 | grep -F "MP/s") + local img_size=$(echo "${dec_output}" | cut -f 1 -d ',') + local img_size_x=$(echo "${img_size}" | cut -f 1 -d ' ') + local img_size_y=$(echo "${img_size}" | cut -f 3 -d ' ') + local img_size_px=$(( ${img_size_x} * ${img_size_y} )) + local dec_speed=$(echo "${dec_output}" | cut -f 5 -d ' ') + + # Record entry in a tab-separated file. + echo -e "${flags}\t${src_img}\t${img_size}\t${img_size_px}\t${cpu_conf}\t${enc_size}\t${dec_speed}" | + tee -a "${runs_file}" + done + done + done + cmd_cpuset "${RUNNER_CPU_ALL}" + cat "${runs_file}" + + if [[ -n "${CI_BUILD_NAME:-}" ]]; then + load_mr_vars_from_commit + { set +x; } 2>/dev/null + local message="Results for ${CI_BUILD_NAME} @ ${CI_COMMIT_SHORT_SHA} (job ${CI_JOB_URL:-}): + +\`\`\` +$(column -t -s " " "${runs_file}") +\`\`\` +" + cmd_post_mr_comment "${message}" + set -x + fi +} + +# Runs the linter (clang-format) on the pending CLs. +cmd_lint() { + merge_request_commits + # { set +x; } 2>/dev/null + local versions=(${1:-6.0 7 8 9}) + local clang_format_bins=("${versions[@]/#/clang-format-}" clang-format) + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + + local installed=() + local clang_patch + local clang_format + for clang_format in "${clang_format_bins[@]}"; do + if ! which "${clang_format}" >/dev/null; then + continue + fi + installed+=("${clang_format}") + local tmppatch="${tmpdir}/${clang_format}.patch" + # We include in this linter all the changes including the uncommited changes + # to avoid printing changes already applied. + set -x + git -C "${MYDIR}" "${clang_format}" --binary "${clang_format}" \ + --style=file --diff "${MR_ANCESTOR_SHA}" -- >"${tmppatch}" + { set +x; } 2>/dev/null + + if grep -E '^--- ' "${tmppatch}">/dev/null; then + if [[ -n "${LINT_OUTPUT:-}" ]]; then + cp "${tmppatch}" "${LINT_OUTPUT}" + fi + clang_patch="${tmppatch}" + else + echo "clang-format check OK" >&2 + return 0 + fi + done + + if [[ ${#installed[@]} -eq 0 ]]; then + echo "You must install clang-format for \"git clang-format\"" >&2 + exit 1 + fi + + # clang-format is installed but found problems. + echo "clang-format findings:" >&2 + "${COLORDIFF_BIN}" < "${clang_patch}" + + echo "clang-format found issues in your patches from ${MR_ANCESTOR_SHA}" \ + "to the current patch. Run \`./ci.sh lint | patch -p1\` from the base" \ + "directory to apply them." >&2 + exit 1 +} + +# Runs clang-tidy on the pending CLs. If the "all" argument is passed it runs +# clang-tidy over all the source files instead. +cmd_tidy() { + local what="${1:-}" + + if [[ -z "${CLANG_TIDY_BIN}" ]]; then + echo "ERROR: You must install clang-tidy-6.0 or newer to use ci.sh tidy" >&2 + exit 1 + fi + + local git_args=() + if [[ "${what}" == "all" ]]; then + git_args=(ls-files) + shift + else + merge_request_commits + git_args=( + diff-tree --no-commit-id --name-only -r "${MR_ANCESTOR_SHA}" + "${MR_HEAD_SHA}" + ) + fi + + # Clang-tidy needs the compilation database generated by cmake. + if [[ ! -e "${BUILD_DIR}/compile_commands.json" ]]; then + # Generate the build options in debug mode, since we need the debug asserts + # enabled for the clang-tidy analyzer to use them. + CMAKE_BUILD_TYPE="Debug" + cmake_configure + fi + + cd "${MYDIR}" + local nprocs=$(nproc --all || echo 1) + local ret=0 + if ! parallel -j"${nprocs}" --keep-order -- \ + "${CLANG_TIDY_BIN}" -p "${BUILD_DIR}" -format-style=file -quiet "$@" {} \ + < <(git "${git_args[@]}" | grep -E '(\.cc|\.cpp)$') \ + >"${BUILD_DIR}/clang-tidy.txt"; then + ret=1 + fi + { set +x; } 2>/dev/null + echo "Findings statistics:" >&2 + grep -E ' \[[A-Za-z\.,\-]+\]' -o "${BUILD_DIR}/clang-tidy.txt" | sort \ + | uniq -c >&2 + + if [[ $ret -ne 0 ]]; then + cat >&2 <&2 <> ~/.ssh/config < mybranch +``` + +### Updating submodules + +The repository uses submodules for external library dependencies in +third_party. Each submodule points to a particular external commit of the +external repository by the hash code of that external commit. Just like +regular source code files, this hash code is part of the current branch and +jpeg xl commit you have checked out. + +When changing branches or when doing `git rebase`, git will unfortunately +*not* automatically set those hashes to the ones of the branch or jpeg xl +commit you changed to nor set the source files of the third_party submodules +to the new state. That is, even though git will have updated the jpeg xl +source code files on your disk to the new ones, it will leave the submodule +hashes and the files in third_party in your workspace to the ones they were +before you changed branches. This will show up in a git diff because this +is seen as a change compared to the branch you switched to. The git diff shows +the difference in hash codes (as if you are changing to the old ones), it does +not show changes in files inside the third_party directory. + +This mismatch can cause at least two problems: + +*) the jpeg xl codebase may not compile due to third_party library version +mismatch if e.g. API changed or a submodule was added/removed. + +*) when using `commit -a` your commit, which may be a technical change +unrelated to submodule changes, will unintentially contain a change to the +submodules hash code, which is undesired unless you actually want to change +the version of third_party libraries. + +To resolve this, the submodules must be updated manually with +the following command after those actions (at least when the submodules +changed): + +``` +git submodule update --init --recursive +``` + +Here, the init flag ensures new modules get added when encessary and the +recursive flag is required for the submodules depending on other submodules. + +If you checkout a different branch, you can spot that submodules changed +when it shows a message similar to this: + +``` +M third_party/brotli +M third_party/lcms +``` + +If you do a rebase you may end up in a harder to solve situation, where +`git submodule update --init --recursive` itself fails with errors such as: + +``` +Unable to checkout '35ef5c554d888bef217d449346067de05e269b30' in submodule path 'third_party/brunsli/third_party/brotli' +Unable to checkout '35ef5c554d888bef217d449346067de05e269b30' in submodule path 'third_party/brotli' +``` + +In that case, you can use the force flag: + +``` +git submodule update --init --recursive --force +``` + +### Iterating changes in your merge request + +To address reviewer changes you need to amend the local changes in your branch +first. Make the changes you need in your commit locally by running `git commit +--amend file1 file2 file3 ...` or `git commit --amend -a` to amend all the +changes from all the staged files. + +Once you have the new version of the "mybranch" branch to re-upload, you need to +force push it to the same branch in your fork. Since you are pushing a different +version of the same commit (as opposed to another commit on top of the existing +ones), you need to force the operation to replace the old version. + +```bash +git push origin mybranch --force +``` + +The merge request should now be updated with the new changes. + +### Merging your changes + +We use "rebase" as a merge policy, which means that there a no "merge" commits +(commits with more than one parent) but instead only a linear history of +changes. If other commits landed in the master branch since you last synced you +need to `git fetch`, `git rebase` and push again your changes which need to go +through the pipeline again to verify that all the tests pass again after +including the latests changes. Instead of doing this manually, you can _Assign_ +the patch to the user @jpegxl-bot which will do this for you. In order for the +bot to manage your Merge Request, you need to add it to your fork as a +Developer. + +### Trying locally a pending merge request. + +If you want to review in your computer a pending merge request proposed by +another user you can fetch the merge request commit with the following command, +replacing `NNNN` with the merge request number: + +```bash +git fetch origin refs/merge-requests/NNNN/head +git checkout FETCH_HEAD +``` + +The first command will add to your local git repository the remote commit for +the pending merge request and store a temporary reference called `FETCH_HEAD`. +The second command then checks out that reference. From this point you can +review the files in your computer, create a local branch for this FETCH_HEAD or +build on top of it. diff --git a/doc/guidelines.md b/doc/guidelines.md new file mode 100644 index 000000000..d44594217 --- /dev/null +++ b/doc/guidelines.md @@ -0,0 +1,67 @@ +# Guidelines for JPEG-XL software + +The following rules govern experimental software contributed for JPEG XL. Merge +requests not complying with these rules will not be accepted by the software +coordinators. + +## License + +The contribution shall be licensed under the Apache 2 license, with each file +beginning with the following header: + +``` +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +``` + +## Language + +The software shall be compilable as C++ without custom preprocessors, and be +sent in the preferred representation for reading/development (without +obfuscation). + +In general, please follow +[guidelines on features and style](https://google.github.io/styleguide/cppguide.html) +for newly added code. + +In particular: + +* The code shall compile under clang++ (version 6) without any warnings. +* Exceptions and RTTI shall not be used. +* Features from C++11 may be used if desired, but not C++14 or later. +* Integer types shall be fixed-size types from stdint.h (e.g. uint32_t), or size_t, or [u]intptr_t. +* NULL shall not be used (use nullptr instead). +* The code shall be formatted by running clang-format. +* Macro names shall be UPPERCASE_WITH_UNDERSCORES. +* External dependencies should be minimized; large dependencies such as Qt or Boost are not allowed. +* Headers shall be surrounded by include guards of the form + + ``` + #ifndef FILENAME_H_ + #define FILENAME_H_ + #endif // FILENAME_H_ + + ``` + +## Acceptance test + +Before merging, the pipeline shall pass. + +Note that tests can also be run locally from the cmake build/ directory: +`ctest -j50 --output-on-failure` (or some other number of parallel tasks) + +The code shall not involve race conditions nor accesses to undefined memory. +In particular, before merging results of a core experiment, tests shall be run +in three configurations: compiling with -fsanitize=address, -fsanitize=memory +and -fsanitize=thread. diff --git a/doc/tables/adobe.md b/doc/tables/adobe.md new file mode 100644 index 000000000..f3beef75a --- /dev/null +++ b/doc/tables/adobe.md @@ -0,0 +1,6 @@ +#### Table M.8 – "Adobe" marker template + +``` +0xEE, 0x00, 0x0E, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x01 +``` + diff --git a/doc/tables/all_tables.pdf b/doc/tables/all_tables.pdf new file mode 100644 index 000000000..be1a9af14 Binary files /dev/null and b/doc/tables/all_tables.pdf differ diff --git a/doc/tables/all_tables.sh b/doc/tables/all_tables.sh new file mode 100755 index 000000000..cfd289a16 --- /dev/null +++ b/doc/tables/all_tables.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cat dct_gen.md \ + is_zero_base.md num_nonzeros_base.md brn_proto.md app0.md icc.md ducky.md \ + adobe.md stock_counts.md stock_values.md symbol_order.md stock_quant.md \ + quant.md freq_context.md num_nonzero_context.md nonzero_buckets.md \ + > all_tables.md diff --git a/doc/tables/app0.md b/doc/tables/app0.md new file mode 100644 index 000000000..266f21047 --- /dev/null +++ b/doc/tables/app0.md @@ -0,0 +1,6 @@ +#### Table M.4 – APP0 template + +``` +0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00 +``` + diff --git a/doc/tables/brn_proto.md b/doc/tables/brn_proto.md new file mode 100644 index 000000000..b5f80b6a7 --- /dev/null +++ b/doc/tables/brn_proto.md @@ -0,0 +1,23 @@ +#### Table M.3 – Protocol Buffer descriptor of top-level structure of losslessly compressed JPEG stream + +```protobuf +message Header { + optional uint64 width = 1; + optional uint64 height = 2; + required uint64 version_and_component_count_code = 3; + optional uint64 subsampling_code = 4; +} + +message Jpeg { + required bytes signature = 1; + required Header header = 2; + optional bytes meta_data = 3; + optional bytes jpeg1_internals = 4; + optional bytes quant_data = 5; + optional bytes histogram_data = 6; + optional bytes dc_data = 7; + optional bytes ac_data = 8; + optional bytes original_jpg = 9; +} +``` + diff --git a/doc/tables/dct_gen.md b/doc/tables/dct_gen.md new file mode 100644 index 000000000..f3b59e248 --- /dev/null +++ b/doc/tables/dct_gen.md @@ -0,0 +1,241 @@ +#### Electronic Insert I.1 – DCT-II / DCT-III code generator + +```python +####################################################################### +# DCT-II / DCT-III generator +# +# Based on: +# "A low multiplicative complexity fast recursive DCT-2 algorithm" +# by Maxim Vashkevich and Alexander Petrovsky / arXiv / 20 Jul 2012 +####################################################################### + +import math +import sys +N = 8 + +####################################################################### +# Base transforms / generators +####################################################################### + +CNTR = 0 +def makeTmp(): + global CNTR + result = "t{:02d}".format(CNTR) + CNTR = CNTR + 1 + return result + +def makeVar(i): + return "i{:02d}".format(i) + +def add(x, y): + tmp = makeTmp() + print(tmp + " = " + x + " + " + y + ";") + return tmp + +def sub(x, y): + tmp = makeTmp() + print(tmp + " = " + x + " - " + y + ";") + return tmp + +def mul(x, c): + tmp = makeTmp() + print(tmp + " = " + x + " * " + c + ";") + return tmp + +# 2.0 * math.cos((a + 0.0) / (b + 0.0) * math.pi) +def C2(a, b): + return "c_c2_" + str(a) + "_" + str(b) + +# 1.0 / C2(a, b) +def iC2(a, b): + return "c_ic2_" + str(a) + "_" + str(b) + +####################################################################### +# Utilities +####################################################################### + +# Generate identity matrix. Usually this matrix is passed to +# DCT algorithm to generate "basis" vectors of the transform. +def makeVars(): + return [makeVar(i) for i in range(N)] + +# Split list of variables info halves. +def split(x): + m = len(x) + m2 = m // 2 + return (x[0 : m2], x[m2 : m]) + +# Make a list of variables in a reverse order. +def reverse(varz): + m = len(varz) + result = [0] * m + for i in range(m): + result[i] = varz[m - 1 - i] + return result + +# Apply permutation +def permute(x, p): + return [x[p[i]] for i in range(len(p))] + +def transposePermutation(p): + n = len(p) + result = [0] * n + for i in range(n): + result[p[i]] = i + return result + +# See paper. Split even-odd elements. +def P(n): + if n == 1: + return [0] + n2 = n // 2 + return [2 * i for i in range(n2)] + [2 * i + 1 for i in range(n2)] + +# See paper. Interleave first and second half. +def Pt(n): + return transposePermutation(P(n)) + +####################################################################### +# Scheme +####################################################################### + +def B2(x): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + bottom = reverse(bottom) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + return t + b + +def iB2(x): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + return t + reverse(b) + +def B4(x, rn): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + rbottom = reverse(bottom) + t = [sub(top[i], rbottom[i]) for i in range(n2)] + b = [mul(bottom[i], C2(rn, 2 * N)) for i in range(n2)] + top = [add(t[i], b[i]) for i in range(n2)] + bottom = [sub(t[i], b[i]) for i in range(n2)] + return top + bottom + +def iB4(x, rn): + n = len(x) + n2 = n // 2 + if n == 1: + raise "ooops" + (top, bottom) = split(x) + t = [add(top[i], bottom[i]) for i in range(n2)] + b = [sub(top[i], bottom[i]) for i in range(n2)] + bottom = [mul(b[i], iC2(rn, 2 * N)) for i in range(n2)] + rbottom = reverse(bottom) + top = [add(t[i], rbottom[i]) for i in range(n2)] + return top + bottom + +def P4(n): + if n == 1: + return [0] + if n == 2: + return [0, 1] + n2 = n // 2 + result = [0] * n + tc = 0 + bc = 0 + i = 0 + result[i] = tc; tc = tc + 1; i = i + 1 + turn = True + while i < n - 1: + if turn: + result[i] = n2 + bc; bc = bc + 1; i = i + 1 + result[i] = n2 + bc; bc = bc + 1; i = i + 1 + else: + result[i] = tc; tc = tc + 1; i = i + 1 + result[i] = tc; tc = tc + 1; i = i + 1 + turn = not turn + result[i] = tc; tc = tc + 1; i = i + 1 + return result + +def iP4(n): + return transposePermutation(P4(n)) + +def d2n(x): + n = len(x) + if n == 1: + return x + y = B2(x) + (top, bottom) = split(y) + return permute(d2n(top) + d4n(bottom, N // 2), Pt(n)) + +def id2n(x): + n = len(x) + if n == 1: + return x + (top, bottom) = split(permute(x, P(n))) + return iB2(id2n(top) + id4n(bottom, N // 2)) + +def d4n(x, rn): + n = len(x) + if n == 1: + return x + y = B4(x, rn) + (top, bottom) = split(y) + rn2 = rn // 2 + return permute(d4n(top, rn2) + d4n(bottom, N - rn2), P4(n)) + +def id4n(x, rn): + n = len(x) + if n == 1: + return x + (top, bottom) = split(permute(x, iP4(n))) + rn2 = rn // 2 + y = id4n(top, rn2) + id4n(bottom, N -rn2) + return iB4(y, rn) + +####################################################################### +# Main. +####################################################################### + +def help(): + print("Usage: %s [N [T]]" % sys.argv[0]) + print(" N should be the power of 2, default is 8") + print(" T is one of {2, 3}, default is 2") + sys.exit() + +def parseInt(s): + try: + return int(s) + except ValueError: + help() + +if __name__ == "__main__": + if len(sys.argv) < 1 or len(sys.argv) > 3: help() + if len(sys.argv) >= 2: + N = parseInt(sys.argv[1]) + if (N & (N - 1)) != 0: help() + type = 0 + if len(sys.argv) >= 3: + typeOption = sys.argv[2] + if len(typeOption) != 1: help() + type = "23".index(typeOption) + if type == -1: help() + if type == 0: + vars = d2n(makeVars()) + else: # type == 1 + vars = id2n(makeVars()) + print("Output vector: " + str(vars)) +``` + diff --git a/doc/tables/ducky.md b/doc/tables/ducky.md new file mode 100644 index 000000000..307f68804 --- /dev/null +++ b/doc/tables/ducky.md @@ -0,0 +1,6 @@ +#### Table M.7 – "Ducky" marker template + +``` +0xEC, 0x00, 0x11, 0x44, 0x75, 0x63, 0x6B, 0x79, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00 +``` + diff --git a/doc/tables/freq_context.md b/doc/tables/freq_context.md new file mode 100644 index 000000000..3e218fb1b --- /dev/null +++ b/doc/tables/freq_context.md @@ -0,0 +1,54 @@ +#### Table M.15 – freq_context + +`scheme == 0`: +``` +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +``` + +`scheme == 1`: +``` +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 +``` + +`scheme == 2`: +``` +0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, +2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, +3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1 +``` + +`scheme == 3`: +``` +0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, +6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, +7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 2, 2, 2 +``` + +`scheme == 4`: +``` + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 8, 8, 9, 9, + 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, +13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, +15, 15, 15, 15, 15, 15, 15, 15, 15, 15 +``` + +`scheme == 5`: +``` + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16, +17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 24, 24, +25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, +29, 29, 30, 30, 30, 30, 31, 31, 31, 31 +``` + +`scheme == 6`: +``` + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, +18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, +36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, +54, 55, 56, 57, 58, 59, 60, 61, 62, 63 +``` + diff --git a/doc/tables/icc.md b/doc/tables/icc.md new file mode 100644 index 000000000..1f3b4cde3 --- /dev/null +++ b/doc/tables/icc.md @@ -0,0 +1,6 @@ +#### Table M.6 – common ICC profile template + +``` +0xE2, 0x0C, 0x58, 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00, 0x01, 0x01, 0x00, 0x00, 0x0C, 0x48, 0x4C, 0x69, 0x6E, 0x6F, 0x02, 0x10, 0x00, 0x00, 0x6D, 0x6E, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5A, 0x20, 0x07, 0xCE, 0x00, 0x02, 0x00, 0x09, 0x00, 0x06, 0x00, 0x31, 0x00, 0x00, 0x61, 0x63, 0x73, 0x70, 0x4D, 0x53, 0x46, 0x54, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x43, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xF6, 0xD6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xD3, 0x2D, 0x48, 0x50, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x33, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, 0x84, 0x00, 0x00, 0x00, 0x6C, 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, 0xF0, 0x00, 0x00, 0x00, 0x14, 0x62, 0x6B, 0x70, 0x74, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x14, 0x72, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x18, 0x00, 0x00, 0x00, 0x14, 0x67, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x2C, 0x00, 0x00, 0x00, 0x14, 0x62, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x14, 0x64, 0x6D, 0x6E, 0x64, 0x00, 0x00, 0x02, 0x54, 0x00, 0x00, 0x00, 0x70, 0x64, 0x6D, 0x64, 0x64, 0x00, 0x00, 0x02, 0xC4, 0x00, 0x00, 0x00, 0x88, 0x76, 0x75, 0x65, 0x64, 0x00, 0x00, 0x03, 0x4C, 0x00, 0x00, 0x00, 0x86, 0x76, 0x69, 0x65, 0x77, 0x00, 0x00, 0x03, 0xD4, 0x00, 0x00, 0x00, 0x24, 0x6C, 0x75, 0x6D, 0x69, 0x00, 0x00, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x14, 0x6D, 0x65, 0x61, 0x73, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x00, 0x24, 0x74, 0x65, 0x63, 0x68, 0x00, 0x00, 0x04, 0x30, 0x00, 0x00, 0x00, 0x0C, 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x04, 0x3C, 0x00, 0x00, 0x08, 0x0C, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x28, 0x63, 0x29, 0x20, 0x31, 0x39, 0x39, 0x38, 0x20, 0x48, 0x65, 0x77, 0x6C, 0x65, 0x74, 0x74, 0x2D, 0x50, 0x61, 0x63, 0x6B, 0x61, 0x72, 0x64, 0x20, 0x43, 0x6F, 0x6D, 0x70, 0x61, 0x6E, 0x79, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x73, 0x52, 0x47, 0x42, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x73, 0x52, 0x47, 0x42, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x51, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x16, 0xCC, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0xA2, 0x00, 0x00, 0x38, 0xF5, 0x00, 0x00, 0x03, 0x90, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x99, 0x00, 0x00, 0xB7, 0x85, 0x00, 0x00, 0x18, 0xDA, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0xA0, 0x00, 0x00, 0x0F, 0x84, 0x00, 0x00, 0xB6, 0xCF, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x49, 0x45, 0x43, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x65, 0x63, 0x2E, 0x63, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x49, 0x45, 0x43, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x65, 0x63, 0x2E, 0x63, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x49, 0x45, 0x43, 0x20, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x20, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x20, 0x52, 0x47, 0x42, 0x20, 0x63, 0x6F, 0x6C, 0x6F, 0x75, 0x72, 0x20, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x2D, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x49, 0x45, 0x43, 0x20, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x20, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x20, 0x52, 0x47, 0x42, 0x20, 0x63, 0x6F, 0x6C, 0x6F, 0x75, 0x72, 0x20, 0x73, 0x70, 0x61, 0x63, 0x65, 0x20, 0x2D, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x63, 0x65, 0x20, 0x56, 0x69, 0x65, 0x77, 0x69, 0x6E, 0x67, 0x20, 0x43, 0x6F, 0x6E, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x63, 0x65, 0x20, 0x56, 0x69, 0x65, 0x77, 0x69, 0x6E, 0x67, 0x20, 0x43, 0x6F, 0x6E, 0x64, 0x69, 0x74, 0x69, 0x6F, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2E, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xA4, 0xFE, 0x00, 0x14, 0x5F, 0x2E, 0x00, 0x10, 0xCF, 0x14, 0x00, 0x03, 0xED, 0xCC, 0x00, 0x04, 0x13, 0x0B, 0x00, 0x03, 0x5C, 0x9E, 0x00, 0x00, 0x00, 0x01, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0x09, 0x56, 0x00, 0x50, 0x00, 0x00, 0x00, 0x57, 0x1F, 0xE7, 0x6D, 0x65, 0x61, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x8F, 0x00, 0x00, 0x00, 0x02, 0x73, 0x69, 0x67, 0x20, 0x00, 0x00, 0x00, 0x00, 0x43, 0x52, 0x54, 0x20, 0x63, 0x75, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0A, 0x00, 0x0F, 0x00, 0x14, 0x00, 0x19, 0x00, 0x1E, 0x00, 0x23, 0x00, 0x28, 0x00, 0x2D, 0x00, 0x32, 0x00, 0x37, 0x00, 0x3B, 0x00, 0x40, 0x00, 0x45, 0x00, 0x4A, 0x00, 0x4F, 0x00, 0x54, 0x00, 0x59, 0x00, 0x5E, 0x00, 0x63, 0x00, 0x68, 0x00, 0x6D, 0x00, 0x72, 0x00, 0x77, 0x00, 0x7C, 0x00, 0x81, 0x00, 0x86, 0x00, 0x8B, 0x00, 0x90, 0x00, 0x95, 0x00, 0x9A, 0x00, 0x9F, 0x00, 0xA4, 0x00, 0xA9, 0x00, 0xAE, 0x00, 0xB2, 0x00, 0xB7, 0x00, 0xBC, 0x00, 0xC1, 0x00, 0xC6, 0x00, 0xCB, 0x00, 0xD0, 0x00, 0xD5, 0x00, 0xDB, 0x00, 0xE0, 0x00, 0xE5, 0x00, 0xEB, 0x00, 0xF0, 0x00, 0xF6, 0x00, 0xFB, 0x01, 0x01, 0x01, 0x07, 0x01, 0x0D, 0x01, 0x13, 0x01, 0x19, 0x01, 0x1F, 0x01, 0x25, 0x01, 0x2B, 0x01, 0x32, 0x01, 0x38, 0x01, 0x3E, 0x01, 0x45, 0x01, 0x4C, 0x01, 0x52, 0x01, 0x59, 0x01, 0x60, 0x01, 0x67, 0x01, 0x6E, 0x01, 0x75, 0x01, 0x7C, 0x01, 0x83, 0x01, 0x8B, 0x01, 0x92, 0x01, 0x9A, 0x01, 0xA1, 0x01, 0xA9, 0x01, 0xB1, 0x01, 0xB9, 0x01, 0xC1, 0x01, 0xC9, 0x01, 0xD1, 0x01, 0xD9, 0x01, 0xE1, 0x01, 0xE9, 0x01, 0xF2, 0x01, 0xFA, 0x02, 0x03, 0x02, 0x0C, 0x02, 0x14, 0x02, 0x1D, 0x02, 0x26, 0x02, 0x2F, 0x02, 0x38, 0x02, 0x41, 0x02, 0x4B, 0x02, 0x54, 0x02, 0x5D, 0x02, 0x67, 0x02, 0x71, 0x02, 0x7A, 0x02, 0x84, 0x02, 0x8E, 0x02, 0x98, 0x02, 0xA2, 0x02, 0xAC, 0x02, 0xB6, 0x02, 0xC1, 0x02, 0xCB, 0x02, 0xD5, 0x02, 0xE0, 0x02, 0xEB, 0x02, 0xF5, 0x03, 0x00, 0x03, 0x0B, 0x03, 0x16, 0x03, 0x21, 0x03, 0x2D, 0x03, 0x38, 0x03, 0x43, 0x03, 0x4F, 0x03, 0x5A, 0x03, 0x66, 0x03, 0x72, 0x03, 0x7E, 0x03, 0x8A, 0x03, 0x96, 0x03, 0xA2, 0x03, 0xAE, 0x03, 0xBA, 0x03, 0xC7, 0x03, 0xD3, 0x03, 0xE0, 0x03, 0xEC, 0x03, 0xF9, 0x04, 0x06, 0x04, 0x13, 0x04, 0x20, 0x04, 0x2D, 0x04, 0x3B, 0x04, 0x48, 0x04, 0x55, 0x04, 0x63, 0x04, 0x71, 0x04, 0x7E, 0x04, 0x8C, 0x04, 0x9A, 0x04, 0xA8, 0x04, 0xB6, 0x04, 0xC4, 0x04, 0xD3, 0x04, 0xE1, 0x04, 0xF0, 0x04, 0xFE, 0x05, 0x0D, 0x05, 0x1C, 0x05, 0x2B, 0x05, 0x3A, 0x05, 0x49, 0x05, 0x58, 0x05, 0x67, 0x05, 0x77, 0x05, 0x86, 0x05, 0x96, 0x05, 0xA6, 0x05, 0xB5, 0x05, 0xC5, 0x05, 0xD5, 0x05, 0xE5, 0x05, 0xF6, 0x06, 0x06, 0x06, 0x16, 0x06, 0x27, 0x06, 0x37, 0x06, 0x48, 0x06, 0x59, 0x06, 0x6A, 0x06, 0x7B, 0x06, 0x8C, 0x06, 0x9D, 0x06, 0xAF, 0x06, 0xC0, 0x06, 0xD1, 0x06, 0xE3, 0x06, 0xF5, 0x07, 0x07, 0x07, 0x19, 0x07, 0x2B, 0x07, 0x3D, 0x07, 0x4F, 0x07, 0x61, 0x07, 0x74, 0x07, 0x86, 0x07, 0x99, 0x07, 0xAC, 0x07, 0xBF, 0x07, 0xD2, 0x07, 0xE5, 0x07, 0xF8, 0x08, 0x0B, 0x08, 0x1F, 0x08, 0x32, 0x08, 0x46, 0x08, 0x5A, 0x08, 0x6E, 0x08, 0x82, 0x08, 0x96, 0x08, 0xAA, 0x08, 0xBE, 0x08, 0xD2, 0x08, 0xE7, 0x08, 0xFB, 0x09, 0x10, 0x09, 0x25, 0x09, 0x3A, 0x09, 0x4F, 0x09, 0x64, 0x09, 0x79, 0x09, 0x8F, 0x09, 0xA4, 0x09, 0xBA, 0x09, 0xCF, 0x09, 0xE5, 0x09, 0xFB, 0x0A, 0x11, 0x0A, 0x27, 0x0A, 0x3D, 0x0A, 0x54, 0x0A, 0x6A, 0x0A, 0x81, 0x0A, 0x98, 0x0A, 0xAE, 0x0A, 0xC5, 0x0A, 0xDC, 0x0A, 0xF3, 0x0B, 0x0B, 0x0B, 0x22, 0x0B, 0x39, 0x0B, 0x51, 0x0B, 0x69, 0x0B, 0x80, 0x0B, 0x98, 0x0B, 0xB0, 0x0B, 0xC8, 0x0B, 0xE1, 0x0B, 0xF9, 0x0C, 0x12, 0x0C, 0x2A, 0x0C, 0x43, 0x0C, 0x5C, 0x0C, 0x75, 0x0C, 0x8E, 0x0C, 0xA7, 0x0C, 0xC0, 0x0C, 0xD9, 0x0C, 0xF3, 0x0D, 0x0D, 0x0D, 0x26, 0x0D, 0x40, 0x0D, 0x5A, 0x0D, 0x74, 0x0D, 0x8E, 0x0D, 0xA9, 0x0D, 0xC3, 0x0D, 0xDE, 0x0D, 0xF8, 0x0E, 0x13, 0x0E, 0x2E, 0x0E, 0x49, 0x0E, 0x64, 0x0E, 0x7F, 0x0E, 0x9B, 0x0E, 0xB6, 0x0E, 0xD2, 0x0E, 0xEE, 0x0F, 0x09, 0x0F, 0x25, 0x0F, 0x41, 0x0F, 0x5E, 0x0F, 0x7A, 0x0F, 0x96, 0x0F, 0xB3, 0x0F, 0xCF, 0x0F, 0xEC, 0x10, 0x09, 0x10, 0x26, 0x10, 0x43, 0x10, 0x61, 0x10, 0x7E, 0x10, 0x9B, 0x10, 0xB9, 0x10, 0xD7, 0x10, 0xF5, 0x11, 0x13, 0x11, 0x31, 0x11, 0x4F, 0x11, 0x6D, 0x11, 0x8C, 0x11, 0xAA, 0x11, 0xC9, 0x11, 0xE8, 0x12, 0x07, 0x12, 0x26, 0x12, 0x45, 0x12, 0x64, 0x12, 0x84, 0x12, 0xA3, 0x12, 0xC3, 0x12, 0xE3, 0x13, 0x03, 0x13, 0x23, 0x13, 0x43, 0x13, 0x63, 0x13, 0x83, 0x13, 0xA4, 0x13, 0xC5, 0x13, 0xE5, 0x14, 0x06, 0x14, 0x27, 0x14, 0x49, 0x14, 0x6A, 0x14, 0x8B, 0x14, 0xAD, 0x14, 0xCE, 0x14, 0xF0, 0x15, 0x12, 0x15, 0x34, 0x15, 0x56, 0x15, 0x78, 0x15, 0x9B, 0x15, 0xBD, 0x15, 0xE0, 0x16, 0x03, 0x16, 0x26, 0x16, 0x49, 0x16, 0x6C, 0x16, 0x8F, 0x16, 0xB2, 0x16, 0xD6, 0x16, 0xFA, 0x17, 0x1D, 0x17, 0x41, 0x17, 0x65, 0x17, 0x89, 0x17, 0xAE, 0x17, 0xD2, 0x17, 0xF7, 0x18, 0x1B, 0x18, 0x40, 0x18, 0x65, 0x18, 0x8A, 0x18, 0xAF, 0x18, 0xD5, 0x18, 0xFA, 0x19, 0x20, 0x19, 0x45, 0x19, 0x6B, 0x19, 0x91, 0x19, 0xB7, 0x19, 0xDD, 0x1A, 0x04, 0x1A, 0x2A, 0x1A, 0x51, 0x1A, 0x77, 0x1A, 0x9E, 0x1A, 0xC5, 0x1A, 0xEC, 0x1B, 0x14, 0x1B, 0x3B, 0x1B, 0x63, 0x1B, 0x8A, 0x1B, 0xB2, 0x1B, 0xDA, 0x1C, 0x02, 0x1C, 0x2A, 0x1C, 0x52, 0x1C, 0x7B, 0x1C, 0xA3, 0x1C, 0xCC, 0x1C, 0xF5, 0x1D, 0x1E, 0x1D, 0x47, 0x1D, 0x70, 0x1D, 0x99, 0x1D, 0xC3, 0x1D, 0xEC, 0x1E, 0x16, 0x1E, 0x40, 0x1E, 0x6A, 0x1E, 0x94, 0x1E, 0xBE, 0x1E, 0xE9, 0x1F, 0x13, 0x1F, 0x3E, 0x1F, 0x69, 0x1F, 0x94, 0x1F, 0xBF, 0x1F, 0xEA, 0x20, 0x15, 0x20, 0x41, 0x20, 0x6C, 0x20, 0x98, 0x20, 0xC4, 0x20, 0xF0, 0x21, 0x1C, 0x21, 0x48, 0x21, 0x75, 0x21, 0xA1, 0x21, 0xCE, 0x21, 0xFB, 0x22, 0x27, 0x22, 0x55, 0x22, 0x82, 0x22, 0xAF, 0x22, 0xDD, 0x23, 0x0A, 0x23, 0x38, 0x23, 0x66, 0x23, 0x94, 0x23, 0xC2, 0x23, 0xF0, 0x24, 0x1F, 0x24, 0x4D, 0x24, 0x7C, 0x24, 0xAB, 0x24, 0xDA, 0x25, 0x09, 0x25, 0x38, 0x25, 0x68, 0x25, 0x97, 0x25, 0xC7, 0x25, 0xF7, 0x26, 0x27, 0x26, 0x57, 0x26, 0x87, 0x26, 0xB7, 0x26, 0xE8, 0x27, 0x18, 0x27, 0x49, 0x27, 0x7A, 0x27, 0xAB, 0x27, 0xDC, 0x28, 0x0D, 0x28, 0x3F, 0x28, 0x71, 0x28, 0xA2, 0x28, 0xD4, 0x29, 0x06, 0x29, 0x38, 0x29, 0x6B, 0x29, 0x9D, 0x29, 0xD0, 0x2A, 0x02, 0x2A, 0x35, 0x2A, 0x68, 0x2A, 0x9B, 0x2A, 0xCF, 0x2B, 0x02, 0x2B, 0x36, 0x2B, 0x69, 0x2B, 0x9D, 0x2B, 0xD1, 0x2C, 0x05, 0x2C, 0x39, 0x2C, 0x6E, 0x2C, 0xA2, 0x2C, 0xD7, 0x2D, 0x0C, 0x2D, 0x41, 0x2D, 0x76, 0x2D, 0xAB, 0x2D, 0xE1, 0x2E, 0x16, 0x2E, 0x4C, 0x2E, 0x82, 0x2E, 0xB7, 0x2E, 0xEE, 0x2F, 0x24, 0x2F, 0x5A, 0x2F, 0x91, 0x2F, 0xC7, 0x2F, 0xFE, 0x30, 0x35, 0x30, 0x6C, 0x30, 0xA4, 0x30, 0xDB, 0x31, 0x12, 0x31, 0x4A, 0x31, 0x82, 0x31, 0xBA, 0x31, 0xF2, 0x32, 0x2A, 0x32, 0x63, 0x32, 0x9B, 0x32, 0xD4, 0x33, 0x0D, 0x33, 0x46, 0x33, 0x7F, 0x33, 0xB8, 0x33, 0xF1, 0x34, 0x2B, 0x34, 0x65, 0x34, 0x9E, 0x34, 0xD8, 0x35, 0x13, 0x35, 0x4D, 0x35, 0x87, 0x35, 0xC2, 0x35, 0xFD, 0x36, 0x37, 0x36, 0x72, 0x36, 0xAE, 0x36, 0xE9, 0x37, 0x24, 0x37, 0x60, 0x37, 0x9C, 0x37, 0xD7, 0x38, 0x14, 0x38, 0x50, 0x38, 0x8C, 0x38, 0xC8, 0x39, 0x05, 0x39, 0x42, 0x39, 0x7F, 0x39, 0xBC, 0x39, 0xF9, 0x3A, 0x36, 0x3A, 0x74, 0x3A, 0xB2, 0x3A, 0xEF, 0x3B, 0x2D, 0x3B, 0x6B, 0x3B, 0xAA, 0x3B, 0xE8, 0x3C, 0x27, 0x3C, 0x65, 0x3C, 0xA4, 0x3C, 0xE3, 0x3D, 0x22, 0x3D, 0x61, 0x3D, 0xA1, 0x3D, 0xE0, 0x3E, 0x20, 0x3E, 0x60, 0x3E, 0xA0, 0x3E, 0xE0, 0x3F, 0x21, 0x3F, 0x61, 0x3F, 0xA2, 0x3F, 0xE2, 0x40, 0x23, 0x40, 0x64, 0x40, 0xA6, 0x40, 0xE7, 0x41, 0x29, 0x41, 0x6A, 0x41, 0xAC, 0x41, 0xEE, 0x42, 0x30, 0x42, 0x72, 0x42, 0xB5, 0x42, 0xF7, 0x43, 0x3A, 0x43, 0x7D, 0x43, 0xC0, 0x44, 0x03, 0x44, 0x47, 0x44, 0x8A, 0x44, 0xCE, 0x45, 0x12, 0x45, 0x55, 0x45, 0x9A, 0x45, 0xDE, 0x46, 0x22, 0x46, 0x67, 0x46, 0xAB, 0x46, 0xF0, 0x47, 0x35, 0x47, 0x7B, 0x47, 0xC0, 0x48, 0x05, 0x48, 0x4B, 0x48, 0x91, 0x48, 0xD7, 0x49, 0x1D, 0x49, 0x63, 0x49, 0xA9, 0x49, 0xF0, 0x4A, 0x37, 0x4A, 0x7D, 0x4A, 0xC4, 0x4B, 0x0C, 0x4B, 0x53, 0x4B, 0x9A, 0x4B, 0xE2, 0x4C, 0x2A, 0x4C, 0x72, 0x4C, 0xBA, 0x4D, 0x02, 0x4D, 0x4A, 0x4D, 0x93, 0x4D, 0xDC, 0x4E, 0x25, 0x4E, 0x6E, 0x4E, 0xB7, 0x4F, 0x00, 0x4F, 0x49, 0x4F, 0x93, 0x4F, 0xDD, 0x50, 0x27, 0x50, 0x71, 0x50, 0xBB, 0x51, 0x06, 0x51, 0x50, 0x51, 0x9B, 0x51, 0xE6, 0x52, 0x31, 0x52, 0x7C, 0x52, 0xC7, 0x53, 0x13, 0x53, 0x5F, 0x53, 0xAA, 0x53, 0xF6, 0x54, 0x42, 0x54, 0x8F, 0x54, 0xDB, 0x55, 0x28, 0x55, 0x75, 0x55, 0xC2, 0x56, 0x0F, 0x56, 0x5C, 0x56, 0xA9, 0x56, 0xF7, 0x57, 0x44, 0x57, 0x92, 0x57, 0xE0, 0x58, 0x2F, 0x58, 0x7D, 0x58, 0xCB, 0x59, 0x1A, 0x59, 0x69, 0x59, 0xB8, 0x5A, 0x07, 0x5A, 0x56, 0x5A, 0xA6, 0x5A, 0xF5, 0x5B, 0x45, 0x5B, 0x95, 0x5B, 0xE5, 0x5C, 0x35, 0x5C, 0x86, 0x5C, 0xD6, 0x5D, 0x27, 0x5D, 0x78, 0x5D, 0xC9, 0x5E, 0x1A, 0x5E, 0x6C, 0x5E, 0xBD, 0x5F, 0x0F, 0x5F, 0x61, 0x5F, 0xB3, 0x60, 0x05, 0x60, 0x57, 0x60, 0xAA, 0x60, 0xFC, 0x61, 0x4F, 0x61, 0xA2, 0x61, 0xF5, 0x62, 0x49, 0x62, 0x9C, 0x62, 0xF0, 0x63, 0x43, 0x63, 0x97, 0x63, 0xEB, 0x64, 0x40, 0x64, 0x94, 0x64, 0xE9, 0x65, 0x3D, 0x65, 0x92, 0x65, 0xE7, 0x66, 0x3D, 0x66, 0x92, 0x66, 0xE8, 0x67, 0x3D, 0x67, 0x93, 0x67, 0xE9, 0x68, 0x3F, 0x68, 0x96, 0x68, 0xEC, 0x69, 0x43, 0x69, 0x9A, 0x69, 0xF1, 0x6A, 0x48, 0x6A, 0x9F, 0x6A, 0xF7, 0x6B, 0x4F, 0x6B, 0xA7, 0x6B, 0xFF, 0x6C, 0x57, 0x6C, 0xAF, 0x6D, 0x08, 0x6D, 0x60, 0x6D, 0xB9, 0x6E, 0x12, 0x6E, 0x6B, 0x6E, 0xC4, 0x6F, 0x1E, 0x6F, 0x78, 0x6F, 0xD1, 0x70, 0x2B, 0x70, 0x86, 0x70, 0xE0, 0x71, 0x3A, 0x71, 0x95, 0x71, 0xF0, 0x72, 0x4B, 0x72, 0xA6, 0x73, 0x01, 0x73, 0x5D, 0x73, 0xB8, 0x74, 0x14, 0x74, 0x70, 0x74, 0xCC, 0x75, 0x28, 0x75, 0x85, 0x75, 0xE1, 0x76, 0x3E, 0x76, 0x9B, 0x76, 0xF8, 0x77, 0x56, 0x77, 0xB3, 0x78, 0x11, 0x78, 0x6E, 0x78, 0xCC, 0x79, 0x2A, 0x79, 0x89, 0x79, 0xE7, 0x7A, 0x46, 0x7A, 0xA5, 0x7B, 0x04, 0x7B, 0x63, 0x7B, 0xC2, 0x7C, 0x21, 0x7C, 0x81, 0x7C, 0xE1, 0x7D, 0x41, 0x7D, 0xA1, 0x7E, 0x01, 0x7E, 0x62, 0x7E, 0xC2, 0x7F, 0x23, 0x7F, 0x84, 0x7F, 0xE5, 0x80, 0x47, 0x80, 0xA8, 0x81, 0x0A, 0x81, 0x6B, 0x81, 0xCD, 0x82, 0x30, 0x82, 0x92, 0x82, 0xF4, 0x83, 0x57, 0x83, 0xBA, 0x84, 0x1D, 0x84, 0x80, 0x84, 0xE3, 0x85, 0x47, 0x85, 0xAB, 0x86, 0x0E, 0x86, 0x72, 0x86, 0xD7, 0x87, 0x3B, 0x87, 0x9F, 0x88, 0x04, 0x88, 0x69, 0x88, 0xCE, 0x89, 0x33, 0x89, 0x99, 0x89, 0xFE, 0x8A, 0x64, 0x8A, 0xCA, 0x8B, 0x30, 0x8B, 0x96, 0x8B, 0xFC, 0x8C, 0x63, 0x8C, 0xCA, 0x8D, 0x31, 0x8D, 0x98, 0x8D, 0xFF, 0x8E, 0x66, 0x8E, 0xCE, 0x8F, 0x36, 0x8F, 0x9E, 0x90, 0x06, 0x90, 0x6E, 0x90, 0xD6, 0x91, 0x3F, 0x91, 0xA8, 0x92, 0x11, 0x92, 0x7A, 0x92, 0xE3, 0x93, 0x4D, 0x93, 0xB6, 0x94, 0x20, 0x94, 0x8A, 0x94, 0xF4, 0x95, 0x5F, 0x95, 0xC9, 0x96, 0x34, 0x96, 0x9F, 0x97, 0x0A, 0x97, 0x75, 0x97, 0xE0, 0x98, 0x4C, 0x98, 0xB8, 0x99, 0x24, 0x99, 0x90, 0x99, 0xFC, 0x9A, 0x68, 0x9A, 0xD5, 0x9B, 0x42, 0x9B, 0xAF, 0x9C, 0x1C, 0x9C, 0x89, 0x9C, 0xF7, 0x9D, 0x64, 0x9D, 0xD2, 0x9E, 0x40, 0x9E, 0xAE, 0x9F, 0x1D, 0x9F, 0x8B, 0x9F, 0xFA, 0xA0, 0x69, 0xA0, 0xD8, 0xA1, 0x47, 0xA1, 0xB6, 0xA2, 0x26, 0xA2, 0x96, 0xA3, 0x06, 0xA3, 0x76, 0xA3, 0xE6, 0xA4, 0x56, 0xA4, 0xC7, 0xA5, 0x38, 0xA5, 0xA9, 0xA6, 0x1A, 0xA6, 0x8B, 0xA6, 0xFD, 0xA7, 0x6E, 0xA7, 0xE0, 0xA8, 0x52, 0xA8, 0xC4, 0xA9, 0x37, 0xA9, 0xA9, 0xAA, 0x1C, 0xAA, 0x8F, 0xAB, 0x02, 0xAB, 0x75, 0xAB, 0xE9, 0xAC, 0x5C, 0xAC, 0xD0, 0xAD, 0x44, 0xAD, 0xB8, 0xAE, 0x2D, 0xAE, 0xA1, 0xAF, 0x16, 0xAF, 0x8B, 0xB0, 0x00, 0xB0, 0x75, 0xB0, 0xEA, 0xB1, 0x60, 0xB1, 0xD6, 0xB2, 0x4B, 0xB2, 0xC2, 0xB3, 0x38, 0xB3, 0xAE, 0xB4, 0x25, 0xB4, 0x9C, 0xB5, 0x13, 0xB5, 0x8A, 0xB6, 0x01, 0xB6, 0x79, 0xB6, 0xF0, 0xB7, 0x68, 0xB7, 0xE0, 0xB8, 0x59, 0xB8, 0xD1, 0xB9, 0x4A, 0xB9, 0xC2, 0xBA, 0x3B, 0xBA, 0xB5, 0xBB, 0x2E, 0xBB, 0xA7, 0xBC, 0x21, 0xBC, 0x9B, 0xBD, 0x15, 0xBD, 0x8F, 0xBE, 0x0A, 0xBE, 0x84, 0xBE, 0xFF, 0xBF, 0x7A, 0xBF, 0xF5, 0xC0, 0x70, 0xC0, 0xEC, 0xC1, 0x67, 0xC1, 0xE3, 0xC2, 0x5F, 0xC2, 0xDB, 0xC3, 0x58, 0xC3, 0xD4, 0xC4, 0x51, 0xC4, 0xCE, 0xC5, 0x4B, 0xC5, 0xC8, 0xC6, 0x46, 0xC6, 0xC3, 0xC7, 0x41, 0xC7, 0xBF, 0xC8, 0x3D, 0xC8, 0xBC, 0xC9, 0x3A, 0xC9, 0xB9, 0xCA, 0x38, 0xCA, 0xB7, 0xCB, 0x36, 0xCB, 0xB6, 0xCC, 0x35, 0xCC, 0xB5, 0xCD, 0x35, 0xCD, 0xB5, 0xCE, 0x36, 0xCE, 0xB6, 0xCF, 0x37, 0xCF, 0xB8, 0xD0, 0x39, 0xD0, 0xBA, 0xD1, 0x3C, 0xD1, 0xBE, 0xD2, 0x3F, 0xD2, 0xC1, 0xD3, 0x44, 0xD3, 0xC6, 0xD4, 0x49, 0xD4, 0xCB, 0xD5, 0x4E, 0xD5, 0xD1, 0xD6, 0x55, 0xD6, 0xD8, 0xD7, 0x5C, 0xD7, 0xE0, 0xD8, 0x64, 0xD8, 0xE8, 0xD9, 0x6C, 0xD9, 0xF1, 0xDA, 0x76, 0xDA, 0xFB, 0xDB, 0x80, 0xDC, 0x05, 0xDC, 0x8A, 0xDD, 0x10, 0xDD, 0x96, 0xDE, 0x1C, 0xDE, 0xA2, 0xDF, 0x29, 0xDF, 0xAF, 0xE0, 0x36, 0xE0, 0xBD, 0xE1, 0x44, 0xE1, 0xCC, 0xE2, 0x53, 0xE2, 0xDB, 0xE3, 0x63, 0xE3, 0xEB, 0xE4, 0x73, 0xE4, 0xFC, 0xE5, 0x84, 0xE6, 0x0D, 0xE6, 0x96, 0xE7, 0x1F, 0xE7, 0xA9, 0xE8, 0x32, 0xE8, 0xBC, 0xE9, 0x46, 0xE9, 0xD0, 0xEA, 0x5B, 0xEA, 0xE5, 0xEB, 0x70, 0xEB, 0xFB, 0xEC, 0x86, 0xED, 0x11, 0xED, 0x9C, 0xEE, 0x28, 0xEE, 0xB4, 0xEF, 0x40, 0xEF, 0xCC, 0xF0, 0x58, 0xF0, 0xE5, 0xF1, 0x72, 0xF1, 0xFF, 0xF2, 0x8C, 0xF3, 0x19, 0xF3, 0xA7, 0xF4, 0x34, 0xF4, 0xC2, 0xF5, 0x50, 0xF5, 0xDE, 0xF6, 0x6D, 0xF6, 0xFB, 0xF7, 0x8A, 0xF8, 0x19, 0xF8, 0xA8, 0xF9, 0x38, 0xF9, 0xC7, 0xFA, 0x57, 0xFA, 0xE7, 0xFB, 0x77, 0xFC, 0x07, 0xFC, 0x98, 0xFD, 0x29, 0xFD, 0xBA, 0xFE, 0x4B, 0xFE, 0xDC, 0xFF, 0x6D, 0xFF, 0xFF +``` + diff --git a/doc/tables/is_zero_base.md b/doc/tables/is_zero_base.md new file mode 100644 index 000000000..7e2d081f3 --- /dev/null +++ b/doc/tables/is_zero_base.md @@ -0,0 +1,9 @@ +#### Table M.1 – is_zero_base table + +``` +228, 216, 216, 195, 192, 189, 182, 184, 179, 176, 171, 168, 166, 159, +156, 151, 151, 150, 150, 146, 144, 138, 138, 137, 135, 131, 127, 126, +124, 123, 124, 123, 122, 121, 118, 117, 114, 115, 116, 116, 115, 115, +114, 111, 111, 111, 112, 111, 110, 110, 110, 111, 111, 114, 110, 111, +112, 113, 116, 120, 126, 131, 147, 160 +``` diff --git a/doc/tables/markdown-pdf.css b/doc/tables/markdown-pdf.css new file mode 100644 index 000000000..c1efc1cc2 --- /dev/null +++ b/doc/tables/markdown-pdf.css @@ -0,0 +1,36 @@ +/* + settings.json: + "markdown-pdf.styles": ["markdown-pdf.css",], + "markdown-pdf.format": "Letter", + "markdown-pdf.margin.top": "1in", + "markdown-pdf.margin.bottom": "1in", + "markdown-pdf.margin.left": "1in", + "markdown-pdf.margin.right": "1in", + "markdown-pdf.stylesRelativePathFile" : true, + "markdown-pdf.displayHeaderFooter": false, + */ + +body { + font-family: "Times"; + font-size: 10pt; + padding: 0; +} + +h4 { + font-family: "Times New Roman"; + font-size: 10pt; + font-weight: bold; +} + +code { + font-family: Consolas, "Source Code Pro"; + font-size: 10pt; +} + +pre.hljs code > div { + padding: 0px; +} + +:not(pre):not(.hljs) > code { + color: #4d4d4c; +} diff --git a/doc/tables/nonzero_buckets.md b/doc/tables/nonzero_buckets.md new file mode 100644 index 000000000..77a5a396e --- /dev/null +++ b/doc/tables/nonzero_buckets.md @@ -0,0 +1,9 @@ +#### Table M.17 – nonzero_buckets + +``` + 0, 1, 2, 3, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, + 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 +``` + diff --git a/doc/tables/num_nonzero_context.md b/doc/tables/num_nonzero_context.md new file mode 100644 index 000000000..b73d48c38 --- /dev/null +++ b/doc/tables/num_nonzero_context.md @@ -0,0 +1,60 @@ +#### Table M.16 – num_nonzero_context + +`scheme == 0`: +``` +0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, +6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, +7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 +``` + +`scheme == 1`: +``` + 0, 2, 2, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, +10, 10, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, +12, 12, 12, 12, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, +14, 14, 14, 14, 14, 14, 14, 14, 14, 14 +``` + +`scheme == 2`: +``` + 0, 4, 4, 8, 8, 8, 12, 12, 12, 12, 16, 16, 16, 16, 16, 16, 20, 20, +20, 20, 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, +24, 24, 24, 24, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, +28, 28, 28, 28, 28, 28, 28, 28, 28, 28 +``` + +`scheme == 3`: +``` + 0, 8, 8, 16, 16, 16, 24, 24, 24, 24, 32, 32, 32, 32, 32, 32, 40, 40, +40, 40, 40, 40, 40, 40, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, +48, 48, 48, 48, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, +55, 55, 55, 55, 55, 55, 55, 55, 55, 55 +``` + +`scheme == 4`: +``` + 0, 16, 16, 32, 32, 32, 48, 48, 48, 48, 64, 64, 64, 64, + 64, 64, 80, 80, 80, 80, 80, 80, 80, 80, 95, 95, 95, 95, + 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 95, 109, 109, +109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, 109, +109, 109, 109, 109, 109, 109, 109, 109 +``` + +`scheme == 5`: +``` + 0, 32, 32, 64, 64, 64, 96, 96, 96, 96, 127, 127, 127, 127, +127, 127, 157, 157, 157, 157, 157, 157, 157, 157, 185, 185, 185, 185, +185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 185, 211, 211, +211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, 211, +211, 211, 211, 211, 211, 211, 211, 211 +``` + +`scheme == 6`: +``` + 0, 64, 64, 127, 127, 127, 188, 188, 188, 188, 246, 246, 246, 246, +246, 246, 300, 300, 300, 300, 300, 300, 300, 300, 348, 348, 348, 348, +348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 348, 388, 388, +388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, 388, +388, 388, 388, 388, 388, 388, 388, 388 +``` + diff --git a/doc/tables/num_nonzeros_base.md b/doc/tables/num_nonzeros_base.md new file mode 100644 index 000000000..165c7389c --- /dev/null +++ b/doc/tables/num_nonzeros_base.md @@ -0,0 +1,258 @@ +#### Table M.2 – num_nonzeros_base table + +``` +251, 252, 117, 249, 161, 136, 83, 238, 184, 126, 137, 129, 140, 119, + 70, 213, 160, 175, 174, 130, 166, 134, 122, 125, 131, 144, 136, 133, +139, 123, 79, 216, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +254, 252, 174, 232, 189, 155, 122, 177, 204, 173, 146, 149, 141, 133, +103, 109, 167, 187, 168, 142, 154, 147, 125, 139, 144, 138, 138, 153, +141, 133, 90, 121, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +251, 240, 197, 176, 184, 177, 114, 89, 194, 165, 153, 161, 158, 136, + 92, 95, 123, 171, 160, 140, 148, 136, 129, 139, 145, 136, 143, 134, +138, 124, 92, 154, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +247, 220, 201, 110, 194, 176, 147, 59, 175, 171, 156, 157, 152, 146, +115, 114, 88, 151, 164, 141, 153, 135, 141, 131, 146, 139, 140, 145, +138, 137, 112, 184, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +238, 179, 203, 63, 194, 173, 149, 71, 139, 169, 154, 159, 150, 146, +117, 143, 78, 122, 152, 137, 149, 138, 138, 133, 134, 142, 142, 142, +148, 128, 118, 199, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +227, 127, 200, 44, 192, 170, 148, 100, 102, 161, 156, 153, 148, 149, +124, 160, 88, 101, 134, 132, 149, 145, 134, 134, 136, 141, 138, 142, +144, 137, 116, 208, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +214, 86, 195, 44, 187, 163, 148, 126, 81, 147, 156, 152, 150, 144, +121, 172, 96, 95, 117, 122, 145, 152, 136, 133, 135, 135, 131, 142, +141, 135, 114, 217, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +198, 56, 191, 54, 171, 162, 147, 144, 74, 128, 152, 149, 150, 142, +119, 177, 101, 100, 106, 111, 135, 154, 136, 137, 136, 132, 133, 142, +144, 130, 117, 222, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +176, 40, 189, 73, 147, 159, 148, 152, 79, 106, 147, 149, 151, 139, +123, 188, 108, 110, 106, 97, 125, 151, 137, 138, 135, 135, 134, 136, +140, 131, 116, 221, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +148, 33, 185, 88, 117, 158, 145, 163, 95, 91, 137, 146, 150, 140, +120, 197, 115, 116, 114, 92, 114, 144, 130, 133, 132, 133, 129, 140, +138, 130, 111, 224, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` +117, 31, 180, 104, 93, 150, 143, 166, 99, 85, 124, 139, 148, 142, +118, 201, 105, 120, 120, 90, 107, 135, 127, 130, 131, 131, 132, 140, +142, 133, 114, 229, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 87, 35, 170, 110, 78, 141, 144, 176, 106, 90, 112, 132, 143, 138, +119, 204, 111, 121, 125, 90, 105, 131, 124, 122, 129, 128, 129, 137, +138, 133, 114, 227, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 63, 42, 159, 123, 73, 127, 142, 191, 105, 91, 105, 123, 139, 137, +120, 209, 117, 110, 122, 98, 110, 125, 115, 123, 122, 126, 128, 134, +141, 129, 113, 229, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 45, 53, 146, 135, 71, 114, 138, 193, 100, 98, 98, 113, 133, 135, +118, 222, 113, 111, 139, 103, 107, 126, 111, 119, 121, 122, 127, 135, +141, 128, 114, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 33, 60, 132, 138, 75, 100, 134, 203, 112, 99, 98, 105, 126, 131, +115, 229, 107, 93, 121, 106, 108, 122, 106, 109, 114, 116, 127, 133, +143, 128, 110, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 24, 70, 118, 134, 76, 87, 130, 201, 110, 96, 99, 97, 119, 130, +111, 229, 97, 104, 125, 102, 112, 125, 101, 109, 113, 114, 125, 129, +142, 127, 112, 241, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 17, 65, 100, 121, 80, 75, 124, 174, 117, 100, 94, 93, 114, 128, +110, 216, 103, 94, 113, 122, 118, 126, 113, 108, 105, 108, 122, 128, +141, 125, 113, 238, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 12, 70, 82, 132, 78, 65, 118, 155, 136, 103, 97, 89, 106, 124, +111, 215, 115, 123, 129, 99, 104, 127, 110, 108, 101, 109, 118, 126, +136, 123, 110, 233, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 8, 66, 61, 117, 91, 59, 108, 195, 101, 112, 99, 99, 99, 116, +106, 230, 127, 99, 144, 101, 118, 137, 117, 111, 106, 104, 116, 121, +134, 122, 110, 223, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 6, 78, 42, 146, 101, 54, 94, 201, 116, 102, 110, 94, 92, 108, +103, 214, 108, 111, 127, 102, 121, 132, 120, 121, 95, 98, 110, 121, +129, 117, 107, 235, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 5, 93, 29, 145, 102, 52, 77, 216, 108, 115, 108, 102, 89, 97, + 94, 229, 89, 103, 139, 120, 103, 151, 102, 100, 97, 96, 99, 111, +125, 116, 104, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 4, 105, 21, 145, 100, 54, 64, 217, 100, 122, 128, 87, 88, 91, + 87, 230, 112, 80, 148, 95, 146, 123, 96, 140, 90, 91, 98, 106, +122, 111, 100, 249, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 4, 130, 14, 142, 104, 56, 51, 208, 116, 135, 100, 89, 82, 84, + 75, 239, 85, 85, 122, 125, 94, 144, 151, 136, 92, 97, 104, 109, +113, 110, 91, 246, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 126, 9, 172, 105, 57, 39, 219, 95, 120, 118, 96, 93, 75, + 66, 241, 102, 134, 96, 156, 146, 162, 130, 112, 82, 89, 97, 101, +116, 103, 82, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 149, 7, 182, 122, 54, 29, 224, 103, 100, 113, 96, 90, 74, + 55, 250, 127, 94, 118, 93, 135, 160, 113, 130, 95, 117, 106, 96, +111, 97, 77, 242, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 150, 4, 170, 138, 59, 20, 229, 91, 150, 107, 98, 92, 68, + 48, 245, 113, 64, 114, 111, 134, 127, 102, 104, 85, 118, 103, 107, +102, 91, 72, 245, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 3, 171, 3, 165, 137, 62, 14, 211, 96, 127, 132, 121, 95, 62, + 37, 248, 102, 57, 144, 85, 127, 191, 102, 97, 127, 104, 91, 102, +107, 81, 64, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 166, 2, 196, 122, 65, 10, 243, 102, 93, 117, 92, 96, 63, + 29, 251, 169, 159, 149, 96, 91, 139, 157, 40, 100, 89, 120, 92, +109, 79, 58, 247, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 176, 2, 189, 118, 48, 7, 219, 68, 43, 109, 96, 129, 75, + 19, 254, 2, 3, 185, 6, 102, 127, 127, 127, 1, 131, 83, 99, +107, 80, 45, 254, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 1, 205, 2, 208, 64, 89, 4, 223, 29, 169, 29, 123, 118, 76, + 11, 240, 202, 243, 65, 6, 12, 243, 96, 55, 102, 102, 114, 102, +107, 74, 31, 247, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 1, 216, 1, 214, 127, 94, 2, 234, 145, 3, 127, 106, 155, 80, + 4, 247, 4, 65, 86, 127, 127, 127, 127, 102, 127, 143, 143, 108, +113, 80, 16, 216, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + +``` + 2, 199, 1, 222, 93, 94, 1, 232, 2, 65, 74, 139, 201, 48, + 2, 254, 169, 127, 52, 243, 251, 249, 102, 86, 202, 153, 65, 65, +146, 69, 8, 238, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +128, 128, 128, 128, 128, 128, 128 +``` + diff --git a/doc/tables/quant.md b/doc/tables/quant.md new file mode 100644 index 000000000..1fb80d754 --- /dev/null +++ b/doc/tables/quant.md @@ -0,0 +1,19 @@ +#### Table M.13 – template quant tables + +`is_luma == true`: +``` + 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, + 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, + 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, +104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, +112, 100, 103, 99 +``` + +`is_luma == false`: +``` +17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, +56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, +99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, +99, 99, 99, 99, 99, 99, 99, 99, 99, 99 +``` + diff --git a/doc/tables/stock_counts.md b/doc/tables/stock_counts.md new file mode 100644 index 000000000..6e8e4458c --- /dev/null +++ b/doc/tables/stock_counts.md @@ -0,0 +1,22 @@ +#### Table M.9 – stock counts arrays + +`is_ac == 0`, `stock_index == 0`: +``` +0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0 +``` + +`is_ac == 0`, `stock_index == 1`: +``` +0, 0, 1, 5, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0 +``` + +`is_ac == 1`, `stock_index == 0`: +``` +0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 126 +``` + +`is_ac == 1`, `stock_index == 1`: +``` +0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 120 +``` + diff --git a/doc/tables/stock_quant.md b/doc/tables/stock_quant.md new file mode 100644 index 000000000..b32fd3c7d --- /dev/null +++ b/doc/tables/stock_quant.md @@ -0,0 +1,130 @@ +#### Table M.12 – stock quant tables + +`is_luma == true`, `stock_index == 0`: +``` + 3, 2, 2, 3, 5, 8, 10, 12, 2, 2, 3, 4, 5, 12, 12, 11, 3, 3, + 3, 5, 8, 11, 14, 11, 3, 3, 4, 6, 10, 17, 16, 12, 4, 4, 7, 11, +14, 22, 21, 15, 5, 7, 11, 13, 16, 21, 23, 18, 10, 13, 16, 17, 21, 24, +24, 20, 14, 18, 19, 20, 22, 20, 21, 20 +``` + +`is_luma == true`, `stock_index == 1`: +``` + 8, 6, 5, 8, 12, 20, 26, 31, 6, 6, 7, 10, 13, 29, 30, 28, 7, 7, + 8, 12, 20, 29, 35, 28, 7, 9, 11, 15, 26, 44, 40, 31, 9, 11, 19, 28, +34, 55, 52, 39, 12, 18, 28, 32, 41, 52, 57, 46, 25, 32, 39, 44, 52, 61, +60, 51, 36, 46, 48, 49, 56, 50, 52, 50 +``` + +`is_luma == true`, `stock_index == 2`: +``` + 6, 4, 4, 6, 10, 16, 20, 24, 5, 5, 6, 8, 10, 23, 24, 22, 6, 5, + 6, 10, 16, 23, 28, 22, 6, 7, 9, 12, 20, 35, 32, 25, 7, 9, 15, 22, +27, 44, 41, 31, 10, 14, 22, 26, 32, 42, 45, 37, 20, 26, 31, 35, 41, 48, +48, 40, 29, 37, 38, 39, 45, 40, 41, 40 +``` + +`is_luma == true`, `stock_index == 3`: +``` + 5, 3, 3, 5, 7, 12, 15, 18, 4, 4, 4, 6, 8, 17, 18, 17, 4, 4, + 5, 7, 12, 17, 21, 17, 4, 5, 7, 9, 15, 26, 24, 19, 5, 7, 11, 17, +20, 33, 31, 23, 7, 11, 17, 19, 24, 31, 34, 28, 15, 19, 23, 26, 31, 36, +36, 30, 22, 28, 29, 29, 34, 30, 31, 30 +``` + +`is_luma == true`, `stock_index == 4`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +``` + +`is_luma == true`, `stock_index == 5`: +``` + 2, 1, 1, 2, 2, 4, 5, 6, 1, 1, 1, 2, 3, 6, 6, 6, 1, 1, + 2, 2, 4, 6, 7, 6, 1, 2, 2, 3, 5, 9, 8, 6, 2, 2, 4, 6, + 7, 11, 10, 8, 2, 4, 6, 6, 8, 10, 11, 9, 5, 6, 8, 9, 10, 12, +12, 10, 7, 9, 10, 10, 11, 10, 10, 10 +``` + +`is_luma == true`, `stock_index == 6`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, + 1, 2, 2, 3, 1, 1, 1, 1, 2, 2, 3, 3, 1, 1, 1, 2, 2, 3, + 3, 3, 1, 1, 2, 2, 3, 3, 3, 3 +``` + +`is_luma == true`, `stock_index == 7`: +``` +10, 7, 6, 10, 14, 24, 31, 37, 7, 7, 8, 11, 16, 35, 36, 33, 8, 8, +10, 14, 24, 34, 41, 34, 8, 10, 13, 17, 31, 52, 48, 37, 11, 13, 22, 34, +41, 65, 62, 46, 14, 21, 33, 38, 49, 62, 68, 55, 29, 38, 47, 52, 62, 73, +72, 61, 43, 55, 57, 59, 67, 60, 62, 59 +``` + +`is_luma == false`, `stock_index == 0`: +``` + 9, 9, 9, 12, 11, 12, 24, 13, 13, 24, 50, 33, 28, 33, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50 +``` + +`is_luma == false`, `stock_index == 1`: +``` + 3, 4, 5, 9, 20, 20, 20, 20, 4, 4, 5, 13, 20, 20, 20, 20, 5, 5, +11, 20, 20, 20, 20, 20, 9, 13, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, +20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, +20, 20, 20, 20, 20, 20, 20, 20, 20, 20 +``` + +`is_luma == false`, `stock_index == 2`: +``` + 9, 9, 12, 24, 50, 50, 50, 50, 9, 11, 13, 33, 50, 50, 50, 50, 12, 13, +28, 50, 50, 50, 50, 50, 24, 33, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, +50, 50, 50, 50, 50, 50, 50, 50, 50, 50 +``` + +`is_luma == false`, `stock_index == 3`: +``` + 5, 5, 7, 14, 30, 30, 30, 30, 5, 6, 8, 20, 30, 30, 30, 30, 7, 8, +17, 30, 30, 30, 30, 30, 14, 20, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, +30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, +30, 30, 30, 30, 30, 30, 30, 30, 30, 30 +``` + +`is_luma == false`, `stock_index == 4`: +``` + 7, 7, 10, 19, 40, 40, 40, 40, 7, 8, 10, 26, 40, 40, 40, 40, 10, 10, +22, 40, 40, 40, 40, 40, 19, 26, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, +40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, +40, 40, 40, 40, 40, 40, 40, 40, 40, 40 +``` + +`is_luma == false`, `stock_index == 5`: +``` + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +``` + +`is_luma == false`, `stock_index == 6`: +``` + 2, 2, 2, 5, 10, 10, 10, 10, 2, 2, 3, 7, 10, 10, 10, 10, 2, 3, + 6, 10, 10, 10, 10, 10, 5, 7, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, +10, 10, 10, 10, 10, 10, 10, 10, 10, 10 +``` + +`is_luma == false`, `stock_index == 7`: +``` +10, 11, 14, 28, 59, 59, 59, 59, 11, 13, 16, 40, 59, 59, 59, 59, 14, 16, +34, 59, 59, 59, 59, 59, 28, 40, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, +59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, +59, 59, 59, 59, 59, 59, 59, 59, 59, 59 +``` + diff --git a/doc/tables/stock_values.md b/doc/tables/stock_values.md new file mode 100644 index 000000000..8e67cff1e --- /dev/null +++ b/doc/tables/stock_values.md @@ -0,0 +1,44 @@ +#### Table M.10 – stock values arrays + +`is_ac == 0`, `stock_index == 0`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 256 +``` + +`is_ac == 0`, `stock_index == 1`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 256 +``` + +`is_ac == 1`, `stock_index == 0`: +``` + 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6, 19, 81, + 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193, + 21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, + 25, 26, 37, 38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, + 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, + 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, +118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, 138, 146, +147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, +168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, +196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, +217, 218, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 241, 242, +243, 244, 245, 246, 247, 248, 249, 250, 256 +``` + +`is_ac == 1`, `stock_index == 1`: +``` + 0, 1, 2, 3, 17, 4, 5, 33, 49, 6, 18, 65, 81, 7, + 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177, 193, 9, + 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37, +241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, + 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, + 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, +117, 118, 119, 120, 121, 122, 130, 131, 132, 133, 134, 135, 136, 137, +138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, +166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, +194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, +215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, +243, 244, 245, 246, 247, 248, 249, 250, 256 +``` + diff --git a/doc/tables/symbol_order.md b/doc/tables/symbol_order.md new file mode 100644 index 000000000..a196c0f52 --- /dev/null +++ b/doc/tables/symbol_order.md @@ -0,0 +1,30 @@ +#### Table M.11 – predefined symbol order + +`is_ac == 0`: +``` +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 +``` + +`is_ac == 1`: +``` + 1, 0, 2, 3, 17, 4, 5, 33, 18, 49, 65, 6, 81, 19, + 97, 7, 34, 113, 50, 129, 20, 145, 161, 8, 35, 66, 177, 193, + 21, 82, 209, 240, 36, 51, 98, 114, 9, 130, 10, 22, 52, 225, + 23, 37, 241, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, + 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, + 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, +116, 117, 118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, +138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, +166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, +194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, +215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, +243, 244, 245, 246, 247, 248, 249, 250, 16, 32, 48, 64, 80, 96, +112, 128, 144, 160, 176, 192, 208, 11, 12, 13, 14, 15, 27, 28, + 29, 30, 31, 43, 44, 45, 46, 47, 59, 60, 61, 62, 63, 75, + 76, 77, 78, 79, 91, 92, 93, 94, 95, 107, 108, 109, 110, 111, +123, 124, 125, 126, 127, 139, 140, 141, 142, 143, 155, 156, 157, 158, +159, 171, 172, 173, 174, 175, 187, 188, 189, 190, 191, 203, 204, 205, +206, 207, 219, 220, 221, 222, 223, 224, 235, 236, 237, 238, 239, 251, +252, 253, 254, 255 +``` + diff --git a/doc/xl_overview.md b/doc/xl_overview.md new file mode 100644 index 000000000..59e0c252d --- /dev/null +++ b/doc/xl_overview.md @@ -0,0 +1,181 @@ +# XL Overview + +## Requirements + +JPEG XL was designed for two main requirements: + +* high quality: visually lossless at reasonable bitrates; +* decoding speed: multithreaded decoding should be able to reach around + 400 Megapixel/s on large images. + +These goals apply to various types of images, including HDR content, whose +support is made possible by full-precision (float32) computations and extensive +support of color spaces and transfer functions. + +High performance is achieved by designing the format with careful consideration +of memory bandwidth usage and ease of SIMD/GPU implementation. + +The full requirements for JPEG XL are listed in document wg1m82079. + +## General architecture + +The architecture follows the traditional block transform model with improvements +in the individual components. For a quick overview, we sketch a "block diagram" +of the lossy format decoder in the form of module names in **bold** followed by +a brief description. Note that post-processing modules in [brackets] are +optional - they are unnecessary or even counterproductive at very high quality +settings. + +**Header**: decode metadata (e.g. image dimensions) from compressed fields +(smaller than Exp-Golomb thanks to per-field encodings). The compression and +small number of required fields enables very compact headers - much smaller than +JFIF and HEVC. The container supports multiple images (e.g. animations/bursts) +and passes (progressive). + +**Bitstream**: decode transform coefficient residuals using rANS-encoded +<#bits,bits> symbols + +**Dequantize**: from adaptive quant map side information, plus chroma from luma + +**DC prediction**: expand DC residuals using adaptive (history-based) predictors + +**Chroma from luma**: restore predicted X from B and Y from B + +**IDCT:** 2x2..32x32, floating-point + +**[Gaborish]**: additional deblocking convolution with 3x3 kernel + +**[Edge preserving filter]**: nonlinear adaptive smoothing controlled by side +information + +**[Noise injection]**: add perceptually pleasing noise according to a per-image +noise model + +**Color space conversion**: from perceptual opsin XYB to linear RGB + +**[Converting to other color spaces via ICC]** + +The encoder is basically the reverse: + +**Color space conversion**: from linear RGB to perceptual opsin XYB + +**[Noise estimation]**: compute a noise model for the image + +**[Gaborish]**: sharpening to counteract the blurring on the decoder side + +**DCT**: transform sizes communicated via per-block side information + +**Chroma from luma**: find the best multipliers of Y for X and B channels of +entire image + +**Adaptive quantization**: iterative search for quant map that yields the best +perceived restoration + +**Quantize**: store 16-bit prediction residuals + +**DC prediction**: store residuals (prediction happens in quantized space) + +**Entropy coding**: rANS and context modeling with clustering + + +# File Structure + +A codestream begins with a `FileHeader` followed by one or more "passes" +(= scans: e.g. DC or AC_LF) which are then added together (summing the +respective color components in Opsin space) to form the final image. There is no +limit to the number of passes, so an encoder could choose to send salient parts +first, followed by arbitrary decompositions of the final image (in terms of +resolution, bit depth, quality or spatial location). + +Each pass contains groups of AC and DC data. A group is a subset of pixels that +can be decoded in parallel. DC groups contain 256x256 DCs (from 2048x2048 input +pixels), AC groups cover 256x256 input pixels. + +Each pass starts with a table of contents (sizes of each of their DC+AC +groups), which enables parallel decoding and/or the decoding of a subset. +However, there is no higher-level TOC of passes, as that would prevent +appending additional images and could be too constraining for the encoder. + + +## Lossless + +JPEG XL supports tools for lossless coding designed by Alexander Rhatushnyak and +Jon Sneyers. They are about 60-75% of size of PNG, and smaller than WebP +lossless for photos. + +An adaptive predictor computes 4 from the NW, N, NE and W pixels and combines +them with weights based on previous errors. The error value is encoded in a +bucket chosen based on a heuristic max error. The result is entropy-coded using +the ANS encoder. + +## Current Reference Implementation + +### Conventions + +The software is written in C++ and built using CMake 3.6 or later. + +Error handling is done by having functions return values of type `jxl::Status` +(a thin wrapper around bool which checks that it is not ignored). A convenience +macro named `JXL_RETURN_IF_ERROR` makes this more convenient by automatically +forwarding errors, and another macro named `JXL_FAILURE` exits with an error +message if reached, with no effect in optimized builds. + +To diagnose the cause of encoder/decoder failures (which often only result in a +generic "decode failed" message), build using the following command: + +```bash +CMAKE_FLAGS="-DJXL_CRASH_ON_ERROR" ./ci.sh opt +``` + +In such builds, the first JXL_FAILURE will print a message identifying where the +problem is and the program will exit immediately afterwards. + +### Architecture + +Getting back to the earlier block diagram: + +**Header** handling is implemented in `headers.h` and `field*`. + +**Bitstream**: `entropy_coder.h`, `dec_ans_*`. + +**(De)quantize**: `quantizer.h`. + +**DC prediction**: `predictor.h`. + +**Chroma from luma**: `chroma_from_luma.h` + +**(I)DCT**: `dct.h`. Instead of operating directly on blocks of memory, the +functions operate on thin wrappers (declared in `block.h`) which can handle +blocks spread across multiple image lines. + +**DCT size selection**: `ac_strategy.cc` + +**[Gaborish]**: `gaborish.h`. + +**[Edge preserving filter]**: `epf.h` + +**[Noise injection]**: `noise*` (currently disabled) + +**Color space conversion**: `color_*`, `dec_xyb.h`. + +## Decoder overview + +After decoding headers, the decoder begins processing frames (`dec_frame.cc`). + +For each pass, it will read the DC group table of contents (TOC) and start +decoding, dequantizing and restoring color correlation of each DC group +(covering 2048x2048 pixels in the input image) in parallel +(`compressed_dc.cc`). The DC is split into parts corresponding to each AC group +(with 1px of extra border); the AC group TOC is read and each AC group (256x256 +pixels) is processed in parallel (`dec_group.cc`). + +In each AC group, the decoder reads per-block side information indicating the +kind of DCT transform; this is followed by the quantization field. Then, AC +coefficients are read, dequantized and have color correlation restored on a +tile per tile basis for better locality. + +After all the groups are read, postprocessing is applied: Gaborish smoothing +and edge preserving filter, to reduce blocking and other artifacts. + +Finally, the image is converted back from the XYB color space +(`dec_xyb.cc`) and saved to the output image (`codec_*.cc`). diff --git a/docker/Dockerfile.jpegxl-builder b/docker/Dockerfile.jpegxl-builder new file mode 100644 index 000000000..552fadbe4 --- /dev/null +++ b/docker/Dockerfile.jpegxl-builder @@ -0,0 +1,18 @@ +# Copyright (c) the JPEG XL Project +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +# Build an Ubuntu-based docker image with the installed software needed to +# develop and test JPEG XL. + +FROM ubuntu:bionic + +# Set a prompt for when using it locally. +ENV PS1="\[\033[01;33m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ " + +COPY scripts /jpegxl_scripts + +RUN /jpegxl_scripts/jpegxl_builder.sh && \ + rm -rf /jpegxl_scripts diff --git a/docker/Dockerfile.jpegxl-builder-run-aarch64 b/docker/Dockerfile.jpegxl-builder-run-aarch64 new file mode 100644 index 000000000..f904cfbf3 --- /dev/null +++ b/docker/Dockerfile.jpegxl-builder-run-aarch64 @@ -0,0 +1,30 @@ +# Copyright (c) the JPEG XL Project +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +# Build an Ubuntu-based docker image for aarch64 with the installed software +# needed to run JPEG XL. This is only useful when running on actual aarch64 +# hardware. + +FROM arm64v8/ubuntu:bionic + +# Set a prompt for when using it locally. +ENV PS1="\[\033[01;33m\]\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ " + +RUN set -ex; \ + apt-get update -y; \ + apt-get install -y \ + bsdmainutils \ + cmake \ + curl \ + extra-cmake-modules \ + git \ + libjpeg8 \ + libgif7 \ + libpng16-16 \ + libqt5x11extras5 \ + libsdl2-2.0-0 \ + parallel; \ + rm -rf /var/lib/apt/lists/*; diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..874df1cb8 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,7 @@ +### Docker container infrastructure for JPEG XL + +This directory contains the requirements to build a docker image for the +JPEG XL project builder. + +Docker images need to be created and upload manually. See ./build.sh for +details. diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 000000000..d0909957b --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +declare -a TARGETS + +load_targets() { + for f in $(find -maxdepth 1 -name 'Dockerfile.*' | sort); do + local target="${f#*Dockerfile.}" + TARGETS+=("${target}") + done +} + +usage() { + cat >&2 <&2 + done +} + +build_target() { + local target="$1" + + local dockerfile="${MYDIR}/Dockerfile.${target}" + # JPEG XL builder images are stored in the gcr.io/jpegxl project. + local tag="gcr.io/jpegxl/${target}" + + echo "Building ${target}" + if ! sudo docker build -t "${tag}" -f "${dockerfile}" "${MYDIR}" >"${target}.log" 2>&1; then + echo "${target} failed. See ${target}.log" >&2 + else + echo "Done, to upload image run:" >&2 + echo " sudo docker push ${tag}" + if [[ "${JPEGXL_PUSH:-}" == "1" ]]; then + echo "sudo docker push ${tag}" >&2 + sudo docker push "${tag}" + # The RepoDigest is only created after it is pushed. + local fulltag=$(sudo docker inspect --format="{{.RepoDigests}}" "${tag}") + fulltag="${fulltag#[}" + fulltag="${fulltag%]}" + echo "Updating .gitlab-ci.yml to ${fulltag}" >&2 + sed -E "s;${tag}@sha256:[0-9a-f]+;${fulltag};" \ + -i "${MYDIR}/../.gitlab-ci.yml" + fi + fi +} + +main() { + cd "${MYDIR}" + local target="${1:-}" + + load_targets + if [[ -z "${target}" ]]; then + usage $0 + exit 1 + fi + + if [[ "${target}" == "all" ]]; then + for target in "${TARGETS[@]}"; do + build_target "${target}" + done + else + for target in "$@"; do + build_target "${target}" + done + fi +} + +main "$@" diff --git a/docker/scripts/emsdk_install.sh b/docker/scripts/emsdk_install.sh new file mode 100755 index 000000000..667a1ccf0 --- /dev/null +++ b/docker/scripts/emsdk_install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +EMSDK_URL="https://github.com/emscripten-core/emsdk/archive/master.tar.gz" +EMSDK_DIR="/opt/emsdk" + +EMSDK_RELEASE="1.39.4" # Alternatively, we can use "latest" + +set -eu -x + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +main() { + local workdir=$(mktemp -d --suffix=qemu) + CLEANUP_FILES+=("${workdir}") + + local emsdktar="${workdir}/emsdk.tar.gz" + curl --output "${emsdktar}" "${EMSDK_URL}" --location + mkdir -p "${EMSDK_DIR}" + tar -zxf "${emsdktar}" -C "${EMSDK_DIR}" --strip-components=1 + + cd "${EMSDK_DIR}" + ./emsdk install --shallow "${EMSDK_RELEASE}" + ./emsdk activate --embedded "${EMSDK_RELEASE}" +} + +main "$@" diff --git a/docker/scripts/jpegxl_builder.sh b/docker/scripts/jpegxl_builder.sh new file mode 100755 index 000000000..b495d3a82 --- /dev/null +++ b/docker/scripts/jpegxl_builder.sh @@ -0,0 +1,225 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Main entry point for all the Dockerfile for jpegxl-builder. This centralized +# file helps sharing code and configuration between Dockerfiles. + +set -eux + +MYDIR=$(dirname $(realpath "$0")) + +# List of Ubuntu arch names supported by the builder (such as "i386"). +LIST_ARCHS=( + amd64 + i386 + arm64 + armhf +) + +# List of target triplets supported by the builder. +LIST_TARGETS=( + x86_64-linux-gnu + i686-linux-gnu + arm-linux-gnueabihf + aarch64-linux-gnu +) + +# Setup the apt repositories and supported architectures. +setup_apt() { + apt-get update -y + apt-get install -y gnupg + + # gcc ppa sources. + cat >/etc/apt/sources.list.d/gcc.list <>"${newlist}" + fi + + main_list=$(echo "${main_list[@]}" | tr ' ' ,) + grep -v -E '^#' "${bkplist}" | + sed -E "s;^deb (http[^ ]+) (.*)\$;deb [arch=${main_list}] \\1 \\2;" \ + >>"${newlist}" + mv "${newlist}" /etc/apt/sources.list +} + +install_pkgs() { + packages=( + # Native compilers + clang-6.0 clang-format-6.0 clang-tidy-6.0 + + # TODO: Consider adding clang-7 and clang-8 to every builder: + # clang-7 clang-format-7 clang-tidy-7 + # clang-8 clang-format-8 clang-tidy-8 + + # Native tools. + bsdmainutils + cmake + curl + extra-cmake-modules + git + llvm + nasm + ninja-build + parallel + pkg-config + + # These are used by the ./ci.sh lint in the native builder. + clang-format-7 + clang-format-8 + + # For coverage builds + gcovr + + # Common libraries. + libstdc++-8-dev + libgif-dev # See libgif-dev comment below. + ) + + # Install packages that are arch-dependent. + local ubarch + for ubarch in "${LIST_ARCHS[@]}"; do + packages+=( + # Library dependencies. These normally depend on the target architecture + # we are compiling for and can't usually be installed for multiple + # architectures at the same time. + libjpeg-dev:"${ubarch}" + libpng-dev:"${ubarch}" + libqt5x11extras5-dev:"${ubarch}" + + libstdc++-8-dev:"${ubarch}" + qtbase5-dev:"${ubarch}" + + # Cross-compiling tools per arch. + libc6-dev-"${ubarch}"-cross + libstdc++-8-dev-"${ubarch}"-cross + + # libgif-dev package can't be installed for multiple architectures at + # the same time. Instead we unpack it manually later. + # libgif-dev:"${ubarch}" + ) + done + + local target + for target in "${LIST_TARGETS[@]}"; do + # Per target cross-compiling tools. + if [[ "${target}" != "x86_64-linux-gnu" ]]; then + packages+=( + binutils-"${target}" + gcc-"${target}" + ) + fi + done + + apt install -y "${packages[@]}" +} + +# Packages that are manually unpacked for each architecture. +UNPACK_PKGS=( + libgif-dev + libclang-common-6.0-dev +) + +# Main script entry point. +main() { + cd "${MYDIR}" + + # Configure the repositories with the sources for multi-arch cross + # compilation. + setup_apt + apt-get update -y + apt-get dist-upgrade -y + + install_pkgs + apt clean + + # Manually extract packages for the target arch that can't install it directly + # at the same time as the native ones. + local ubarch + for ubarch in "${LIST_ARCHS[@]}"; do + if [[ "${ubarch}" != "amd64" ]]; then + local pkg + for pkg in "${UNPACK_PKGS[@]}"; do + apt download "${pkg}":"${ubarch}" + dpkg -x "${pkg}"_*_"${ubarch}".deb / + done + fi + done + # TODO: Add clang from the llvm repos. This is problematic since we are + # installing libclang-common-6.0-dev:"${ubarch}" from the ubuntu ports repos + # which is not available in the llvm repos so it might have a different + # version than the ubuntu ones. + + # Install webp manually since the version in Ubuntu is rather old. + local target + for target in "${LIST_TARGETS[@]}"; do + if [[ "${target}" == "x86_64-linux-gnu" ]]; then + CC=clang-6.0 CXX=clang++-6.0 ./webp_install.sh + else + CC=clang-6.0 CXX=clang++-6.0 ./webp_install.sh \ + -DCMAKE_INSTALL_PREFIX=/usr/"${target}" \ + -DCMAKE_C_COMPILER_TARGET="${target}" \ + -DCMAKE_CXX_COMPILER_TARGET="${target}" \ + -DCMAKE_SYSTEM_PROCESSOR="${target%%-*}" \ + -DCMAKE_SYSTEM_NAME=Linux + fi + done + + # TODO: Add msan for the target when cross-compiling. This only installs it + # for amd64. + ./msan_install.sh + + # Build and install qemu user-linux targets. + ./qemu_install.sh + + # Install emscripten SDK. + ./emsdk_install.sh + + # Cleanup. + rm -rf /var/lib/apt/lists/* +} + +main "$@" diff --git a/docker/scripts/msan_install.sh b/docker/scripts/msan_install.sh new file mode 100755 index 000000000..23d458aab --- /dev/null +++ b/docker/scripts/msan_install.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +MYDIR=$(dirname $(realpath "$0")) + +# Convenience flag to pass both CMAKE_C_FLAGS and CMAKE_CXX_FLAGS +CMAKE_FLAGS=${CMAKE_FLAGS:-} +CMAKE_C_FLAGS=${CMAKE_C_FLAGS:-${CMAKE_FLAGS}} +CMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS:-${CMAKE_FLAGS}} +CMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS:-} + +CLANG_VERSION="${CLANG_VERSION:-}" +# Detect the clang version suffix and store it in CLANG_VERSION. For example, +# "6.0" for clang 6 or "7" for clang 7. +detect_clang_version() { + if [[ -n "${CLANG_VERSION}" ]]; then + return 0 + fi + local clang_version=$("${CC:-clang}" --version | head -n1) + local llvm_tag + case "${clang_version}" in + "clang version 6."*) + CLANG_VERSION="6.0" + ;; + "clang version 7."*) + CLANG_VERSION="7" + ;; + "clang version 8."*) + CLANG_VERSION="8" + ;; + "clang version 9."*) + CLANG_VERSION="9" + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac +} + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +# Install libc++ libraries compiled with msan in the msan_prefix for the current +# compiler version. +cmd_msan_install() { + local tmpdir=$(mktemp -d) + CLEANUP_FILES+=("${tmpdir}") + # Detect the llvm to install: + export CC="${CC:-clang}" + export CXX="${CXX:-clang++}" + detect_clang_version + local llvm_tag + case "${CLANG_VERSION}" in + "6.0") + llvm_tag="llvmorg-6.0.1" + ;; + "7") + llvm_tag="llvmorg-7.0.1" + ;; + "8") + llvm_tag="llvmorg-8.0.0" + ;; + *) + echo "Unknown clang version: ${clang_version}" >&2 + return 1 + esac + local llvm_targz="${tmpdir}/${llvm_tag}.tar.gz" + curl -L --show-error -o "${llvm_targz}" \ + "https://github.com/llvm/llvm-project/archive/${llvm_tag}.tar.gz" + tar -C "${tmpdir}" -zxf "${llvm_targz}" + local llvm_root="${tmpdir}/llvm-project-${llvm_tag}" + + local msan_prefix="${HOME}/.msan/${CLANG_VERSION}" + rm -rf "${msan_prefix}" + + declare -A CMAKE_EXTRAS + CMAKE_EXTRAS[libcxx]="\ + -DLIBCXX_CXX_ABI=libstdc++ \ + -DLIBCXX_INSTALL_EXPERIMENTAL_LIBRARY=ON" + + for project in libcxx; do + local proj_build="${tmpdir}/build-${project}" + local proj_dir="${llvm_root}/${project}" + mkdir -p "${proj_build}" + cmake -B"${proj_build}" -H"${proj_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_USE_SANITIZER=Memory \ + -DLLVM_PATH="${llvm_root}/llvm" \ + -DLLVM_CONFIG_PATH="$(which llvm-config llvm-config-7 llvm-config-6.0 | \ + head -n1)" \ + -DCMAKE_CXX_FLAGS="${CMAKE_CXX_FLAGS}" \ + -DCMAKE_C_FLAGS="${CMAKE_C_FLAGS}" \ + -DCMAKE_EXE_LINKER_FLAGS="${CMAKE_EXE_LINKER_FLAGS}" \ + -DCMAKE_INSTALL_PREFIX="${msan_prefix}" \ + ${CMAKE_EXTRAS[${project}]} + cmake --build "${proj_build}" + ninja -C "${proj_build}" install + done +} + +main() { + set -x + for version in 6.0 7 8; do + if ! which "clang-${version}" >/dev/null; then + echo "Skipping msan install for clang version ${version}" + continue + fi + ( + trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + export CLANG_VERSION=${version} + export CC=clang-${version} + export CXX=clang++-${version} + cmd_msan_install + ) & + done + wait +} + +main "$@" diff --git a/docker/scripts/qemu_install.sh b/docker/scripts/qemu_install.sh new file mode 100755 index 000000000..af9d69f20 --- /dev/null +++ b/docker/scripts/qemu_install.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +QEMU_RELEASE="4.1.0" +QEMU_URL="https://download.qemu.org/qemu-${QEMU_RELEASE}.tar.xz" +QEMU_ARCHS=( + aarch64 + arm + i386 + # TODO: Consider adding these: + # aarch64_be + # mips64el + # mips64 + # mips + # ppc64 + # ppc +) + +# Ubuntu packages not installed that are needed to build qemu. +QEMU_BUILD_DEPS=( + libglib2.0-dev + libpixman-1-dev + flex + bison +) + +set -eu -x + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +main() { + local workdir=$(mktemp -d --suffix=qemu) + CLEANUP_FILES+=("${workdir}") + + apt install -y "${QEMU_BUILD_DEPS[@]}" + + local qemutar="${workdir}/qemu.tar.gz" + curl --output "${qemutar}" "${QEMU_URL}" + tar -Jxf "${qemutar}" -C "${workdir}" + local srcdir="${workdir}/qemu-${QEMU_RELEASE}" + + local builddir="${workdir}/build" + local prefixdir="${workdir}/prefix" + mkdir -p "${builddir}" + + # List of targets to build. + local targets="" + local make_targets=() + local target + for target in "${QEMU_ARCHS[@]}"; do + targets="${targets} ${target}-linux-user" + # Build just the linux-user targets. + make_targets+=("${target}-linux-user/all") + done + + cd "${builddir}" + "${srcdir}/configure" \ + --prefix="${prefixdir}" \ + --static --disable-system --enable-linux-user \ + --target-list="${targets}" + + make -j $(nproc --all || echo 1) "${make_targets[@]}" + + # Manually install these into the non-standard location. This script runs as + # root anyway. + for target in "${QEMU_ARCHS[@]}"; do + cp "${target}-linux-user/qemu-${target}" "/usr/bin/qemu-${target}-static" + done + + apt autoremove -y --purge "${QEMU_BUILD_DEPS[@]}" +} + +main "$@" diff --git a/docker/scripts/webp_install.sh b/docker/scripts/webp_install.sh new file mode 100755 index 000000000..46b76d4fc --- /dev/null +++ b/docker/scripts/webp_install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +WEBP_RELEASE="1.0.2" +WEBP_URL="https://codeload.github.com/webmproject/libwebp/tar.gz/v${WEBP_RELEASE}" + +set -eu -x + +# Temporary files cleanup hooks. +CLEANUP_FILES=() +cleanup() { + if [[ ${#CLEANUP_FILES[@]} -ne 0 ]]; then + rm -fr "${CLEANUP_FILES[@]}" + fi +} +trap "{ set +x; } 2>/dev/null; cleanup" INT TERM EXIT + +main() { + local workdir=$(mktemp -d --suffix=webp) + CLEANUP_FILES+=("${workdir}") + + local webptar="${workdir}/webp.tar.gz" + curl --output "${webptar}" "${WEBP_URL}" + tar -zxvf "${webptar}" -C "${workdir}" + local srcdir="${workdir}/libwebp-${WEBP_RELEASE}" + local builddir="${srcdir}/build" + cmake -B"${builddir}" -H"${srcdir}" -G Ninja \ + -DCMAKE_INSTALL_PREFIX=/usr \ + "$@" + cmake --build "${builddir}" + ninja -C "${builddir}" install +} + +main "$@" diff --git a/include/jpegxl/decode.h b/include/jpegxl/decode.h new file mode 100644 index 000000000..2f845699f --- /dev/null +++ b/include/jpegxl/decode.h @@ -0,0 +1,109 @@ +/* Copyright (c) the JPEG XL Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef JPEGXL_DECODE_H_ +#define JPEGXL_DECODE_H_ + +#include +#include + +#include "jpegxl/jpegxl_export.h" +#include "jpegxl/memory_manager.h" + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Decoder library version. + * + * @returns the decoder library version as an integer: + * MAJOR_VERSION * 1000000 + MINOR_VERSION * 1000 + PATCH_VERSION. For example, + * version 1.2.3 would return 1002003. + */ +JPEGXL_EXPORT uint32_t JpegxlDecoderVersion(void); + +enum JpegxlSignature { + /** Not enough bytes were passed to determine if a valid signature was found. + */ + JPEGXL_SIG_NOT_ENOUGH_BYTES = 0, + + /** No valid JPEGXL header was found. */ + JPEGXL_SIG_INVALID = 1, + + /** A valid transcoded JPEG XL image signature was found. */ + JPEGXL_SIG_BRUNSLI = 2, + + /** A valid JPEG XL image signature was found. */ + JPEGXL_SIG_JPEGXL = 4, + + /* Mask representing any valid JPEGXL signature. */ + JPEGXL_SIG_ANY = JPEGXL_SIG_BRUNSLI | JPEGXL_SIG_JPEGXL, +}; + +/** + * JPEG XL signature identification. + * + * Checks if the passed buffer contains a valid JPEG XL signature header and + * returns the type of signature header if found. The passed @p buf of size + * @p size doesn't need to be a full image, only the beginning of the file. + * + * @returns a flag indicating if a JPEG XL signature was found and what type. + * - JPEGXL_SIG_INVALID no valid signature found. + * - JPEGXL_SIG_BRUNSLI a valid Brunsli signature was found. + * - JPEGXL_SIG_JPEGXL a valid JPEG XL signature was found. + * - JPEGXL_SIG_NOT_ENOUGH_BYTES not enough bytes were passed to determine + * if a valid signature is there. + * You can mask the result of this function with JPEGXL_SIG_ANY to determine + * if a valid signature (of any kind) was found. + */ +JPEGXL_EXPORT enum JpegxlSignature JpegxlSignatureCheck(const uint8_t* buf, + size_t len); + +/** + * Opaque structure that holds the JPEGXL decoder. + * + * Allocated and initialized with JpegxlDecoderCreate(). + * Cleaned up and deallocated with JpegxlDecoderDestroy(). + */ +typedef struct JpegxlDecoderStruct JpegxlDecoder; + +/** + * Creates an instance of JpegxlDecoder and initializes it. + * + * @p memory_manager will be used for all the library dynamic allocations made + * from this instance. The parameter may be NULL, in which case the default + * allocator will be used. See jpegxl/memory_manager.h for details. + * + * @param memory_manager custom allocator function. It may be NULL. The memory + * manager will be copied internally. + * @returns @c NULL if the instance can not be allocated or initialized + * @returns pointer to initialized JpegxlDecoder otherwise + */ +JPEGXL_EXPORT JpegxlDecoder* JpegxlDecoderCreate( + const JpegxlMemoryManager* memory_manager); + +/** + * Deinitializes and frees JpegxlDecoder instance. + * + * @param dec instance to be cleaned up and deallocated. + */ +JPEGXL_EXPORT void JpegxlDecoderDestroy(JpegxlDecoder* dec); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JPEGXL_DECODE_H_ */ diff --git a/include/jpegxl/memory_manager.h b/include/jpegxl/memory_manager.h new file mode 100644 index 000000000..13ee2019c --- /dev/null +++ b/include/jpegxl/memory_manager.h @@ -0,0 +1,71 @@ +/* Copyright (c) the JPEG XL Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef JPEGXL_MEMORY_MANAGER_H_ +#define JPEGXL_MEMORY_MANAGER_H_ + +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** + * Allocating function for a memory region of a given size. + * + * Allocates a contiguous memory region of size @p size bytes. The returned + * memory may not be aligned to a specific size or initialized at all. + * + * @param opaque custom memory manager handle provided by the caller. + * @param size in bytes of the requested memory region. + * @returns @c 0 if the memory can not be allocated, + * @returns pointer to the memory otherwise. + */ +typedef void* (*jpegxl_alloc_func)(void* opaque, size_t size); + +/** + * Deallocating function pointer type. + * + * This function @b MUST do nothing if @p address is @c 0. + * + * @param opaque custom memory manager handle provided by the caller. + * @param address memory region pointer returned by ::brotli_alloc_func, or @c 0 + */ +typedef void (*jpegxl_free_func)(void* opaque, void* address); + +/** + * Memory Manager struct. + * These functions, when provided by the caller, will be used to handle memory + * allocations. + */ +typedef struct JpegxlMemoryManagerStruct { + /* The opaque pointer that will be passed as the first parameter to all the + * functions in this struct. */ + void* opaque; + + /* Alloc/free functions. These can either be both NULL or none of them NULL. + * All dynamic memory will be allocated and freed with these functions if + * not NULL. */ + jpegxl_alloc_func alloc; + jpegxl_free_func free; + + /* TODO(deymo): Add cache-aligned alloc/free functions here. */ +} JpegxlMemoryManager; + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JPEGXL_MEMORY_MANAGER_H_ */ diff --git a/include/jpegxl/parallel_runner.h b/include/jpegxl/parallel_runner.h new file mode 100644 index 000000000..835bbe195 --- /dev/null +++ b/include/jpegxl/parallel_runner.h @@ -0,0 +1,152 @@ +/* Copyright (c) the JPEG XL Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** API for running data operations in parallel in a multi-threaded environment. + * This module allows the JPEG XL caller to define their own way of creating and + * assigning threads. + * + * The JpegxlParallelRunner function type defines a parallel data processing + * runner that may be implemented by the caller to allow the library to process + * in multiple threads. The multi-threaded processing in this library only + * requires to run the same function over each number of a range, possibly + * running each call in a different thread. The JPEG XL caller is responsible + * for implementing this logic using the thread APIs available in their system. + * For convenience, a C++ implementation based on std::thread is provided in + * jpegxl/parallel_runner_thread.h (part of the jpegxl_threads library). + * + * Thread pools usually store small numbers of heterogeneous tasks in a queue. + * When tasks are identical or differ only by an integer input parameter, it is + * much faster to store just one function of an integer parameter and call it + * for each value. Conventional vector-of-tasks can be run in parallel using a + * lambda function adapter that simply calls task_funcs[task]. + * + * If no multi-threading is desired, a @c NULL value of JpegxlParallelRunner + * will use an internal implementation without multi-threading. + */ + +#ifndef JPEGXL_PARALLEL_RUNNER_H_ +#define JPEGXL_PARALLEL_RUNNER_H_ + +#include +#include + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/** Return code used in the JpegxlParallel* functions as return value. A value + * of 0 means success and any other value means error. The special value + * JPEGXL_PARALLEL_RET_RUNNER_ERROR can be used by the runner to indicate any + * other error. + */ +typedef int JpegxlParallelRetCode; + +#define JPEGXL_PARALLEL_RET_RUNNER_ERROR (-1) + +/** + * Parallel run initialization callback. See JpegxlParallelRunner for details. + * + * This function MUST be called by the JpegxlParallelRunner only once, on the + * same thread that called JpegxlParallelRunner, before any parallel execution. + * The purpose of this call is to provide the maximum number of threads that the + * JpegxlParallelRunner will use, which can be used by JPEG XL to allocate + * per-thread storage if needed. + * + * @param jpegxl_opaque the @p jpegxl_opaque handle provided to + * JpegxlParallelRunner() must be passed here. + * @param num_threads the maximum number of threads. This value must be + * positive. + * @returns 0 if the initialization process was successful. + * @returns an error code if there was an error, which should be returned by + * JpegxlParallelRunner(). + */ +typedef JpegxlParallelRetCode (*JpegxlParallelRunInit)(void* jpegxl_opaque, + size_t num_threads); + +/** + * Parallel run data processing callback. See JpegxlParallelRunner for details. + * + * This function MUST be called once for every number in the range [start_range, + * end_range) (including start_range but not including end_range) passing this + * number as the @p value. Calls for different value may be executed from + * different threads in parallel. + * + * @param jpegxl_opaque the @p jpegxl_opaque handle provided to + * JpegxlParallelRunner() must be passed here. + * @param value the number in the range [start_range, end_range) of the call. + * @param thread_id the thread number where this function is being called from. + * This must be lower than the @p num_threads value passed to + * JpegxlParallelRunInit. + */ +typedef void (*JpegxlParallelRunFunction)(void* jpegxl_opaque, uint32_t value, + size_t thread_id); + +/** + * JpegxlParallelRunner function type. A parallel runner implementation can be + * provided by a JPEG XL caller to allow running computations in multiple + * threads. This function must call the initialization function @p init in the + * same thread that called it and then call the passed @p func once for every + * number in the range [start_range, end_range) (including start_range but not + * including end_range) possibly from different multiple threads in parallel. + * + * The JpegxlParallelRunner function does not need to be re-entrant. This means + * that the same JpegxlParallelRunner function with the same runner_opaque + * provided parameter will not be called from the library from either @p init or + * @p func in the same decoder or encoder instance. However, a single decoding + * or encoding instance may call the provided JpegxlParallelRunner multiple + * times for different parts of the decoding or encoding process. + * + * @returns 0 if the @p init call succeeded (returned 0) and no other error + * occurred in the runner code. + * @returns JPEGXL_PARALLEL_RET_RUNNER_ERROR if an error occurred in the runner + * code, for example, setting up the threads. + * @return the return value of @p init() if non-zero. + */ +typedef JpegxlParallelRetCode (*JpegxlParallelRunner)( + void* runner_opaque, void* jpegxl_opaque, JpegxlParallelRunInit init, + JpegxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + +/* The following is an example of a JpegxlParallelRunner that doesn't use any + * multi-threading. Note that this implementation doesn't store any state + * between multiple calls of the ExampleSequentialRunner function, so the + * runner_opaque value is not used. + + JpegxlParallelRetCode ExampleSequentialRunner(void* runner_opaque, + void* jpegxl_opaque, + JpegxlParallelRunInit init, + JpegxlParallelRunFunction func, + uint32_t start_range, + uint32_t end_range) { + // We only use one thread (the currently running thread). + JpegxlParallelRetCode init_ret = (*init)(jpegxl_opaque, 1); + if (init_ret != 0) return init_ret; + + // In case of other initialization error (for example when initializing the + // threads) one can return JPEGXL_PARALLEL_RET_RUNNER_ERROR. + + for (uint32_t i = start_range; i < end_range; i++) { + // Every call is in the thread number 0. These don't need to be in any + // order. + (*func)(jpegxl_opaque, i, 0); + } + return 0; + } + */ + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* JPEGXL_PARALLEL_RUNNER_H_ */ diff --git a/include/jpegxl/thread_parallel_runner.h b/include/jpegxl/thread_parallel_runner.h new file mode 100644 index 000000000..fee840a22 --- /dev/null +++ b/include/jpegxl/thread_parallel_runner.h @@ -0,0 +1,179 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Implementation of a JpegxlParallelRunner using std::threads. +// +// The main class in this module, ThreadParallelRunner, implements a static +// method ThreadParallelRunner::Runner than can be passed as a +// JpegxlParallelRunner when using the JPEG XL library. This uses std::thread +// internally and related synchronization functions. The number of threads +// created is fixed at construction time and the threads are re-used for every +// ThreadParallelRunner::Runner call. Only one concurrent Runner() call per +// instance is allowed at a time. +// +// This is a scalable, lower-overhead thread pool runner, especially suitable +// for data-parallel computations in the fork-join model, where clients need to +// know when all tasks have completed. +// +// This thread pool can efficiently load-balance millions of tasks using an +// atomic counter, thus avoiding per-task virtual or system calls. With 48 +// hyperthreads and 1M tasks that add to an atomic counter, overall runtime is +// 10-20x higher when using std::async, and ~200x for a queue-based thread pool. +// +// Usage: +// ThreadParallelRunner runner; +// JpegxlDecode( +// ... , &ThreadParallelRunner::Runner, static_cast(&runner)); + +#ifndef JPEGXL_THREAD_PARALLEL_RUNNER_H_ +#define JPEGXL_THREAD_PARALLEL_RUNNER_H_ + +#include +#include +#include +#include + +#include // max +#include +#include //NOLINT +#include //NOLINT +#include //NOLINT +#include + +#include "jpegxl/parallel_runner.h" + +namespace jpegxl { + +class ThreadParallelRunner { + public: + // JpegxlParallelRunner interface. + static JpegxlParallelRetCode Runner(void* runner_opaque, void* jpegxl_opaque, + JpegxlParallelRunInit init, + JpegxlParallelRunFunction func, + uint32_t start_range, uint32_t end_range); + + // Starts the given number of worker threads and blocks until they are ready. + // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks + // run on the main thread. + explicit ThreadParallelRunner( + int num_worker_threads = std::thread::hardware_concurrency()); + + // Waits for all threads to exit. + ~ThreadParallelRunner(); + + // Returns number of worker threads created (some may be sleeping and never + // wake up in time to participate in Run). Useful for characterizing + // performance; 0 means "run on main thread". + size_t NumWorkerThreads() const { return num_worker_threads_; } + + // Returns maximum number of main/worker threads that may call Func. Useful + // for allocating per-thread storage. + size_t NumThreads() const { return num_threads_; } + + // Runs func(thread, thread) on all thread(s) that may participate in Run. + // If NumThreads() == 0, runs on the main thread with thread == 0, otherwise + // concurrently called by each worker thread in [0, NumThreads()). + template + void RunOnEachThread(const Func& func) { + if (num_worker_threads_ == 0) { + const int thread = 0; + func(thread, thread); + return; + } + + data_func_ = + reinterpret_cast(&CallClosure); + jpegxl_opaque_ = const_cast(static_cast(&func)); + StartWorkers(kWorkerOnce); + WorkersReadyBarrier(); + } + + private: + // After construction and between calls to Run, workers are "ready", i.e. + // waiting on worker_start_cv_. They are "started" by sending a "command" + // and notifying all worker_start_cv_ waiters. (That is why all workers + // must be ready/waiting - otherwise, the notification will not reach all of + // them and the main thread waits in vain for them to report readiness.) + using WorkerCommand = uint64_t; + + // Special values; all others encode the begin/end parameters. Note that all + // these are no-op ranges (begin >= end) and therefore never used to encode + // ranges. + static constexpr WorkerCommand kWorkerWait = ~1ULL; + static constexpr WorkerCommand kWorkerOnce = ~2ULL; + static constexpr WorkerCommand kWorkerExit = ~3ULL; + + // Calls f(task, thread). Used for type erasure of Func arguments. The + // signature must match JpegxlParallelRunFunction, hence a void* argument. + template + static void CallClosure(void* f, const uint32_t task, const size_t thread) { + (*reinterpret_cast(f))(task, thread); + } + + void WorkersReadyBarrier() { + std::unique_lock lock(mutex_); + // Typically only a single iteration. + while (workers_ready_ != threads_.size()) { + workers_ready_cv_.wait(lock); + } + workers_ready_ = 0; + + // Safely handle spurious worker wakeups. + worker_start_command_ = kWorkerWait; + } + + // Precondition: all workers are ready. + void StartWorkers(const WorkerCommand worker_command) { + mutex_.lock(); + worker_start_command_ = worker_command; + // Workers will need this lock, so release it before they wake up. + mutex_.unlock(); + worker_start_cv_.notify_all(); + } + + // Attempts to reserve and perform some work from the global range of tasks, + // which is encoded within "command". Returns after all tasks are reserved. + static void RunRange(ThreadParallelRunner* self, const WorkerCommand command, + const int thread); + + static void ThreadFunc(ThreadParallelRunner* self, int thread); + + // Unmodified after ctor, but cannot be const because we call thread::join(). + std::vector threads_; + + const uint32_t num_worker_threads_; // == threads_.size() + const uint32_t num_threads_; + + std::atomic depth_{0}; // detects if Run is re-entered (not supported). + + std::mutex mutex_; // guards both cv and their variables. + std::condition_variable workers_ready_cv_; + uint32_t workers_ready_ = 0; + std::condition_variable worker_start_cv_; + WorkerCommand worker_start_command_; + + // Written by main thread, read by workers (after mutex lock/unlock). + JpegxlParallelRunFunction data_func_; + void* jpegxl_opaque_; + + // Updated by workers; padding avoids false sharing. + uint8_t padding1[64]; + std::atomic num_reserved_{0}; + uint8_t padding2[64]; +}; + +} // namespace jpegxl + +#endif /* JPEGXL_THREAD_PARALLEL_RUNNER_H_ */ diff --git a/jpegxl.cmake b/jpegxl.cmake new file mode 100644 index 000000000..3b3ded4b4 --- /dev/null +++ b/jpegxl.cmake @@ -0,0 +1,438 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(JPEGXL_MAJOR_VERSION 0) +set(JPEGXL_MINOR_VERSION 0) +set(JPEGXL_PATCH_VERSION 1) +set(JPEGXL_LIBRARY_VERSION + "${JPEGXL_MAJOR_VERSION}.${JPEGXL_MINOR_VERSION}.${JPEGXL_PATCH_VERSION}") + +# This the library API compatibility version. +set(JPEGXL_LIBRARY_SOVERSION "${JPEGXL_MAJOR_VERSION}") + +# TODO(deymo): Split this into encoder and decoder targets +set(JPEGXL_INTERNAL_SOURCES + jxl/modular/config.h + jxl/modular/encoding/context_predict.h + jxl/modular/encoding/encoding.cpp + jxl/modular/encoding/encoding.h + jxl/modular/encoding/weighted_predict.h + jxl/modular/image/image.cpp + jxl/modular/image/image.h + jxl/modular/ma/bit.cpp + jxl/modular/ma/bit.h + jxl/modular/ma/chance.cpp + jxl/modular/ma/chance.h + jxl/modular/ma/compound.h + jxl/modular/ma/compound_enc.h + jxl/modular/ma/rac.h + jxl/modular/ma/rac_enc.h + jxl/modular/ma/symbol.h + jxl/modular/ma/symbol_enc.h + jxl/modular/ma/util.h + jxl/modular/memio.h + jxl/modular/transform/near-lossless.h + jxl/modular/transform/palette.h + jxl/modular/transform/quantize.h + jxl/modular/transform/squeeze.h + jxl/modular/transform/subsample.h + jxl/modular/transform/subtractgreen.h + jxl/modular/transform/transform.cpp + jxl/modular/transform/transform.h + jxl/modular/transform/ycocg.h + jxl/modular/util.h + jxl/ac_context.h + jxl/ac_strategy.cc + jxl/ac_strategy.h + jxl/ans_common.cc + jxl/ans_common.h + jxl/ans_params.h + jxl/ar_control_field.cc + jxl/ar_control_field.h + jxl/aux_out.cc + jxl/aux_out.h + jxl/aux_out.h + jxl/aux_out_fwd.h + jxl/base/arch_specific.cc + jxl/base/arch_specific.h + jxl/base/bits.h + jxl/base/byte_order.h + jxl/base/cache_aligned.cc + jxl/base/cache_aligned.h + jxl/base/compiler_specific.h + jxl/base/data_parallel.cc + jxl/base/data_parallel.h + jxl/base/descriptive_statistics.cc + jxl/base/descriptive_statistics.h + jxl/base/fast_log.h + jxl/base/file_io.h + jxl/base/iaca.h + jxl/base/os_specific.cc + jxl/base/os_specific.h + jxl/base/override.h + jxl/base/padded_bytes.cc + jxl/base/padded_bytes.h + jxl/base/profiler.h + jxl/base/robust_statistics.h + jxl/base/span.h + jxl/base/status.cc + jxl/base/status.h + jxl/base/tsc_timer.h + jxl/block.h + jxl/brotli.cc + jxl/brotli.h + jxl/brunsli.cc + jxl/brunsli.h + jxl/butteraugli/butteraugli.cc + jxl/butteraugli/butteraugli.h + jxl/chroma_from_luma.cc + jxl/chroma_from_luma.h + jxl/coeff_order.cc + jxl/coeff_order.h + jxl/coeff_order_fwd.h + jxl/color_encoding.cc + jxl/color_encoding.h + jxl/color_management.cc + jxl/color_management.h + jxl/common.h + jxl/compressed_dc.cc + jxl/compressed_dc.h + jxl/convolve.h + jxl/dct.cc + jxl/dct.h + jxl/dct_simd_4.h + jxl/dct_simd_8.h + jxl/dct_simd_any.h + jxl/dct_util.cc + jxl/dct_util.h + jxl/dec_ans.cc + jxl/dec_ans.h + jxl/dec_bit_reader.h + jxl/dec_cache.h + jxl/dec_context_map.cc + jxl/dec_context_map.h + jxl/dec_file.cc + jxl/dec_file.h + jxl/dec_frame.cc + jxl/dec_frame.h + jxl/dec_group.cc + jxl/dec_group.h + jxl/dec_modular.cc + jxl/dec_modular.h + jxl/dec_params.h + jxl/dec_reconstruct.cc + jxl/dec_reconstruct.h + jxl/dec_xyb.cc + jxl/dec_xyb.h + jxl/detect_dots.cc + jxl/detect_dots.h + jxl/dot_dictionary.cc + jxl/dot_dictionary.h + jxl/enc_ac_strategy.cc + jxl/enc_ac_strategy.h + jxl/enc_adaptive_quantization.cc + jxl/enc_adaptive_quantization.h + jxl/enc_ans.cc + jxl/enc_ans.h + jxl/enc_bit_writer.cc + jxl/enc_bit_writer.h + jxl/enc_butteraugli_comparator.cc + jxl/enc_butteraugli_comparator.h + jxl/enc_cache.cc + jxl/enc_cache.h + jxl/enc_cluster.cc + jxl/enc_cluster.h + jxl/enc_comparator.cc + jxl/enc_comparator.h + jxl/enc_context_map.cc + jxl/enc_context_map.h + jxl/enc_file.cc + jxl/enc_file.h + jxl/enc_frame.cc + jxl/enc_frame.h + jxl/enc_gamma_correct.h + jxl/enc_group.cc + jxl/enc_group.h + jxl/enc_modular.cc + jxl/enc_modular.h + jxl/enc_params.h + jxl/enc_xyb.cc + jxl/enc_xyb.h + jxl/entropy_coder.cc + jxl/entropy_coder.h + jxl/epf.cc + jxl/epf.h + jxl/external_image.cc + jxl/external_image.h + jxl/field_encodings.h + jxl/fields.cc + jxl/fields.h + jxl/frame_header.cc + jxl/frame_header.h + jxl/gaborish.cc + jxl/gaborish.h + jxl/gauss_blur.cc + jxl/gauss_blur.h + jxl/headers.cc + jxl/headers.h + jxl/icc_codec.cc + jxl/icc_codec.h + jxl/image.cc + jxl/image.h + jxl/image_bundle.cc + jxl/image_bundle.h + jxl/image_ops.h + jxl/jpegxl/decode.cc + jxl/jxl_inspection.h + jxl/lehmer_code.h + jxl/linalg.cc + jxl/linalg.h + jxl/loop_filter.cc + jxl/loop_filter.h + jxl/memory_manager_internal.cc + jxl/memory_manager_internal.h + jxl/multiframe.cc + jxl/multiframe.h + jxl/noise.cc + jxl/noise.h + jxl/noise_distributions.h + jxl/opsin_params.cc + jxl/opsin_params.h + jxl/optimize.cc + jxl/optimize.h + jxl/passes_state.cc + jxl/passes_state.h + jxl/patch_dictionary.cc + jxl/patch_dictionary.h + jxl/predictor.cc + jxl/predictor.h + jxl/quant_weights.cc + jxl/quant_weights.h + jxl/quantizer.cc + jxl/quantizer.h + jxl/rational_polynomial.h + jxl/splines.cc + jxl/splines.h + jxl/splines_fastmath.h + jxl/toc.cc + jxl/toc.h + jxl/xorshift128plus.h +) + +set(JPEGXL_INTERNAL_FLAGS + # F_FLAGS + -fmerge-all-constants + -fno-builtin-fwrite + -fno-builtin-fread + + # WARN_FLAGS + -Wall + -Wextra + -Wc++11-compat + -Wc++2a-extensions + -Wdeprecated-increment-bool + -Wfloat-overflow-conversion + -Wfloat-zero-conversion + -Wfor-loop-analysis + -Wformat-security + -Wgnu-redeclared-enum + -Wimplicit-fallthrough + -Winfinite-recursion + -Wliteral-conversion + -Wno-c++98-compat + -Wno-register # Needed by public headers in lcms + -Wno-unused-command-line-argument + -Wno-sign-compare + -Wno-unused-function + -Wno-unused-parameter + -Wnon-virtual-dtor + -Woverloaded-virtual + -Wprivate-header + -Wself-assign + -Wstring-conversion + -Wtautological-overlap-compare + -Wthread-safety-analysis + -Wundefined-func-template + -Wunused-comparison + -Wvla +) + +if (WIN32) +list(APPEND JPEGXL_INTERNAL_FLAGS + -Wno-c++98-compat-pedantic + -Wno-cast-align + -Wno-double-promotion + -Wno-float-equal + -Wno-format-nonliteral + -Wno-global-constructors + -Wno-language-extension-token + -Wno-missing-prototypes + -Wno-shadow + -Wno-shadow-field-in-constructor + -Wno-sign-conversion + -Wno-unused-member-function + -Wno-unused-template + -Wno-used-but-marked-unused + -Wno-zero-as-null-pointer-constant +) +else() +list(APPEND JPEGXL_INTERNAL_FLAGS + -fno-signed-char + -fsized-deallocation + -fnew-alignment=8 + -fno-cxx-exceptions + -fno-exceptions + -fno-slp-vectorize + -fno-vectorize + + # Language flags + -disable-free + -disable-llvm-verifier + -fmath-errno +) +endif () + +set(JPEGXL_INTERNAL_LIBS + brotlicommon-static + brotlienc-static + brotlidec-static + brunslicommon-static + brunslidec-static + brunslienc-static + hwy + Threads::Threads + ${CMAKE_DL_LIBS} +) +set(JPEGXL_LIBRARY_REQUIRES + "libbrotlicommon libbrotlienc libbrotlidec") + +if (JPEGXL_ENABLE_SKCMS) + list(APPEND JPEGXL_INTERNAL_FLAGS -DJPEGXL_ENABLE_SKCMS=1) + list(APPEND JPEGXL_INTERNAL_LIBS skcms) +else () + list(APPEND JPEGXL_INTERNAL_LIBS lcms2) +endif () + +if(JPEGXL_ENABLE_COVERAGE) +set(JPEGXL_COVERAGE_FLAGS + -g -O0 -fprofile-arcs -ftest-coverage -DJXL_DISABLE_SLOW_TESTS + -DJXL_ENABLE_ASSERT=0 +) +endif() + +# Object library. This is used to hold the set of objects and properties. +add_library(jpegxl-obj OBJECT ${JPEGXL_INTERNAL_SOURCES}) +target_compile_options(jpegxl-obj PRIVATE ${JPEGXL_INTERNAL_FLAGS}) +target_compile_options(jpegxl-obj PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +if (NOT "${JPEGXL_EMSCRIPTEN}") +set_property(TARGET jpegxl-obj PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() +target_include_directories(jpegxl-obj PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + $ + $ + $ +) +target_compile_definitions(jpegxl-obj PUBLIC + JPEGXL_MAJOR_VERSION=${JPEGXL_MAJOR_VERSION} + JPEGXL_MINOR_VERSION=${JPEGXL_MINOR_VERSION} + JPEGXL_PATCH_VERSION=${JPEGXL_PATCH_VERSION} + # Used to determine if we are building the library when defined or just + # including the library when not defined. This is public so libjpegxl shared + # library gets this define too. + JPEGXL_INTERNAL_LIBRARY_BUILD +) + +if (JPEGXL_ENABLE_SKCMS) + target_include_directories(jpegxl-obj PRIVATE + $ + ) +else () + target_include_directories(jpegxl-obj PRIVATE + $ + ) +endif () + +# Headers for exporting/importing public headers +include(GenerateExportHeader) +# TODO(deymo): Add these visibility properties to the static dependencies of +# jpegxl-obj since those are currently compiled with the default visibility. +set_target_properties(jpegxl-obj PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 + DEFINE_SYMBOL JPEGXL_INTERNAL_LIBRARY_BUILD +) +generate_export_header(jpegxl-obj + BASE_NAME JPEGXL + EXPORT_FILE_NAME include/jpegxl/jpegxl_export.h) +target_include_directories(jpegxl-obj PUBLIC + ${CMAKE_BINARY_DIR}/include) + +# Private static library. This exposes all the internal functions and is used +# for tests. +add_library(jpegxl-static STATIC $) +target_link_libraries(jpegxl-static + PUBLIC ${JPEGXL_COVERAGE_FLAGS} ${JPEGXL_INTERNAL_LIBS} hwy) +target_include_directories(jpegxl-static PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_BINARY_DIR}/include") + +# gperftools (TCMalloc) does not compile on OSX. +# TODO(deymo): Move TCMalloc linkage to the tools/ directory since the library +# shouldn't do any allocs anyway. +if(${JPEGXL_ENABLE_TCMALLOC}) + target_link_libraries(jpegxl-static PUBLIC gperftools) +endif() # JPEGXL_ENABLE_TCMALLOC + +if (NOT "${JPEGXL_EMSCRIPTEN}") + +# Public shared library. +add_library(jpegxl SHARED $) +target_link_libraries(jpegxl PUBLIC ${JPEGXL_COVERAGE_FLAGS}) +target_link_libraries(jpegxl PRIVATE ${JPEGXL_INTERNAL_LIBS}) +target_include_directories(jpegxl PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_BINARY_DIR}/include") +set_target_properties(jpegxl PROPERTIES + VERSION ${JPEGXL_LIBRARY_VERSION} + SOVERSION ${JPEGXL_LIBRARY_SOVERSION}) + +# Add a jpegxl.version file as a version script to tag symbols with the +# appropriate version number. This script is also used to limit what's exposed +# in the shared library from the static dependencies bundled here. +set_target_properties(jpegxl PROPERTIES LINK_DEPENDS ${CMAKE_SOURCE_DIR}/jxl/jpegxl.version) +if(APPLE) +set_property(TARGET jpegxl APPEND_STRING PROPERTY + LINK_FLAGS "-Wl,-exported_symbols_list,${CMAKE_SOURCE_DIR}/jxl/jpegxl_osx.syms") +elseif(WIN32) + # Nothing needed here, we use __declspec(dllexport) (jpegxl_export.h) +else() +set_property(TARGET jpegxl APPEND_STRING PROPERTY + LINK_FLAGS " -Wl,--version-script=${CMAKE_SOURCE_DIR}/jxl/jpegxl.version") +endif() # APPLE +install(TARGETS jpegxl + DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/jpegxl + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/jpegxl") +install(DIRECTORY ${CMAKE_BINARY_DIR}/include/jpegxl + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/jpegxl") + +# Add a pkg-config file for libjpegxl. +configure_file("${CMAKE_SOURCE_DIR}/jxl/libjpegxl.pc.in" "libjpegxl.pc" @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libjpegxl.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") + +endif() # JPEGXL_EMSCRIPTEN diff --git a/jpegxl_extras.cmake b/jpegxl_extras.cmake new file mode 100644 index 000000000..c2e6b1786 --- /dev/null +++ b/jpegxl_extras.cmake @@ -0,0 +1,86 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(JPEGXL_EXTRAS_SOURCES + jxl/extras/codec.cc + jxl/extras/codec.h + jxl/extras/codec_apng.cc + jxl/extras/codec_apng.h + jxl/extras/codec_gif.cc + jxl/extras/codec_gif.h + jxl/extras/codec_jpg.cc + jxl/extras/codec_jpg.h + jxl/extras/codec_pgx.cc + jxl/extras/codec_pgx.h + jxl/extras/codec_png.cc + jxl/extras/codec_png.h + jxl/extras/codec_pnm.cc + jxl/extras/codec_pnm.h +) + +find_package(GIF REQUIRED 5) +find_package(JPEG REQUIRED) +find_package(ZLIB REQUIRED) # dependency of PNG +find_package(PNG REQUIRED) +find_package(PkgConfig) +pkg_check_modules(OpenEXR IMPORTED_TARGET OpenEXR) + +foreach (lib gif jpeg png zlib) + string(TOUPPER "${lib}" LIB) + find_library(${LIB}_STATIC_LIBRARY + NAMES lib${lib}.a + HINTS + ENV ${LIB}_DIR + PATH_SUFFIXES lib + ) + if(NOT "${${LIB}_STATIC_LIBRARY}" STREQUAL "${LIB}_STATIC_LIBRARY-NOTFOUND") + list(APPEND JPEGXL_STATIC_EXTRAS_LIBS "${${LIB}_STATIC_LIBRARY}") + else() + message("Warning: lib${lib}.a not found, using ${${LIB}_LIBRARIES}") + list(APPEND JPEGXL_STATIC_EXTRAS_LIBS "${${LIB}_LIBRARIES}") + endif() +endforeach() + +# We only define a static library for jpegxl_extras since it uses internal parts +# of jpegxl library which are not accessible from outside the library in the +# shared library case. +add_library(jpegxl_extras-static STATIC "${JPEGXL_EXTRAS_SOURCES}") +target_compile_options(jpegxl_extras-static PRIVATE "${JPEGXL_INTERNAL_FLAGS}") +set_property(TARGET jpegxl_extras-static PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jpegxl_extras-static PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${GIF_INCLUDE_DIRS}" + "${JPEG_INCLUDE_DIRS}" + "${PNG_INCLUDE_DIRS}" +) + +target_link_libraries(jpegxl_extras-static PUBLIC + jpegxl-static + lodepng + ${JPEGXL_STATIC_EXTRAS_LIBS} +) + +if (JPEGXL_ENABLE_SJPEG) + target_compile_definitions(jpegxl_extras-static PUBLIC -DJPEGXL_ENABLE_SJPEG=1) + target_link_libraries(jpegxl_extras-static PUBLIC sjpeg) +endif () + +if (OpenEXR_FOUND) + target_sources(jpegxl_extras-static PRIVATE + jxl/extras/codec_exr.cc + jxl/extras/codec_exr.h + ) + target_compile_definitions(jpegxl_extras-static PRIVATE -DJPEGXL_ENABLE_EXR=1) + target_link_libraries(jpegxl_extras-static PUBLIC PkgConfig::OpenEXR) +endif () diff --git a/jpegxl_threads.cmake b/jpegxl_threads.cmake new file mode 100644 index 000000000..ff13cbc34 --- /dev/null +++ b/jpegxl_threads.cmake @@ -0,0 +1,36 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(JPEGXL_THREADS_SOURCES + jxl/threads/thread_parallel_runner.cc +) + +find_package(Threads REQUIRED) + +# We only provide a static jpegxl_threads library since it is a non-stable C++ +# API and very small. It is likely better to bundle it in the programs that +# need it. +add_library(jpegxl_threads STATIC "${JPEGXL_THREADS_SOURCES}") +target_compile_options(jpegxl_threads PRIVATE "${JPEGXL_INTERNAL_FLAGS}") +set_property(TARGET jpegxl_threads PROPERTY POSITION_INDEPENDENT_CODE ON) +target_include_directories(jpegxl_threads PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(jpegxl_threads + PUBLIC + Threads::Threads + PRIVATE + #TODO(deymo): Change this to use the shared library. Right now it causes + # problems when including both the shared and static library. + jpegxl-static +) diff --git a/jpegxltests.cmake b/jpegxltests.cmake new file mode 100644 index 000000000..9496e7199 --- /dev/null +++ b/jpegxltests.cmake @@ -0,0 +1,105 @@ +# Copyright (c) the JPEG XL Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set(TEST_FILES + jxl/ac_strategy_test.cc + jxl/adaptive_reconstruction_test.cc + jxl/ans_common_test.cc + jxl/ans_test.cc + jxl/bit_reader_test.cc + jxl/bits_test.cc + jxl/brotli_test.cc + jxl/brunsli_test.cc + jxl/byte_order_test.cc + jxl/coeff_order_test.cc + jxl/color_encoding_test.cc + jxl/color_management_test.cc + jxl/compressed_dc_test.cc + jxl/compressed_image_test.cc + jxl/convolve_test.cc + jxl/data_parallel_test.cc + jxl/dct_test.cc + jxl/dct_util_test.cc + jxl/descriptive_statistics_test.cc + jxl/entropy_coder_test.cc + jxl/external_image_test.cc + jxl/extras/codec_test.cc + jxl/fields_test.cc + jxl/gaborish_test.cc + jxl/gamma_correct_test.cc + jxl/gradient_test.cc + jxl/iaca_test.cc + jxl/icc_codec_test.cc + jxl/image_test.cc + jxl/jpeg_kpasses_test.cc + jxl/jpegxl/decode_test.cc + jxl/jxl_test.cc + jxl/lehmer_code_test.cc + jxl/linalg_test.cc + jxl/opsin_image_test.cc + jxl/opsin_inverse_test.cc + jxl/optimize_test.cc + jxl/padded_bytes_test.cc + jxl/passes_test.cc + jxl/predictor_test.cc + jxl/preview_test.cc + jxl/quant_weights_test.cc + jxl/quantizer_test.cc + jxl/rational_polynomial_test.cc + jxl/robust_statistics_test.cc + jxl/speed_tier_test.cc + jxl/splines_test.cc + jxl/threads/thread_parallel_runner_test.cc + jxl/toc_test.cc + jxl/xorshift128plus_test.cc +) + +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests) +foreach (TESTFILE IN LISTS TEST_FILES) + # The TESTNAME is the name without the extension or directory. + get_filename_component(TESTNAME ${TESTFILE} NAME_WE) + add_executable(${TESTNAME} ${TESTFILE}) + target_compile_definitions(${TESTNAME} PRIVATE + -DTEST_DATA_PATH="${CMAKE_CURRENT_SOURCE_DIR}/third_party/testdata") + target_compile_options(${TESTNAME} PRIVATE + ${JPEGXL_INTERNAL_FLAGS} + # Add coverage flags to the test binary so code in the private headers of + # the library is also instrumented when running tests that execute it. + ${JPEGXL_COVERAGE_FLAGS} + ) + target_link_libraries(${TESTNAME} + jpegxl-static + jpegxl_threads + jpegxl_extras-static + gmock + gtest + gtest_main + ) + # Output test targets in the test directory. + set_target_properties(${TESTNAME} PROPERTIES PREFIX "tests/") + if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + set_target_properties(${TESTNAME} PROPERTIES COMPILE_FLAGS "-Wno-error") + endif () + if(${CMAKE_VERSION} VERSION_LESS "3.10.3") + gtest_discover_tests(${TESTNAME} TIMEOUT 30) + else () + gtest_discover_tests(${TESTNAME} DISCOVERY_TIMEOUT 30) + endif () +endforeach () + +# googletest doesn't compile clean with clang-cl (-Wundef) +if (WIN32 AND ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-error") + set_target_properties(gtest_main PROPERTIES COMPILE_FLAGS "-Wno-error") +endif () diff --git a/jxl/ac_context.h b/jxl/ac_context.h new file mode 100644 index 000000000..d9ace3dcf --- /dev/null +++ b/jxl/ac_context.h @@ -0,0 +1,113 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_AC_CONTEXT_H_ +#define JXL_AC_CONTEXT_H_ + +#include "jxl/base/bits.h" +#include "jxl/base/status.h" +#include "jxl/coeff_order_fwd.h" + +namespace jxl { + +// Block context used for scanning order, number of non-zeros, AC coefficients. +// Equal to the channel. +constexpr uint32_t kDCTOrderContextStart = 0; +constexpr uint32_t kOrderContexts = 10; + +// The number of predicted nonzeros goes from 0 to 1008. We use +// ceil(log2(predicted+1)) as a context for the number of nonzeros, so from 0 to +// 10, inclusive. +constexpr uint32_t kNonZeroBuckets = 37; + +static const uint16_t kCoeffFreqContext[64] = { + 0xBAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, + 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, +}; + +static const uint16_t kCoeffNumNonzeroContext[64] = { + 0xBAD, 0, 31, 62, 62, 93, 93, 93, 93, 123, 123, 123, 123, + 152, 152, 152, 152, 152, 152, 152, 152, 180, 180, 180, 180, 180, + 180, 180, 180, 180, 180, 180, 180, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, + 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, +}; + +// Supremum of ZeroDensityContext(x, y) + 1. +constexpr int kZeroDensityContextCount = 458; + +/* This function is used for entropy-sources pre-clustering. + * + * Ideally, each combination of |nonzeros_left| and |k| should go to its own + * bucket; but it implies (64 * 63 / 2) == 2016 buckets. If there is other + * dimension (e.g. block context), then number of primary clusters becomes too + * big. + * + * To solve this problem, |nonzeros_left| and |k| values are clustered. It is + * known that their sum is at most 64, consequently, the total number buckets + * is at most A(64) * B(64). + */ +// TODO(user): investigate, why disabling pre-clustering makes entropy code +// less dense. Perhaps we would need to add HQ clustering algorithm that would +// be able to squeeze better by spending more CPU cycles. +static JXL_INLINE size_t ZeroDensityContext(size_t nonzeros_left, size_t k, + size_t covered_blocks, + size_t log2_covered_blocks, + size_t prev) { + JXL_DASSERT(1 << log2_covered_blocks == covered_blocks); + nonzeros_left = (nonzeros_left + covered_blocks - 1) >> log2_covered_blocks; + k >>= log2_covered_blocks; + JXL_DASSERT(k > 0); + JXL_DASSERT(k < 64); + JXL_DASSERT(nonzeros_left > 0); + JXL_DASSERT(nonzeros_left + k < 65); + return (kCoeffNumNonzeroContext[nonzeros_left] + kCoeffFreqContext[k]) * 2 + + prev; +} + +// Context map for AC coefficients consists of 2 blocks: +// |kOrderContexts x : context for number of non-zeros in the block +// kNonZeroBuckets| computed from block context and predicted value +// (based top and left values) +// |kOrderContexts x : context for AC coefficient symbols, +// kZeroDensityContextCount| computed from block context, +// number of non-zeros left and +// index in scan order +constexpr uint32_t kNumContexts = (kOrderContexts * kNonZeroBuckets) + + (kOrderContexts * kZeroDensityContextCount); + +// Non-zero context is based on number of non-zeros and block context. +// For better clustering, contexts with same number of non-zeros are grouped. +static inline uint32_t NonZeroContext(uint32_t non_zeros, uint32_t block_ctx) { + uint32_t ctx; + if (non_zeros >= 64) non_zeros = 64; + if (non_zeros < 8) + ctx = non_zeros; + else + ctx = 4 + non_zeros / 2; + return ctx * kOrderContexts + block_ctx; +} + +// Non-zero context is based on number of non-zeros and block context. +// For better clustering, contexts with same number of non-zeros are grouped. +constexpr uint32_t ZeroDensityContextsOffset(uint32_t block_ctx) { + return kOrderContexts * kNonZeroBuckets + + kZeroDensityContextCount * block_ctx; +} + +} // namespace jxl + +#endif // JXL_AC_CONTEXT_H_ diff --git a/jxl/ac_strategy.cc b/jxl/ac_strategy.cc new file mode 100644 index 000000000..71c751cfc --- /dev/null +++ b/jxl/ac_strategy.cc @@ -0,0 +1,1540 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/ac_strategy.h" + +#include + +#include +#include +#include // iota +#include +#include + +#include "jxl/base/profiler.h" +#include "jxl/block.h" +#include "jxl/common.h" +#include "jxl/dct.h" +#include "jxl/image_ops.h" + +namespace jxl { +namespace { + +template +struct DoDCT { + template + HWY_ATTR void operator()(const From& from, const To& to) { + ComputeScaledDCT()(from, to); + } +}; + +template +struct DoDCT { + template + HWY_ATTR void operator()(const From& from, const To& to) { + ComputeTransposedScaledDCT()(from, to); + } +}; + +template +struct DoIDCT { + template + HWY_ATTR void operator()(const From& from, const To& to) { + ComputeScaledIDCT()(from, to); + } +}; + +template +struct DoIDCT { + template + HWY_ATTR void operator()(const From& from, const To& to) const { + ComputeTransposedScaledIDCT()(from, to); + } +}; + +// Computes the lowest-frequency LF_ROWSxLF_COLS-sized square in output, which +// is a DCT_ROWS*DCT_COLS-sized DCT block, by doing a ROWS*COLS DCT on the +// input block. +template +HWY_ATTR JXL_INLINE void ReinterpretingDCT(const float* input, + const size_t input_stride, + float* output, + const size_t output_stride) { + static_assert(LF_ROWS == ROWS, + "ReinterpretingDCT should only be called with LF == N"); + static_assert(LF_COLS == COLS, + "ReinterpretingDCT should only be called with LF == N"); + HWY_ALIGN float block[ROWS * COLS] = {}; + for (size_t y = 0; y < ROWS; y++) { + for (size_t x = 0; x < COLS; x++) { + block[y * COLS + x] = input[y * input_stride + x]; + } + } + + constexpr size_t OUT_ROWS = CoefficientRows(ROWS, COLS); + constexpr size_t OUT_COLS = CoefficientColumns(ROWS, COLS); + DoDCT()(FromBlock(block), + ScaleToBlock(block)); + if (ROWS < COLS) { + for (size_t y = 0; y < LF_ROWS; y++) { + for (size_t x = 0; x < LF_COLS; x++) { + output[y * output_stride + x] = + block[y * COLS + x] * DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } else { + for (size_t y = 0; y < LF_COLS; y++) { + for (size_t x = 0; x < LF_ROWS; x++) { + output[y * output_stride + x] = + block[y * ROWS + x] * DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } +} + +// Inverse of ReinterpretingDCT. +template +HWY_ATTR JXL_INLINE void ReinterpretingIDCT(const float* input, + const size_t input_stride, + float* output, + const size_t output_stride) { + HWY_ALIGN float block[ROWS * COLS] = {}; + if (ROWS < COLS) { + for (size_t y = 0; y < LF_ROWS; y++) { + for (size_t x = 0; x < LF_COLS; x++) { + block[y * COLS + x] = input[y * input_stride + x] * + DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } else { + for (size_t y = 0; y < LF_COLS; y++) { + for (size_t x = 0; x < LF_ROWS; x++) { + block[y * ROWS + x] = input[y * input_stride + x] * + DCTTotalResampleScale(y) * + DCTTotalResampleScale(x); + } + } + } + + constexpr size_t IN_ROWS = CoefficientRows(ROWS, COLS); + constexpr size_t IN_COLS = CoefficientColumns(ROWS, COLS); + DoIDCT()(FromBlock(block), + ToBlock(block)); + + for (size_t y = 0; y < ROWS; y++) { + for (size_t x = 0; x < COLS; x++) { + output[y * output_stride + x] = block[y * COLS + x]; + } + } +} + +template +void DCT2TopBlock(const float* block, size_t stride, float* out) { + static_assert(kBlockDim % S == 0, "S should be a divisor of kBlockDim"); + static_assert(S % 2 == 0, "S should be even"); + float temp[kDCTBlockSize]; + constexpr size_t num_2x2 = S / 2; + for (size_t y = 0; y < num_2x2; y++) { + for (size_t x = 0; x < num_2x2; x++) { + float c00 = block[y * 2 * stride + x * 2]; + float c01 = block[y * 2 * stride + x * 2 + 1]; + float c10 = block[(y * 2 + 1) * stride + x * 2]; + float c11 = block[(y * 2 + 1) * stride + x * 2 + 1]; + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + r00 *= 0.25f; + r01 *= 0.25f; + r10 *= 0.25f; + r11 *= 0.25f; + temp[y * kBlockDim + x] = r00; + temp[y * kBlockDim + num_2x2 + x] = r01; + temp[(y + num_2x2) * kBlockDim + x] = r10; + temp[(y + num_2x2) * kBlockDim + num_2x2 + x] = r11; + } + } + for (size_t y = 0; y < S; y++) { + for (size_t x = 0; x < S; x++) { + out[y * kBlockDim + x] = temp[y * kBlockDim + x]; + } + } +} + +template +void IDCT2TopBlock(const float* block, size_t stride_out, float* out) { + static_assert(kBlockDim % S == 0, "S should be a divisor of kBlockDim"); + static_assert(S % 2 == 0, "S should be even"); + float temp[kDCTBlockSize]; + constexpr size_t num_2x2 = S / 2; + for (size_t y = 0; y < num_2x2; y++) { + for (size_t x = 0; x < num_2x2; x++) { + float c00 = block[y * kBlockDim + x]; + float c01 = block[y * kBlockDim + num_2x2 + x]; + float c10 = block[(y + num_2x2) * kBlockDim + x]; + float c11 = block[(y + num_2x2) * kBlockDim + num_2x2 + x]; + float r00 = c00 + c01 + c10 + c11; + float r01 = c00 + c01 - c10 - c11; + float r10 = c00 - c01 + c10 - c11; + float r11 = c00 - c01 - c10 + c11; + temp[y * 2 * kBlockDim + x * 2] = r00; + temp[y * 2 * kBlockDim + x * 2 + 1] = r01; + temp[(y * 2 + 1) * kBlockDim + x * 2] = r10; + temp[(y * 2 + 1) * kBlockDim + x * 2 + 1] = r11; + } + } + for (size_t y = 0; y < S; y++) { + for (size_t x = 0; x < S; x++) { + out[y * stride_out + x] = temp[y * kBlockDim + x]; + } + } +} + +// Tries to generalize zig-zag order to non-square blocks. Surprisingly, in +// square block frequency along the (i + j == const) diagonals is roughly the +// same. For historical reasons, consecutive diagonals are traversed +// in alternating directions - so called "zig-zag" (or "snake") order. +static AcStrategy::CoeffOrderAndLut ComputeNaturalCoeffOrder() { + AcStrategy::CoeffOrderAndLut coeff; + for (size_t s = 0; s < AcStrategy::kNumValidStrategies; s++) { + const AcStrategy acs = AcStrategy::FromRawStrategy(s); + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + CoefficientLayout(&cy, &cx); + const size_t num_coeffs = kDCTBlockSize * cx * cy; + coeff_order_t* JXL_RESTRICT order_start = + coeff.order + s * AcStrategy::kMaxCoeffArea; + coeff_order_t* JXL_RESTRICT lut_start = + coeff.lut + s * AcStrategy::kMaxCoeffArea; + std::iota(order_start, order_start + num_coeffs, 0); + + auto compute_key = [cx, cy](int32_t pos) { + JXL_DASSERT(cx != 0 && cy != 0); + size_t y = pos / (cx * kBlockDim); + size_t x = pos % (cx * kBlockDim); + // Ensure that LLFs are first in the order. + if (x < cx && y < cy) { + return std::make_pair(-1, static_cast(y * cx + x)); + } + int max_dim = std::max(cx, cy); + int scaled_y = y * max_dim / cy; + int scaled_x = x * max_dim / cx; + return std::make_pair(scaled_x + scaled_y, (scaled_x + scaled_y) % 2 == 0 + ? scaled_x - scaled_y + : scaled_y - scaled_x); + }; + + std::sort(order_start, order_start + num_coeffs, + [compute_key](int32_t pos, int32_t other_pos) { + return compute_key(pos) < compute_key(other_pos); + }); + + for (size_t i = 0; i < num_coeffs; i++) { + lut_start[order_start[i]] = i; + } + } + return coeff; +}; + +HWY_ALIGN constexpr float k4x4AFVBasisTranspose[16][16] = { + { + 0.2500000000000000, + 0.8769029297991420, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + -0.4105377591765233, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + 0.2206518106944235, + 0.0000000000000000, + 0.0000000000000000, + -0.7071067811865474, + 0.6235485373547691, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + -0.1014005039375376, + 0.4067007583026075, + -0.2125574805828875, + 0.0000000000000000, + -0.0643507165794627, + -0.4517556589999482, + -0.3046847507248690, + 0.3017929516615495, + 0.4082482904638627, + 0.1747866975480809, + -0.2110560104933578, + -0.1426608480880726, + -0.1381354035075859, + -0.1743760259965107, + 0.1135498731499434, + }, + { + 0.2500000000000000, + -0.1014005039375375, + 0.4444481661973445, + 0.3085497062849767, + 0.0000000000000000, + -0.0643507165794627, + 0.1585450355184006, + 0.5112616136591823, + 0.2579236279634118, + 0.0000000000000000, + 0.0812611176717539, + 0.1856718091610980, + -0.3416446842253372, + 0.3302282550303788, + 0.0702790691196284, + -0.0741750459581035, + }, + { + 0.2500000000000000, + 0.2206518106944236, + 0.0000000000000000, + 0.0000000000000000, + 0.7071067811865476, + 0.6235485373547694, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + }, + { + 0.2500000000000000, + -0.1014005039375378, + 0.0000000000000000, + 0.4706702258572536, + 0.0000000000000000, + -0.0643507165794628, + -0.0403851516082220, + 0.0000000000000000, + 0.1627234014286620, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.7367497537172237, + 0.0875511500058708, + -0.2921026642334881, + 0.1940289303259434, + }, + { + 0.2500000000000000, + -0.1014005039375377, + 0.1957439937204294, + -0.1621205195722993, + 0.0000000000000000, + -0.0643507165794628, + 0.0074182263792424, + -0.2904801297289980, + 0.0952002265347504, + 0.0000000000000000, + -0.3675398009862027, + 0.4921585901373873, + 0.2462710772207515, + -0.0794670660590957, + 0.3623817333531167, + -0.4351904965232280, + }, + { + 0.2500000000000000, + -0.1014005039375376, + 0.2929100136981264, + 0.0000000000000000, + 0.0000000000000000, + -0.0643507165794627, + 0.3935103426921017, + -0.0657870154914280, + 0.0000000000000000, + -0.4082482904638628, + -0.3078822139579090, + -0.3852501370925192, + -0.0857401903551931, + -0.4613374887461511, + 0.0000000000000000, + 0.2191868483885747, + }, + { + 0.2500000000000000, + -0.1014005039375376, + -0.4067007583026072, + -0.2125574805828705, + 0.0000000000000000, + -0.0643507165794627, + -0.4517556589999464, + 0.3046847507248840, + 0.3017929516615503, + -0.4082482904638635, + -0.1747866975480813, + 0.2110560104933581, + -0.1426608480880734, + -0.1381354035075829, + -0.1743760259965108, + 0.1135498731499426, + }, + { + 0.2500000000000000, + -0.1014005039375377, + -0.1957439937204287, + -0.1621205195722833, + 0.0000000000000000, + -0.0643507165794628, + 0.0074182263792444, + 0.2904801297290076, + 0.0952002265347505, + 0.0000000000000000, + 0.3675398009862011, + -0.4921585901373891, + 0.2462710772207514, + -0.0794670660591026, + 0.3623817333531165, + -0.4351904965232251, + }, + { + 0.2500000000000000, + -0.1014005039375375, + 0.0000000000000000, + -0.4706702258572528, + 0.0000000000000000, + -0.0643507165794627, + 0.1107416575309343, + 0.0000000000000000, + -0.1627234014286617, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + 0.1488339922711357, + 0.4972464710953509, + 0.2921026642334879, + 0.5550443808910661, + }, + { + 0.2500000000000000, + -0.1014005039375377, + 0.1137907446044809, + -0.1464291867126764, + 0.0000000000000000, + -0.0643507165794628, + 0.0829816309488205, + -0.2388977352334460, + -0.3531238544981630, + -0.4082482904638630, + 0.4826689115059883, + 0.1741941265991622, + -0.0476868035022925, + 0.1253805944856366, + -0.4326608024727445, + -0.2546827712406646, + }, + { + 0.2500000000000000, + -0.1014005039375377, + -0.4444481661973438, + 0.3085497062849487, + 0.0000000000000000, + -0.0643507165794628, + 0.1585450355183970, + -0.5112616136592012, + 0.2579236279634129, + 0.0000000000000000, + -0.0812611176717504, + -0.1856718091610990, + -0.3416446842253373, + 0.3302282550303805, + 0.0702790691196282, + -0.0741750459581023, + }, + { + 0.2500000000000000, + -0.1014005039375376, + -0.2929100136981264, + 0.0000000000000000, + 0.0000000000000000, + -0.0643507165794627, + 0.3935103426921022, + 0.0657870154914254, + 0.0000000000000000, + 0.4082482904638634, + 0.3078822139579031, + 0.3852501370925211, + -0.0857401903551927, + -0.4613374887461554, + 0.0000000000000000, + 0.2191868483885728, + }, + { + 0.2500000000000000, + -0.1014005039375376, + -0.1137907446044814, + -0.1464291867126654, + 0.0000000000000000, + -0.0643507165794627, + 0.0829816309488214, + 0.2388977352334547, + -0.3531238544981624, + 0.4082482904638630, + -0.4826689115059858, + -0.1741941265991621, + -0.0476868035022928, + 0.1253805944856431, + -0.4326608024727457, + -0.2546827712406641, + }, + { + 0.2500000000000000, + -0.1014005039375374, + 0.0000000000000000, + 0.4251149611657548, + 0.0000000000000000, + -0.0643507165794626, + -0.4517556589999480, + 0.0000000000000000, + -0.6035859033230976, + 0.0000000000000000, + 0.0000000000000000, + 0.0000000000000000, + -0.1426608480880724, + -0.1381354035075845, + 0.3487520519930227, + 0.1135498731499429, + }, +}; + +HWY_ALIGN constexpr float k4x4AFVBasis[16][16] = { + { + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + }, + { + 0.876902929799142, + 0.2206518106944235, + -0.10140050393753763, + -0.1014005039375375, + 0.2206518106944236, + -0.10140050393753777, + -0.10140050393753772, + -0.10140050393753763, + -0.10140050393753758, + -0.10140050393753769, + -0.1014005039375375, + -0.10140050393753768, + -0.10140050393753768, + -0.10140050393753759, + -0.10140050393753763, + -0.10140050393753741, + }, + { + 0.0, + 0.0, + 0.40670075830260755, + 0.44444816619734445, + 0.0, + 0.0, + 0.19574399372042936, + 0.2929100136981264, + -0.40670075830260716, + -0.19574399372042872, + 0.0, + 0.11379074460448091, + -0.44444816619734384, + -0.29291001369812636, + -0.1137907446044814, + 0.0, + }, + { + 0.0, + 0.0, + -0.21255748058288748, + 0.3085497062849767, + 0.0, + 0.4706702258572536, + -0.1621205195722993, + 0.0, + -0.21255748058287047, + -0.16212051957228327, + -0.47067022585725277, + -0.1464291867126764, + 0.3085497062849487, + 0.0, + -0.14642918671266536, + 0.4251149611657548, + }, + { + 0.0, + -0.7071067811865474, + 0.0, + 0.0, + 0.7071067811865476, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + }, + { + -0.4105377591765233, + 0.6235485373547691, + -0.06435071657946274, + -0.06435071657946266, + 0.6235485373547694, + -0.06435071657946284, + -0.0643507165794628, + -0.06435071657946274, + -0.06435071657946272, + -0.06435071657946279, + -0.06435071657946266, + -0.06435071657946277, + -0.06435071657946277, + -0.06435071657946273, + -0.06435071657946274, + -0.0643507165794626, + }, + { + 0.0, + 0.0, + -0.4517556589999482, + 0.15854503551840063, + 0.0, + -0.04038515160822202, + 0.0074182263792423875, + 0.39351034269210167, + -0.45175565899994635, + 0.007418226379244351, + 0.1107416575309343, + 0.08298163094882051, + 0.15854503551839705, + 0.3935103426921022, + 0.0829816309488214, + -0.45175565899994796, + }, + { + 0.0, + 0.0, + -0.304684750724869, + 0.5112616136591823, + 0.0, + 0.0, + -0.290480129728998, + -0.06578701549142804, + 0.304684750724884, + 0.2904801297290076, + 0.0, + -0.23889773523344604, + -0.5112616136592012, + 0.06578701549142545, + 0.23889773523345467, + 0.0, + }, + { + 0.0, + 0.0, + 0.3017929516615495, + 0.25792362796341184, + 0.0, + 0.16272340142866204, + 0.09520022653475037, + 0.0, + 0.3017929516615503, + 0.09520022653475055, + -0.16272340142866173, + -0.35312385449816297, + 0.25792362796341295, + 0.0, + -0.3531238544981624, + -0.6035859033230976, + }, + { + 0.0, + 0.0, + 0.40824829046386274, + 0.0, + 0.0, + 0.0, + 0.0, + -0.4082482904638628, + -0.4082482904638635, + 0.0, + 0.0, + -0.40824829046386296, + 0.0, + 0.4082482904638634, + 0.408248290463863, + 0.0, + }, + { + 0.0, + 0.0, + 0.1747866975480809, + 0.0812611176717539, + 0.0, + 0.0, + -0.3675398009862027, + -0.307882213957909, + -0.17478669754808135, + 0.3675398009862011, + 0.0, + 0.4826689115059883, + -0.08126111767175039, + 0.30788221395790305, + -0.48266891150598584, + 0.0, + }, + { + 0.0, + 0.0, + -0.21105601049335784, + 0.18567180916109802, + 0.0, + 0.0, + 0.49215859013738733, + -0.38525013709251915, + 0.21105601049335806, + -0.49215859013738905, + 0.0, + 0.17419412659916217, + -0.18567180916109904, + 0.3852501370925211, + -0.1741941265991621, + 0.0, + }, + { + 0.0, + 0.0, + -0.14266084808807264, + -0.3416446842253372, + 0.0, + 0.7367497537172237, + 0.24627107722075148, + -0.08574019035519306, + -0.14266084808807344, + 0.24627107722075137, + 0.14883399227113567, + -0.04768680350229251, + -0.3416446842253373, + -0.08574019035519267, + -0.047686803502292804, + -0.14266084808807242, + }, + { + 0.0, + 0.0, + -0.13813540350758585, + 0.3302282550303788, + 0.0, + 0.08755115000587084, + -0.07946706605909573, + -0.4613374887461511, + -0.13813540350758294, + -0.07946706605910261, + 0.49724647109535086, + 0.12538059448563663, + 0.3302282550303805, + -0.4613374887461554, + 0.12538059448564315, + -0.13813540350758452, + }, + { + 0.0, + 0.0, + -0.17437602599651067, + 0.0702790691196284, + 0.0, + -0.2921026642334881, + 0.3623817333531167, + 0.0, + -0.1743760259965108, + 0.36238173335311646, + 0.29210266423348785, + -0.4326608024727445, + 0.07027906911962818, + 0.0, + -0.4326608024727457, + 0.34875205199302267, + }, + { + 0.0, + 0.0, + 0.11354987314994337, + -0.07417504595810355, + 0.0, + 0.19402893032594343, + -0.435190496523228, + 0.21918684838857466, + 0.11354987314994257, + -0.4351904965232251, + 0.5550443808910661, + -0.25468277124066463, + -0.07417504595810233, + 0.2191868483885728, + -0.25468277124066413, + 0.1135498731499429, + }, +}; + +using D = HWY_CAPPED(float, 16); + +// Coefficient layout: +// - (even, even) positions hold AFV coefficients +// - (odd, even) positions hold DCT4x4 coefficients +// - (any, odd) positions hold DCT4x8 coefficients +template +HWY_ATTR void AFVTransformFromPixels(const float* JXL_RESTRICT pixels, + size_t pixels_stride, + float* JXL_RESTRICT coefficients) { + size_t afv_x = afv_kind & 1; + size_t afv_y = afv_kind / 2; + HWY_ALIGN float block[4 * 8]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + block[(afv_y == 1 ? 3 - iy : iy) * 4 + (afv_x == 1 ? 3 - ix : ix)] = + pixels[(iy + 4 * afv_y) * pixels_stride + ix + 4 * afv_x]; + } + } + // AFV coefficients in (even, even) positions. + HWY_ALIGN float coeff[4 * 4]; + AFVDCT4x4(block, coeff); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + coefficients[iy * 2 * 8 + ix * 2] = coeff[iy * 4 + ix]; + } + } + // 4x4 DCT of the block with same y and different x. + ComputeTransposedScaledDCT<4>()( + FromLines<4>(pixels + afv_y * 4 * pixels_stride + (afv_x == 1 ? 0 : 4), + pixels_stride), + ScaleToBlock<4, 4>(block)); + // ... in (odd, even) positions. + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[iy * 2 * 8 + ix * 2 + 1] = block[iy * 4 + ix]; + } + } + // 4x8 DCT of the other half of the block. + ComputeScaledDCT<4, 8>()( + FromLines<8>(pixels + (afv_y == 1 ? 0 : 4) * pixels_stride, + pixels_stride), + ScaleToBlock<4, 8>(block)); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[(1 + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + float block00 = coefficients[0] * 0.25f; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + coefficients[0] = (block00 + block01 + 2 * block10) * 0.25f; + coefficients[1] = (block00 - block01) * 0.5f; + coefficients[8] = (block00 + block01 - 2 * block10) * 0.25f; +} + +template +HWY_ATTR void AFVTransformToPixels(const float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, + size_t pixels_stride) { + size_t afv_x = afv_kind & 1; + size_t afv_y = afv_kind / 2; + float dcs[3] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + dcs[0] = (block00 + block10 + block01) * 4.0f; + dcs[1] = (block00 + block10 - block01); + dcs[2] = block00 - block10; + // IAFV: (even, even) positions. + HWY_ALIGN float coeff[4 * 4]; + coeff[0] = dcs[0]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + coeff[iy * 4 + ix] = coefficients[iy * 2 * 8 + ix * 2]; + } + } + HWY_ALIGN float block[4 * 8]; + AFVIDCT4x4(coeff, block); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + pixels[(iy + afv_y * 4) * pixels_stride + afv_x * 4 + ix] = + block[(afv_y == 1 ? 3 - iy : iy) * 4 + (afv_x == 1 ? 3 - ix : ix)]; + } + } + // IDCT4x4 in (odd, even) positions. + block[0] = dcs[1]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 4 + ix] = coefficients[iy * 2 * 8 + ix * 2 + 1]; + } + } + ComputeTransposedScaledIDCT<4>()( + FromBlock<4, 4>(block), + ToLines<4>(pixels + afv_y * 4 * pixels_stride + (afv_x == 1 ? 0 : 4), + pixels_stride)); + // IDCT4x8. + block[0] = dcs[2]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(1 + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<4, 8>()( + FromBlock<4, 8>(block), + ToLines<8>(pixels + (afv_y == 1 ? 0 : 4) * pixels_stride, pixels_stride)); +} + +} // namespace + +HWY_ATTR void AFVDCT4x4(const float* JXL_RESTRICT pixels, + float* JXL_RESTRICT coeffs) { + for (size_t i = 0; i < 16; i += D::N) { + auto scalar = Zero(D()); + for (size_t j = 0; j < 16; j++) { + auto px = Set(D(), pixels[j]); + auto basis = Load(D(), k4x4AFVBasisTranspose[j] + i); + scalar = MulAdd(px, basis, scalar); + } + Store(scalar, D(), coeffs + i); + } +} + +HWY_ATTR void AFVIDCT4x4(const float* JXL_RESTRICT coeffs, + float* JXL_RESTRICT pixels) { + for (size_t i = 0; i < 16; i += D::N) { + auto pixel = Zero(D()); + for (size_t j = 0; j < 16; j++) { + auto cf = Set(D(), coeffs[j]); + auto basis = Load(D(), k4x4AFVBasis[j] + i); + pixel = MulAdd(cf, basis, pixel); + } + Store(pixel, D(), pixels + i); + } +} + +const AcStrategy::CoeffOrderAndLut* AcStrategy::CoeffOrder() { + static AcStrategy::CoeffOrderAndLut order = ComputeNaturalCoeffOrder(); + return ℴ +} + +// These definitions are needed before C++17. +constexpr size_t AcStrategy::kMaxCoeffBlocks; +constexpr size_t AcStrategy::kMaxBlockDim; +constexpr size_t AcStrategy::kMaxCoeffArea; + +HWY_ATTR void AcStrategy::TransformFromPixels( + const float* JXL_RESTRICT pixels, size_t pixels_stride, + float* JXL_RESTRICT coefficients) const { + switch (strategy_) { + case Type::IDENTITY: { + PROFILER_ZONE("DCT Identity"); + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + float block_dc = 0; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + block_dc += pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix]; + } + } + block_dc *= 1.0f / 16; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 1 && iy == 1) continue; + coefficients[(y + iy * 2) * 8 + x + ix * 2] = + pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix] - + pixels[(y * 4 + 1) * pixels_stride + x * 4 + 1]; + } + } + coefficients[(y + 2) * 8 + x + 2] = coefficients[y * 8 + x]; + coefficients[y * 8 + x] = block_dc; + } + } + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + break; + } + case Type::DCT8X4: { + PROFILER_ZONE("DCT 8x4"); + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 8]; + ComputeScaledDCT<8, 4>()(FromLines<4>(pixels + x * 4, pixels_stride), + ScaleToBlock<4, 8>(block)); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + // Store transposed. + coefficients[(x + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + } + float block0 = coefficients[0]; + float block1 = coefficients[8]; + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + break; + } + case Type::DCT4X8: { + PROFILER_ZONE("DCT 4x8"); + for (size_t y = 0; y < 2; y++) { + HWY_ALIGN float block[4 * 8]; + ComputeScaledDCT<4, 8>()( + FromLines<8>(pixels + y * 4 * pixels_stride, pixels_stride), + ScaleToBlock<4, 8>(block)); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + coefficients[(y + iy * 2) * 8 + ix] = block[iy * 8 + ix]; + } + } + } + float block0 = coefficients[0]; + float block1 = coefficients[8]; + coefficients[0] = (block0 + block1) * 0.5f; + coefficients[8] = (block0 - block1) * 0.5f; + break; + } + case Type::DCT4X4: { + PROFILER_ZONE("DCT 4"); + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 4]; + ComputeTransposedScaledDCT<4>()( + FromLines<4>(pixels + y * 4 * pixels_stride + x * 4, + pixels_stride), + ScaleToBlock<4>(block)); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + coefficients[(y + iy * 2) * 8 + x + ix * 2] = block[iy * 4 + ix]; + } + } + } + } + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + coefficients[0] = (block00 + block01 + block10 + block11) * 0.25f; + coefficients[1] = (block00 + block01 - block10 - block11) * 0.25f; + coefficients[8] = (block00 - block01 + block10 - block11) * 0.25f; + coefficients[9] = (block00 - block01 - block10 + block11) * 0.25f; + break; + } + case Type::DCT2X2: { + PROFILER_ZONE("DCT 2"); + DCT2TopBlock<8>(pixels, pixels_stride, coefficients); + DCT2TopBlock<4>(coefficients, kBlockDim, coefficients); + DCT2TopBlock<2>(coefficients, kBlockDim, coefficients); + break; + } + case Type::DCT16X16: { + PROFILER_ZONE("DCT 16"); + ComputeTransposedScaledDCT<2 * kBlockDim>()( + FromLines<2 * kBlockDim>(pixels, pixels_stride), + ScaleToBlock<2 * kBlockDim>(coefficients)); + break; + } + case Type::DCT16X8: { + PROFILER_ZONE("DCT 16x8"); + ComputeScaledDCT<16, 8>()(FromLines<8>(pixels, pixels_stride), + ScaleToBlock<8, 16>(coefficients)); + break; + } + case Type::DCT8X16: { + PROFILER_ZONE("DCT 8x16"); + ComputeScaledDCT<8, 16>()(FromLines<16>(pixels, pixels_stride), + ScaleToBlock<8, 16>(coefficients)); + break; + } + case Type::DCT32X8: { + PROFILER_ZONE("DCT 32x8"); + ComputeScaledDCT<32, 8>()(FromLines<8>(pixels, pixels_stride), + ScaleToBlock<8, 32>(coefficients)); + break; + } + case Type::DCT8X32: { + PROFILER_ZONE("DCT 8x32"); + ComputeScaledDCT<8, 32>()(FromLines<32>(pixels, pixels_stride), + ScaleToBlock<8, 32>(coefficients)); + break; + } + case Type::DCT32X16: { + PROFILER_ZONE("DCT 32x16"); + ComputeScaledDCT<32, 16>()(FromLines<16>(pixels, pixels_stride), + ScaleToBlock<16, 32>(coefficients)); + break; + } + case Type::DCT16X32: { + PROFILER_ZONE("DCT 16x32"); + ComputeScaledDCT<16, 32>()(FromLines<32>(pixels, pixels_stride), + ScaleToBlock<16, 32>(coefficients)); + break; + } + case Type::DCT32X32: { + PROFILER_ZONE("DCT 32"); + ComputeTransposedScaledDCT<4 * kBlockDim>()( + FromLines<4 * kBlockDim>(pixels, pixels_stride), + ScaleToBlock<4 * kBlockDim>(coefficients)); + break; + } + case Type::DCT: { + PROFILER_ZONE("DCT 8"); + ComputeTransposedScaledDCT()( + FromLines(pixels, pixels_stride), + ScaleToBlock(coefficients)); + break; + } + case Type::AFV0: { + PROFILER_ZONE("AFV0"); + AFVTransformFromPixels<0>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV1: { + PROFILER_ZONE("AFV1"); + AFVTransformFromPixels<1>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV2: { + PROFILER_ZONE("AFV2"); + AFVTransformFromPixels<2>(pixels, pixels_stride, coefficients); + break; + } + case Type::AFV3: { + PROFILER_ZONE("AFV3"); + AFVTransformFromPixels<3>(pixels, pixels_stride, coefficients); + break; + } + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +HWY_ATTR void AcStrategy::TransformToPixels( + const float* JXL_RESTRICT coefficients, float* JXL_RESTRICT pixels, + size_t pixels_stride) const { + switch (strategy_) { + case Type::IDENTITY: { + PROFILER_ZONE("IDCT Identity"); + float dcs[4] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + float block_dc = dcs[y * 2 + x]; + float residual_sum = 0; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + residual_sum += coefficients[(y + iy * 2) * 8 + x + ix * 2]; + } + } + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1] = + block_dc - residual_sum * (1.0f / 16); + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 1 && iy == 1) continue; + pixels[(y * 4 + iy) * pixels_stride + x * 4 + ix] = + coefficients[(y + iy * 2) * 8 + x + ix * 2] + + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1]; + } + } + pixels[y * 4 * pixels_stride + x * 4] = + coefficients[(y + 2) * 8 + x + 2] + + pixels[(4 * y + 1) * pixels_stride + 4 * x + 1]; + } + } + break; + } + case Type::DCT8X4: { + PROFILER_ZONE("IDCT 8x4"); + float dcs[2] = {}; + float block0 = coefficients[0]; + float block1 = coefficients[8]; + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 8]; + block[0] = dcs[x]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(x + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<8, 4>()(FromBlock<4, 8>(block), + ToLines<4>(pixels + x * 4, pixels_stride)); + } + break; + } + case Type::DCT4X8: { + PROFILER_ZONE("IDCT 4x8"); + float dcs[2] = {}; + float block0 = coefficients[0]; + float block1 = coefficients[8]; + dcs[0] = block0 + block1; + dcs[1] = block0 - block1; + for (size_t y = 0; y < 2; y++) { + HWY_ALIGN float block[4 * 8]; + block[0] = dcs[y]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 8 + ix] = coefficients[(y + iy * 2) * 8 + ix]; + } + } + ComputeScaledIDCT<4, 8>()( + FromBlock<4, 8>(block), + ToLines<8>(pixels + y * 4 * pixels_stride, pixels_stride)); + } + break; + } + case Type::DCT4X4: { + PROFILER_ZONE("IDCT 4"); + float dcs[4] = {}; + float block00 = coefficients[0]; + float block01 = coefficients[1]; + float block10 = coefficients[8]; + float block11 = coefficients[9]; + dcs[0] = block00 + block01 + block10 + block11; + dcs[1] = block00 + block01 - block10 - block11; + dcs[2] = block00 - block01 + block10 - block11; + dcs[3] = block00 - block01 - block10 + block11; + for (size_t y = 0; y < 2; y++) { + for (size_t x = 0; x < 2; x++) { + HWY_ALIGN float block[4 * 4]; + block[0] = dcs[y * 2 + x]; + for (size_t iy = 0; iy < 4; iy++) { + for (size_t ix = 0; ix < 4; ix++) { + if (ix == 0 && iy == 0) continue; + block[iy * 4 + ix] = coefficients[(y + iy * 2) * 8 + x + ix * 2]; + } + } + ComputeTransposedScaledIDCT<4>()( + FromBlock<4>(block), + ToLines<4>(pixels + y * 4 * pixels_stride + x * 4, + pixels_stride)); + } + } + break; + } + case Type::DCT2X2: { + PROFILER_ZONE("IDCT 2"); + HWY_ALIGN float coeffs[kDCTBlockSize]; + memcpy(coeffs, coefficients, sizeof(float) * kDCTBlockSize); + IDCT2TopBlock<2>(coeffs, kBlockDim, coeffs); + IDCT2TopBlock<4>(coeffs, kBlockDim, coeffs); + IDCT2TopBlock<8>(coeffs, kBlockDim, coeffs); + for (size_t y = 0; y < kBlockDim; y++) { + for (size_t x = 0; x < kBlockDim; x++) { + pixels[y * pixels_stride + x] = coeffs[y * kBlockDim + x]; + } + } + break; + } + case Type::DCT16X16: { + PROFILER_ZONE("IDCT 16"); + ComputeTransposedScaledIDCT<2 * kBlockDim>()( + FromBlock<2 * kBlockDim>(coefficients), + ToLines<2 * kBlockDim>(pixels, pixels_stride)); + break; + } + case Type::DCT16X8: { + PROFILER_ZONE("IDCT 16x8"); + ComputeScaledIDCT<16, 8>()(FromBlock<8, 16>(coefficients), + ToLines<8>(pixels, pixels_stride)); + break; + } + case Type::DCT8X16: { + PROFILER_ZONE("IDCT 8x16"); + ComputeScaledIDCT<8, 16>()(FromBlock<8, 16>(coefficients), + ToLines<16>(pixels, pixels_stride)); + break; + } + case Type::DCT32X8: { + PROFILER_ZONE("IDCT 32x8"); + ComputeScaledIDCT<32, 8>()(FromBlock<8, 32>(coefficients), + ToLines<8>(pixels, pixels_stride)); + break; + } + case Type::DCT8X32: { + PROFILER_ZONE("IDCT 8x32"); + ComputeScaledIDCT<8, 32>()(FromBlock<8, 32>(coefficients), + ToLines<32>(pixels, pixels_stride)); + break; + } + case Type::DCT32X16: { + PROFILER_ZONE("IDCT 32x16"); + ComputeScaledIDCT<32, 16>()(FromBlock<16, 32>(coefficients), + ToLines<16>(pixels, pixels_stride)); + break; + } + case Type::DCT16X32: { + PROFILER_ZONE("IDCT 16x32"); + ComputeScaledIDCT<16, 32>()(FromBlock<16, 32>(coefficients), + ToLines<32>(pixels, pixels_stride)); + break; + } + case Type::DCT32X32: { + PROFILER_ZONE("IDCT 32"); + ComputeTransposedScaledIDCT<4 * kBlockDim>()( + FromBlock<4 * kBlockDim>(coefficients), + ToLines<4 * kBlockDim>(pixels, pixels_stride)); + break; + } + case Type::DCT: { + PROFILER_ZONE("IDCT 8"); + ComputeTransposedScaledIDCT()( + FromBlock(coefficients), + ToLines(pixels, pixels_stride)); + break; + } + case Type::AFV0: { + PROFILER_ZONE("IAFV0"); + AFVTransformToPixels<0>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV1: { + PROFILER_ZONE("IAFV1"); + AFVTransformToPixels<1>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV2: { + PROFILER_ZONE("IAFV2"); + AFVTransformToPixels<2>(coefficients, pixels, pixels_stride); + break; + } + case Type::AFV3: { + PROFILER_ZONE("IAFV3"); + AFVTransformToPixels<3>(coefficients, pixels, pixels_stride); + break; + } + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +HWY_ATTR void AcStrategy::LowestFrequenciesFromDC(const float* dc, + size_t dc_stride, + float* llf) const { + switch (strategy_) { + case Type::DCT16X8: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT8X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT16X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 2 * kBlockDim); + break; + } + case Type::DCT32X8: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT8X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT32X16: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT16X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT32X32: { + ReinterpretingDCT( + dc, dc_stride, llf, 4 * kBlockDim); + break; + } + case Type::DCT: + case Type::DCT2X2: + case Type::DCT4X4: + case Type::DCT4X8: + case Type::DCT8X4: + case Type::AFV0: + case Type::AFV1: + case Type::AFV2: + case Type::AFV3: + case Type::IDENTITY: + llf[0] = dc[0]; + break; + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + }; +} + +HWY_ATTR void AcStrategy::DCFromLowestFrequencies(const float* block, float* dc, + size_t dc_stride) const { + switch (strategy_) { + case Type::DCT16X8: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT8X16: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT16X16: { + ReinterpretingIDCT( + block, 2 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X8: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT8X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X16: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT16X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT32X32: { + ReinterpretingIDCT( + block, 4 * kBlockDim, dc, dc_stride); + break; + } + case Type::DCT: + case Type::DCT2X2: + case Type::DCT4X4: + case Type::DCT4X8: + case Type::DCT8X4: + case Type::AFV0: + case Type::AFV1: + case Type::AFV2: + case Type::AFV3: + case Type::IDENTITY: + dc[0] = block[0]; + break; + case Type::kNumValidStrategies: + JXL_ABORT("Invalid strategy"); + } +} + +AcStrategyImage::AcStrategyImage(size_t xsize, size_t ysize) + : layers_(xsize, ysize) { + row_ = layers_.Row(0); + stride_ = layers_.PixelsPerRow(); +} + +size_t AcStrategyImage::CountBlocks(AcStrategy::Type type) const { + size_t ret = 0; + for (size_t y = 0; y < layers_.ysize(); y++) { + const uint8_t* JXL_RESTRICT row = layers_.ConstRow(y); + for (size_t x = 0; x < layers_.xsize(); x++) { + if (row[x] == ((static_cast(type) << 1) | 1)) ret++; + } + } + return ret; +} + +} // namespace jxl diff --git a/jxl/ac_strategy.h b/jxl/ac_strategy.h new file mode 100644 index 000000000..03faa0548 --- /dev/null +++ b/jxl/ac_strategy.h @@ -0,0 +1,324 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_AC_STRATEGY_H_ +#define JXL_AC_STRATEGY_H_ + +#include +#include + +#include + +#include "jxl/aux_out_fwd.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/status.h" +#include "jxl/coeff_order_fwd.h" +#include "jxl/common.h" +#include "jxl/dct.h" +#include "jxl/dct_util.h" +#include "jxl/image.h" +#include "jxl/image_ops.h" + +// Defines the different kinds of transforms, and heuristics to choose between +// them. +// `AcStrategy` represents what transform should be used, and which sub-block of +// that transform we are currently in. Note that DCT4x4 is applied on all four +// 4x4 sub-blocks of an 8x8 block. +// `AcStrategyImage` defines which strategy should be used for each 8x8 block +// of the image. The highest 4 bits represent the strategy to be used, the +// lowest 4 represent the index of the block inside that strategy. Blocks should +// be aligned, i.e. 32x32 blocks should only start in positions that are +// multiples of 32. + +namespace jxl { + +template +struct square_root { + static constexpr float value = + square_root::value * 1.4142135623730951f; +}; + +template <> +struct square_root<1> { + static constexpr float value = 1.0f; +}; + +// Apply the DCT algorithm-intrinsic constants to DCTResampleScale. +// Note that the DCTScales constants give results that are scaled with a factor +// proportional to 1/sqrt(N), so we counteract that here. +// We also use the fact that 1/(sqrt(N) DCTScales(N)) == sqrt(N) IDCTScales(N) +// to avoid a division. +template +constexpr float DCTTotalResampleScale(size_t x) { + return square_root::value * DCTScales()[x] * + square_root::value * IDCTScales()[x] * + DCTResampleScales::kScales[x]; +} + +void AFVDCT4x4(const float* JXL_RESTRICT pixels, float* JXL_RESTRICT coeffs); +void AFVIDCT4x4(const float* JXL_RESTRICT coeffs, float* JXL_RESTRICT pixels); + +class AcStrategy { + public: + // Extremal values for the number of blocks/coefficients of a single strategy. + static constexpr size_t kMaxCoeffBlocks = 4; + static constexpr size_t kMaxBlockDim = kBlockDim * kMaxCoeffBlocks; + static constexpr size_t kMaxCoeffArea = kMaxBlockDim * kMaxBlockDim; + + // Raw strategy types. + enum Type : uint32_t { + // Regular block size DCT (value matches kQuantKind) + DCT = 0, + // Encode pixels without transforming (value matches kQuantKind) + IDENTITY = 1, + // Use 2-by-2 DCT (value matches kQuantKind) + DCT2X2 = 2, + // Use 4-by-4 DCT (value matches kQuantKind) + DCT4X4 = 3, + // Use 16-by-16 DCT + DCT16X16 = 4, + // Use 32-by-32 DCT + DCT32X32 = 5, + // Use 16-by-8 DCT + DCT16X8 = 6, + // Use 8-by-16 DCT + DCT8X16 = 7, + // Use 32-by-8 DCT + DCT32X8 = 8, + // Use 8-by-32 DCT + DCT8X32 = 9, + // Use 32-by-16 DCT + DCT32X16 = 10, + // Use 16-by-32 DCT + DCT16X32 = 11, + // 4x8 and 8x4 DCT + DCT4X8 = 12, + DCT8X4 = 13, + // Corner-DCT. + AFV0 = 14, + AFV1 = 15, + AFV2 = 16, + AFV3 = 17, + // Marker for num of valid strategies. + kNumValidStrategies + }; + + static constexpr uint32_t TypeBit(const Type type) { + return 1u << static_cast(type); + } + + // Returns true if this block is the first 8x8 block (i.e. top-left) of a + // possibly multi-block strategy. + JXL_INLINE bool IsFirstBlock() const { return is_first_; } + + JXL_INLINE bool IsMultiblock() const { + constexpr uint32_t bits = TypeBit(Type::DCT16X16) | + TypeBit(Type::DCT32X32) | TypeBit(Type::DCT16X8) | + TypeBit(Type::DCT8X16) | TypeBit(Type::DCT32X8) | + TypeBit(Type::DCT8X32) | TypeBit(Type::DCT16X32) | + TypeBit(Type::DCT32X16); + return ((1u << static_cast(Strategy())) & bits) != 0; + } + + // Returns the raw strategy value. Should only be used for tokenization. + JXL_INLINE uint8_t RawStrategy() const { + return static_cast(strategy_); + } + + JXL_INLINE Type Strategy() const { return strategy_; } + + // Inverse check + static JXL_INLINE constexpr bool IsRawStrategyValid(uint8_t raw_strategy) { + return raw_strategy < kNumValidStrategies; + } + static JXL_INLINE AcStrategy FromRawStrategy(uint8_t raw_strategy) { + return FromRawStrategy(static_cast(raw_strategy)); + } + static JXL_INLINE AcStrategy FromRawStrategy(Type raw_strategy) { + return AcStrategy(raw_strategy, /*is_first_block=*/true); + } + + // "Natural order" means the order of increasing of "anisotropic" frequency of + // continuous version of DCT basis. + // Round-trip, for any given strategy s: + // X = NaturalCoeffOrder(s)[NaturalCoeffOrderLutN(s)[X]] + // X = NaturalCoeffOrderLut(s)[NaturalCoeffOrderN(s)[X]] + JXL_INLINE const coeff_order_t* NaturalCoeffOrder() const { + return CoeffOrder()->order + RawStrategy() * kMaxCoeffArea; + } + + JXL_INLINE const coeff_order_t* NaturalCoeffOrderLut() const { + return CoeffOrder()->lut + RawStrategy() * kMaxCoeffArea; + } + + // Number of 8x8 blocks that this strategy will cover. 0 for non-top-left + // blocks inside a multi-block transform. + JXL_INLINE size_t covered_blocks_x() const { + static constexpr const uint8_t kLut[] = {1, 1, 1, 1, 2, 4, 1, 2, 1, + 4, 2, 4, 1, 1, 1, 1, 1, 1}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + JXL_INLINE size_t covered_blocks_y() const { + static constexpr const uint8_t kLut[] = {1, 1, 1, 1, 2, 4, 2, 1, 4, + 1, 4, 2, 1, 1, 1, 1, 1, 1}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + JXL_INLINE size_t log2_covered_blocks() const { + static constexpr const uint8_t kLut[] = {0, 0, 0, 0, 2, 4, 1, 1, 2, + 2, 3, 3, 0, 0, 0, 0, 0, 0}; + static_assert(sizeof(kLut) / sizeof(*kLut) == kNumValidStrategies, + "Update LUT"); + return kLut[size_t(strategy_)]; + } + + // 1 / covered_block_x() / covered_block_y(), for fast division. + // Should only be called with block_ == 0. + JXL_INLINE float inverse_covered_blocks() const { + if (strategy_ == Type::DCT32X32) return 1.0f / 16; + if (strategy_ == Type::DCT16X16) return 0.25f; + if (strategy_ == Type::DCT8X16 || strategy_ == Type::DCT16X8) return 0.5f; + if (strategy_ == Type::DCT8X32 || strategy_ == Type::DCT32X8) return 0.25f; + if (strategy_ == Type::DCT32X16 || strategy_ == Type::DCT16X32) + return 1.0f / 8; + return 1.0f; + } + + JXL_INLINE float InverseNumACCoefficients() const { + JXL_DASSERT(IsFirstBlock()); + if (strategy_ == Type::DCT32X32) return 1.0f / (32 * 32 - 16); + if (strategy_ == Type::DCT16X16) return 1.0f / (16 * 16 - 4); + if (strategy_ == Type::DCT8X16 || strategy_ == Type::DCT16X8) + return 1.0f / (8 * 16 - 2); + if (strategy_ == Type::DCT8X32 || strategy_ == Type::DCT32X8) + return 1.0f / (8 * 32 - 4); + if (strategy_ == Type::DCT32X16 || strategy_ == Type::DCT16X32) + return 1.0f / (32 * 16 - 8); + return 1.0f / (8 * 8 - 1); + } + + // Pixel to coefficients and vice-versa + HWY_ATTR void TransformFromPixels(const float* JXL_RESTRICT pixels, + size_t pixels_stride, + float* JXL_RESTRICT coefficients) const; + HWY_ATTR void TransformToPixels(const float* JXL_RESTRICT coefficients, + float* JXL_RESTRICT pixels, + size_t pixels_stride) const; + + // Same as above, but for DC image. + HWY_ATTR void LowestFrequenciesFromDC(const float* dc, size_t dc_stride, + float* llf) const; + HWY_ATTR void DCFromLowestFrequencies(const float* block, float* dc, + size_t dc_stride) const; + + struct CoeffOrderAndLut { + coeff_order_t order[kNumValidStrategies * kMaxCoeffArea]; + coeff_order_t lut[kNumValidStrategies * kMaxCoeffArea]; + }; + + private: + friend class AcStrategyRow; + JXL_INLINE AcStrategy(Type strategy, bool is_first) + : strategy_(strategy), is_first_(is_first) { + JXL_DASSERT(IsMultiblock() || is_first == true); + } + + Type strategy_; + bool is_first_; + + static const CoeffOrderAndLut* CoeffOrder(); +}; + +// Class to use a certain row of the AC strategy. +class AcStrategyRow { + public: + explicit AcStrategyRow(const uint8_t* row) : row_(row) {} + AcStrategy operator[](size_t x) const { + return AcStrategy(static_cast(row_[x] >> 1), row_[x] & 1); + } + + private: + const uint8_t* JXL_RESTRICT row_; +}; + +class AcStrategyImage { + public: + AcStrategyImage() = default; + AcStrategyImage(size_t xsize, size_t ysize); + AcStrategyImage(AcStrategyImage&&) = default; + AcStrategyImage& operator=(AcStrategyImage&&) = default; + + void FillDCT8() { + FillImage((static_cast(AcStrategy::Type::DCT) << 1) | 1, + &layers_); + } + + void FillInvalid() { FillImage(INVALID, &layers_); } + + void Set(size_t x, size_t y, AcStrategy::Type type) { +#if JXL_ENABLE_ASSERT + AcStrategy acs = AcStrategy::FromRawStrategy(type); +#endif // JXL_ENABLE_ASSERT + JXL_ASSERT(y + acs.covered_blocks_y() <= layers_.ysize()); + JXL_ASSERT(x + acs.covered_blocks_x() <= layers_.xsize()); + SetNoBoundsCheck(x, y, type); + } + + void SetNoBoundsCheck(size_t x, size_t y, AcStrategy::Type type) { + AcStrategy acs = AcStrategy::FromRawStrategy(type); + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + row_[(y + iy) * stride_ + x + ix] = + (static_cast(type) << 1) | ((iy | ix) == 0 ? 1 : 0); + } + } + } + + bool IsValid(size_t x, size_t y) { return row_[y * stride_ + x] != INVALID; } + + AcStrategyRow ConstRow(size_t y, size_t x_prefix = 0) const { + return AcStrategyRow(layers_.ConstRow(y) + x_prefix); + } + + AcStrategyRow ConstRow(const Rect& rect, size_t y) const { + return ConstRow(rect.y0() + y, rect.x0()); + } + + size_t PixelsPerRow() const { return layers_.PixelsPerRow(); } + + size_t xsize() const { return layers_.xsize(); } + size_t ysize() const { return layers_.ysize(); } + + // Count the number of blocks of a given type. + size_t CountBlocks(AcStrategy::Type type) const; + + private: + ImageB layers_; + uint8_t* JXL_RESTRICT row_; + size_t stride_; + + // A value that does not represent a valid combined AC strategy + // value. Used as a sentinel. + static constexpr uint8_t INVALID = 0xFF; +}; + +} // namespace jxl + +#endif // JXL_AC_STRATEGY_H_ diff --git a/jxl/ac_strategy_test.cc b/jxl/ac_strategy_test.cc new file mode 100644 index 000000000..bb52028dd --- /dev/null +++ b/jxl/ac_strategy_test.cc @@ -0,0 +1,405 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/ac_strategy.h" + +#include + +#include +#include + +#include "gtest/gtest.h" +#include "jxl/common.h" +#include "jxl/dct.h" + +namespace jxl { +namespace { + +// Test that DCT -> IDCT is a noop. +void TestAcStrategyRoundtrip(AcStrategy::Type type) { + AcStrategy acs = AcStrategy::FromRawStrategy(type); + HWY_ALIGN float coeffs[AcStrategy::kMaxCoeffArea] = {}; + HWY_ALIGN float idct[AcStrategy::kMaxCoeffArea]; + for (size_t i = 0; i < 64 << acs.log2_covered_blocks(); i++) { + HWY_ALIGN float input[AcStrategy::kMaxCoeffArea] = {}; + input[i] = 0.2; + acs.TransformFromPixels(input, acs.covered_blocks_x() * 8, coeffs); + ASSERT_NEAR(coeffs[0], 0.2 / (64 << acs.log2_covered_blocks()), 1e-6) + << " i = " << i; + acs.TransformToPixels(coeffs, idct, acs.covered_blocks_x() * 8); + for (size_t j = 0; j < 64 << acs.log2_covered_blocks(); j++) { + EXPECT_NEAR(idct[j], input[j], 1e-6) << "j = " << j << " i = " << i; + } + } + // Test DC. + std::fill(std::begin(idct), std::end(idct), 0); + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + HWY_ALIGN float dc[AcStrategy::kMaxCoeffArea] = {}; + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2; + acs.LowestFrequenciesFromDC(dc, acs.covered_blocks_x() * 8, coeffs); + acs.DCFromLowestFrequencies(coeffs, idct, acs.covered_blocks_x() * 8); + for (size_t j = 0; j < 64 << acs.log2_covered_blocks(); j++) { + EXPECT_NEAR(idct[j], dc[j], 1e-6) + << "j = " << j << " x = " << x << " y = " << y; + } + } + } +} + +// Test that DC(2x2) -> DCT coefficients -> IDCT -> downsampled IDCT is a noop. +void TestAcStrategyRoundtripDownsample(AcStrategy::Type type) { + AcStrategy acs = AcStrategy::FromRawStrategy(type); + HWY_ALIGN float coeffs[AcStrategy::kMaxCoeffArea] = {}; + HWY_ALIGN float idct[AcStrategy::kMaxCoeffArea]; + for (size_t y = 0; y < acs.covered_blocks_y(); y++) { + for (size_t x = 0; x < acs.covered_blocks_x(); x++) { + HWY_ALIGN float dc[AcStrategy::kMaxCoeffArea] = {}; + dc[y * acs.covered_blocks_x() * 8 + x] = 0.2; + acs.LowestFrequenciesFromDC(dc, acs.covered_blocks_x() * 8, coeffs); + acs.TransformToPixels(coeffs, idct, acs.covered_blocks_x() * 8); + // Downsample + for (size_t dy = 0; dy < acs.covered_blocks_y(); dy++) { + for (size_t dx = 0; dx < acs.covered_blocks_x(); dx++) { + float sum = 0; + for (size_t iy = 0; iy < 8; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + sum += idct[(dy * 8 + iy) * 8 * acs.covered_blocks_x() + dx * 8 + + ix]; + } + } + sum /= 64.0f; + EXPECT_NEAR(sum, dc[dy * 8 * acs.covered_blocks_x() + dx], 1e-6); + } + } + } + } +} + +// Test that IDCT(block with zeros in the non-topleft corner) -> downsampled +// IDCT is the same as IDCT -> DC(2x2) of the same block. +void TestAcStrategyDownsample(AcStrategy::Type type) { + AcStrategy acs = AcStrategy::FromRawStrategy(type); + HWY_ALIGN float idct[AcStrategy::kMaxCoeffArea]; + HWY_ALIGN float idct_acs_downsampled[AcStrategy::kMaxCoeffArea] = {}; + size_t cx = acs.covered_blocks_y(); + size_t cy = acs.covered_blocks_x(); + CoefficientLayout(&cy, &cx); + for (size_t y = 0; y < cy; y++) { + for (size_t x = 0; x < cx; x++) { + HWY_ALIGN float coeffs[AcStrategy::kMaxCoeffArea] = {}; + coeffs[y * cx * 8 + x] = 0.2; + acs.TransformToPixels(coeffs, idct, acs.covered_blocks_x() * 8); + acs.DCFromLowestFrequencies(coeffs, idct_acs_downsampled, + acs.covered_blocks_x() * 8); + // Downsample + for (size_t dy = 0; dy < acs.covered_blocks_y(); dy++) { + for (size_t dx = 0; dx < acs.covered_blocks_x(); dx++) { + float sum = 0; + for (size_t iy = 0; iy < 8; iy++) { + for (size_t ix = 0; ix < 8; ix++) { + sum += idct[(dy * 8 + iy) * 8 * acs.covered_blocks_x() + dx * 8 + + ix]; + } + } + sum /= 64; + EXPECT_NEAR( + sum, idct_acs_downsampled[dy * 8 * acs.covered_blocks_x() + dx], + 1e-6); + } + } + } + } +} + +float I8(int N, int u) { + float eps = u == 0 ? std::sqrt(0.5) : 1.0; + return std::sqrt(2.0 / N) * eps * cos(u * kPi / (2.0 * N)); +} + +float D8(int N, int u) { return 1.0f / (N * I8(N, u)); } + +float I(int N, int u) { + if (N == 8) + return I8(N, u); + else + return D8(N, u); +} + +float D(int N, int u) { + if (N == 8) + return D8(N, u); + else + return I8(N, u); +} + +float C(int N, int n, int x) { + if (n > N) return 1.0 / C(n, N, x); + if (n == N) + return 1.0; + else + return cos(x * kPi / (2 * N)) * C(N / 2, n, x); +} + +float ScaleF(int N, int n, int x) { + return std::sqrt(n * N) * D(N, x) * I(n, x) * C(N, n, x); +} + +TEST(AcStrategyTest, TestConstant32) { + for (size_t i = 0; i < 32; i++) { + EXPECT_NEAR(D(32, i), DCTScales<32>()[i], 1e-4); + EXPECT_NEAR(I(32, i), IDCTScales<32>()[i], 1e-4); + } + for (int i = 0; i < 8; i++) { + float e1 = C(32, 8, i) - DCTResampleScales<32, 8>::kScales[i]; + float e2 = C(8, 32, i) - DCTResampleScales<8, 32>::kScales[i]; + EXPECT_LT(std::fabs(e1), 1e-4); + EXPECT_LT(std::fabs(e2), 1e-4); + } + EXPECT_NEAR(std::sqrt(8), square_root<8>::value, 1e-4); + EXPECT_NEAR(std::sqrt(16), square_root<16>::value, 1e-4); + for (int i = 0; i < 8; i++) { + float e1 = ScaleF(32, 8, i) - DCTTotalResampleScale<32, 8>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } + for (int i = 0; i < 4; i++) { + float e1 = ScaleF(32, 4, i) - DCTTotalResampleScale<32, 4>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } +} + +TEST(AcStrategyTest, TestConstant16) { + for (size_t i = 0; i < 16; i++) { + EXPECT_NEAR(D(16, i), DCTScales<16>()[i], 1e-4); + EXPECT_NEAR(I(16, i), IDCTScales<16>()[i], 1e-4); + } + for (int i = 0; i < 4; i++) { + float e1 = ScaleF(16, 4, i) - DCTTotalResampleScale<16, 4>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } + for (int i = 0; i < 2; i++) { + float e1 = ScaleF(16, 2, i) - DCTTotalResampleScale<16, 2>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } +} + +TEST(AcStrategyTest, TestConstant8) { + for (size_t i = 0; i < 8; i++) { + EXPECT_NEAR(D(8, i), DCTScales<8>()[i], 1e-4); + EXPECT_NEAR(I(8, i), IDCTScales<8>()[i], 1e-4); + } + for (int i = 0; i < 2; i++) { + float e1 = ScaleF(8, 2, i) - DCTTotalResampleScale<8, 2>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } + for (int i = 0; i < 1; i++) { + float e1 = ScaleF(8, 1, i) - DCTTotalResampleScale<8, 1>(i); + EXPECT_LT(std::fabs(e1), 1e-4); + } +} + +TEST(AcStrategyTest, DownsampleDCT) { + TestAcStrategyDownsample(AcStrategy::Type::DCT); +} +TEST(AcStrategyTest, DownsampleIDENTITY) { + TestAcStrategyDownsample(AcStrategy::Type::IDENTITY); +} +TEST(AcStrategyTest, DownsampleDCT2X2) { + TestAcStrategyDownsample(AcStrategy::Type::DCT2X2); +} +TEST(AcStrategyTest, DownsampleDCT4X4) { + TestAcStrategyDownsample(AcStrategy::Type::DCT4X4); +} +TEST(AcStrategyTest, DownsampleDCT16X16) { + TestAcStrategyDownsample(AcStrategy::Type::DCT16X16); +} +TEST(AcStrategyTest, DownsampleDCT32X32) { + TestAcStrategyDownsample(AcStrategy::Type::DCT32X32); +} +TEST(AcStrategyTest, DownsampleDCT16X8) { + TestAcStrategyDownsample(AcStrategy::Type::DCT16X8); +} +TEST(AcStrategyTest, DownsampleDCT8X16) { + TestAcStrategyDownsample(AcStrategy::Type::DCT8X16); +} +TEST(AcStrategyTest, DownsampleDCT32X8) { + TestAcStrategyDownsample(AcStrategy::Type::DCT32X8); +} +TEST(AcStrategyTest, DownsampleDCT8X32) { + TestAcStrategyDownsample(AcStrategy::Type::DCT8X32); +} +TEST(AcStrategyTest, DownsampleDCT32X16) { + TestAcStrategyDownsample(AcStrategy::Type::DCT32X16); +} +TEST(AcStrategyTest, DownsampleDCT16X32) { + TestAcStrategyDownsample(AcStrategy::Type::DCT16X32); +} +TEST(AcStrategyTest, DownsampleDCT4X8) { + TestAcStrategyDownsample(AcStrategy::Type::DCT4X8); +} +TEST(AcStrategyTest, DownsampleDCT8X4) { + TestAcStrategyDownsample(AcStrategy::Type::DCT8X4); +} +TEST(AcStrategyTest, DownsampleAFV0) { + TestAcStrategyDownsample(AcStrategy::Type::AFV0); +} +TEST(AcStrategyTest, DownsampleAFV1) { + TestAcStrategyDownsample(AcStrategy::Type::AFV1); +} + +TEST(AcStrategyTest, RoundtripDownsampleDCT) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT); +} +TEST(AcStrategyTest, RoundtripDownsampleIDENTITY) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::IDENTITY); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT2X2) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT2X2); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT4X4) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT4X4); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT16X16) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT16X16); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT32X32) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT32X32); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT16X8) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT16X8); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT8X16) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT8X16); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT32X8) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT32X8); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT8X32) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT8X32); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT32X16) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT32X16); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT16X32) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT16X32); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT4X8) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT4X8); +} +TEST(AcStrategyTest, RoundtripDownsampleDCT8X4) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::DCT8X4); +} +TEST(AcStrategyTest, RoundtripDownsampleAFV0) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::AFV0); +} +TEST(AcStrategyTest, RoundtripDownsampleAFV1) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::AFV1); +} +TEST(AcStrategyTest, RoundtripDownsampleAFV2) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::AFV2); +} +TEST(AcStrategyTest, RoundtripDownsampleAFV3) { + TestAcStrategyRoundtripDownsample(AcStrategy::Type::AFV3); +} + +TEST(AcStrategyTest, RoundtripDCT) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT); +} +TEST(AcStrategyTest, RoundtripIDENTITY) { + TestAcStrategyRoundtrip(AcStrategy::Type::IDENTITY); +} +TEST(AcStrategyTest, RoundtripDCT2X2) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT2X2); +} +TEST(AcStrategyTest, RoundtripDCT4X4) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT4X4); +} +TEST(AcStrategyTest, RoundtripDCT16X16) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT16X16); +} +TEST(AcStrategyTest, RoundtripDCT32X32) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT32X32); +} +TEST(AcStrategyTest, RoundtripDCT16X8) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT16X8); +} +TEST(AcStrategyTest, RoundtripDCT8X16) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT8X16); +} +TEST(AcStrategyTest, RoundtripDCT32X8) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT32X8); +} +TEST(AcStrategyTest, RoundtripDCT8X32) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT8X32); +} +TEST(AcStrategyTest, RoundtripDCT32X16) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT32X16); +} +TEST(AcStrategyTest, RoundtripDCT16X32) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT16X32); +} +TEST(AcStrategyTest, RoundtripDCT4X8) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT4X8); +} +TEST(AcStrategyTest, RoundtripDCT8X4) { + TestAcStrategyRoundtrip(AcStrategy::Type::DCT8X4); +} +TEST(AcStrategyTest, RoundtripAFV0) { + TestAcStrategyRoundtrip(AcStrategy::Type::AFV0); +} +TEST(AcStrategyTest, RoundtripAFV1) { + TestAcStrategyRoundtrip(AcStrategy::Type::AFV1); +} +TEST(AcStrategyTest, RoundtripAFV2) { + TestAcStrategyRoundtrip(AcStrategy::Type::AFV2); +} +TEST(AcStrategyTest, RoundtripAFV3) { + TestAcStrategyRoundtrip(AcStrategy::Type::AFV3); +} + +TEST(AcStrategyTest, RoundtripAFVDCT) { + HWY_ALIGN float idct[16]; + for (size_t i = 0; i < 16; i++) { + HWY_ALIGN float pixels[16] = {}; + pixels[i] = 1; + HWY_ALIGN float coeffs[16] = {}; + + AFVDCT4x4(pixels, coeffs); + AFVIDCT4x4(coeffs, idct); + for (size_t j = 0; j < 16; j++) { + EXPECT_NEAR(idct[j], pixels[j], 1e-6); + } + } +} + +TEST(AcStrategyTest, BenchmarkAFV) { + AcStrategy acs = AcStrategy::FromRawStrategy(AcStrategy::Type::AFV0); + HWY_ALIGN float pixels[64] = {1}; + HWY_ALIGN float coeffs[64] = {}; + for (size_t i = 0; i < 1 << 22; i++) { + acs.TransformToPixels(coeffs, pixels, 8); + acs.TransformFromPixels(pixels, 8, coeffs); + } +} + +TEST(AcStrategyTest, BenchmarkAFVDCT) { + HWY_ALIGN float pixels[64] = {1}; + HWY_ALIGN float coeffs[64] = {}; + for (size_t i = 0; i < 1 << 22; i++) { + AFVDCT4x4(pixels, coeffs); + AFVIDCT4x4(coeffs, pixels); + } +} +} // namespace +} // namespace jxl diff --git a/jxl/adaptive_reconstruction_test.cc b/jxl/adaptive_reconstruction_test.cc new file mode 100644 index 000000000..7743c8ef5 --- /dev/null +++ b/jxl/adaptive_reconstruction_test.cc @@ -0,0 +1,171 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/ac_strategy.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/common.h" +#include "jxl/epf.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/image_ops.h" +#include "jxl/image_test_utils.h" +#include "jxl/loop_filter.h" +#include "jxl/quant_weights.h" +#include "jxl/quantizer.h" + +namespace jxl { +namespace { + +constexpr bool kPrint = false; + +const size_t xsize = 8; +const size_t ysize = 8; + +void GenerateFlat(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + Image3F in(xsize, ysize); + // Plane c = foreground, all others = background. + for (size_t y = 0; y < ysize; ++y) { + float* rows[3] = {in.PlaneRow(0, y), in.PlaneRow(1, y), + in.PlaneRow(2, y)}; + for (size_t x = 0; x < xsize; ++x) { + rows[0][x] = rows[1][x] = rows[2][x] = background; + rows[c][x] = foreground; + } + } + images->push_back(std::move(in)); + } +} + +// Single foreground point at any position in any channel +void GeneratePoints(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + Image3F in(xsize, ysize); + FillImage(background, &in); + in.PlaneRow(c, y)[x] = foreground; + images->push_back(std::move(in)); + } + } + } +} + +void GenerateHorzEdges(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + // Begin of foreground rows + for (size_t y = 1; y < ysize; ++y) { + Image3F in(xsize, ysize); + FillImage(background, &in); + for (size_t iy = y; iy < ysize; ++iy) { + std::fill(in.PlaneRow(c, iy), in.PlaneRow(c, iy) + xsize, foreground); + } + images->push_back(std::move(in)); + } + } +} + +void GenerateVertEdges(const float background, const float foreground, + std::vector* images) { + for (size_t c = 0; c < Image3F::kNumPlanes; ++c) { + // Begin of foreground columns + for (size_t x = 1; x < xsize; ++x) { + Image3F in(xsize, ysize); + FillImage(background, &in); + for (size_t iy = 0; iy < ysize; ++iy) { + float* JXL_RESTRICT row = in.PlaneRow(c, iy); + for (size_t ix = x; ix < xsize; ++ix) { + row[ix] = foreground; + } + } + images->push_back(std::move(in)); + } + } +} + +// Ensures input remains unchanged by filter - verifies the edge-preserving +// nature of the filter because inputs are piecewise constant. +void EnsureUnchanged(const float background, const float foreground) { + std::vector images; + GenerateFlat(background, foreground, &images); + GeneratePoints(background, foreground, &images); + GenerateHorzEdges(background, foreground, &images); + GenerateVertEdges(background, foreground, &images); + + DequantMatrices dequant; + size_t xsize_blocks = DivCeil(xsize, kBlockDim); + size_t ysize_blocks = DivCeil(ysize, kBlockDim); + LoopFilter lf; + ImageF sigma(xsize_blocks + 4, ysize_blocks + 4); + FillImage(-0.5f, &sigma); + + Image3F storage1(xsize + 4 * kBlockDim, kEpf1InputRows); + Image3F storage2(xsize + 2 * kBlockDim, kEpf2InputRows); + lf.gab = false; + + for (size_t idx_image = 0; idx_image < images.size(); ++idx_image) { + const Image3F& in = images[idx_image]; + + if (kPrint) { + printf("--In\n"); + for (size_t y = 0; y < ysize; ++y) { + const float* row_x = in.ConstPlaneRow(0, y); + const float* row_y = in.ConstPlaneRow(1, y); + const float* row_b = in.ConstPlaneRow(2, y); + for (size_t x = 0; x < ysize; ++x) { + printf("%6.2f|%6.2f|%6.2f ", row_x[x], row_y[x], row_b[x]); + } + printf("\n"); + } + } + + Image3F out = CopyImage(in); // = in_out + Image3F padded = PadImageSymmetric(in, 2 * kBlockDim); + EdgePreservingFilter(lf, Rect(0, 0, xsize, ysize), padded, + Rect(0, 0, xsize_blocks, ysize_blocks), sigma, + Rect(0, 0, xsize, ysize), &out, &storage1, &storage2); + + if (kPrint) { + printf("--Out\n"); + for (size_t y = 0; y < ysize; ++y) { + const float* row_x = out.ConstPlaneRow(0, y); + const float* row_y = out.ConstPlaneRow(1, y); + const float* row_b = out.ConstPlaneRow(2, y); + for (size_t x = 0; x < ysize; ++x) { + printf("%6.2f|%6.2f|%6.2f ", row_x[x], row_y[x], row_b[x]); + } + printf("\n"); + } + } + + VerifyRelativeError(in, out, 1E-3, 1E-4); + } +} + +TEST(AdaptiveReconstructionTest, TestBright) { EnsureUnchanged(1.0f, 128.0f); } +TEST(AdaptiveReconstructionTest, TestDark) { EnsureUnchanged(128.0f, 1.0f); } + +} // namespace +} // namespace jxl diff --git a/jxl/ans_common.cc b/jxl/ans_common.cc new file mode 100644 index 000000000..ed965de5a --- /dev/null +++ b/jxl/ans_common.cc @@ -0,0 +1,143 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/ans_common.h" + +#include + +#include "jxl/ans_params.h" +#include "jxl/base/status.h" + +namespace jxl { + +std::vector CreateFlatHistogram(int length, int total_count) { + JXL_ASSERT(length > 0); + JXL_ASSERT(length <= total_count); + const int count = total_count / length; + std::vector result(length, count); + const int rem_counts = total_count % length; + for (int i = 0; i < rem_counts; ++i) { + ++result[i]; + } + return result; +} + +// First, all trailing non-occuring symbols are removed from the distribution; +// if this leaves the distribution empty, a dummy symbol with max weight is +// added. This ensures that the resulting distribution sums to total table size. +// Then, `entry_size` is chosen to be the largest power of two so that +// `table_size` = ANS_TAB_SIZE/`entry_size` is at least as big as the +// distribution size. +// Note that each entry will only ever contain two different symbols, and +// consecutive ranges of offsets, which allows us to use a compact +// representation. +// Each entry is initialized with only the (symbol=i, offset) pairs; then +// positions for which the entry overflows (i.e. distribution[i] > entry_size) +// or is not full are computed, and put into a stack in increasing order. +// Missing symbols in the distribution are padded with 0 (because `table_size` +// >= number of symbols). The `cutoff` value for each entry is initialized to +// the number of occupied slots in that entry (i.e. `distributions[i]`). While +// the overflowing-symbol stack is not empty (which implies that the +// underflowing-symbol stack also is not), the top overfull and underfull +// positions are popped from the stack; the empty slots in the underfull entry +// are then filled with as many slots as needed from the overfull entry; such +// slots are placed after the slots in the overfull entry, and `offsets[1]` is +// computed accordingly. The formerly underfull entry is thus now neither +// underfull nor overfull, and represents exactly two symbols. The overfull +// entry might be either overfull or underfull, and is pushed into the +// corresponding stack. +void InitAliasTable(std::vector distribution, int range, + AliasTable::Entry* JXL_RESTRICT a) { + while (!distribution.empty() && distribution.back() == 0) { + distribution.pop_back(); + } + // Ensure that a valid table is always returned, even for an empty + // alphabet. Otherwise, a specially-crafted stream might crash the + // decoder. + if (distribution.empty()) { + distribution.emplace_back(range); + } + const int kTableSizeLog = ANS_LOG_MAX_ALPHABET_SIZE; + const int kTableSize = ANS_MAX_ALPHA_SIZE; +#if JXL_ENABLE_ASSERT + int sum = std::accumulate(distribution.begin(), distribution.end(), 0); +#endif // JXL_ENABLE_ASSERT + JXL_ASSERT(sum == range); + JXL_ASSERT(distribution.size() <= (1 << kTableSizeLog)); + // range must be a power of two + JXL_ASSERT((range & (range - 1)) == 0); + JXL_ASSERT(kTableSize >= distribution.size()); + JXL_ASSERT(kTableSize <= range); + const int entry_size = range >> kTableSizeLog; // this is exact + std::vector underfull_posn; + std::vector overfull_posn; + int cutoffs[kTableSize]; + // Initialize entries. + for (size_t i = 0; i < distribution.size(); i++) { + cutoffs[i] = distribution[i]; + if (cutoffs[i] > entry_size) { + overfull_posn.push_back(i); + } else if (cutoffs[i] < entry_size) { + underfull_posn.push_back(i); + } + } + for (int i = distribution.size(); i < kTableSize; i++) { + cutoffs[i] = 0; + underfull_posn.push_back(i); + } + // Reassign overflow/underflow values. + while (!overfull_posn.empty()) { + int overfull_i = overfull_posn.back(); + overfull_posn.pop_back(); + JXL_ASSERT(!underfull_posn.empty()); + int underfull_i = underfull_posn.back(); + underfull_posn.pop_back(); + int underfull_by = entry_size - cutoffs[underfull_i]; + cutoffs[overfull_i] -= underfull_by; + // overfull positions have their original symbols + a[underfull_i].right_value = overfull_i; + a[underfull_i].offsets1 = cutoffs[overfull_i]; + // Slots in the right part of entry underfull_i were taken from the end + // of the symbols in entry overfull_i. + if (cutoffs[overfull_i] < entry_size) { + underfull_posn.push_back(overfull_i); + } else if (cutoffs[overfull_i] > entry_size) { + overfull_posn.push_back(overfull_i); + } + } + for (int i = 0; i < kTableSize; i++) { + // cutoffs[i] is properly initialized but the clang-analyzer doesn't infer + // it since it is partially initialized across two for-loops. + // NOLINTNEXTLINE(clang-analyzer-core.UndefinedBinaryOperatorResult) + if (cutoffs[i] == entry_size) { + a[i].right_value = i; + a[i].offsets1 = 0; + a[i].cutoff = 0; + } else { + // Note that, if cutoff is not equal to entry_size, + // a[i].offsets1 was initialized with (overfull cutoff) - + // (entry_size - a[i].cutoff). Thus, subtracting + // a[i].cutoff cannot make it negative. + a[i].offsets1 -= cutoffs[i]; + a[i].cutoff = cutoffs[i]; + } + const size_t freq0 = i < distribution.size() ? distribution[i] : 0; + const size_t i1 = a[i].right_value; + const size_t freq1 = i1 < distribution.size() ? distribution[i1] : 0; + a[i].freq0 = static_cast(freq0); + a[i].freq1_xor_freq0 = static_cast(freq1 ^ freq0); + } +} + +} // namespace jxl diff --git a/jxl/ans_common.h b/jxl/ans_common.h new file mode 100644 index 000000000..7f142200b --- /dev/null +++ b/jxl/ans_common.h @@ -0,0 +1,152 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_ANS_COMMON_H_ +#define JXL_ANS_COMMON_H_ + +#include + +#include +#include +#include +#include + +#include "jxl/ans_params.h" +#include "jxl/base/byte_order.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Returns the precision (number of bits) that should be used to store +// a histogram count such that Log2Floor(count) == logcount. +static JXL_INLINE uint32_t GetPopulationCountPrecision(uint32_t logcount, + uint32_t shift) { + return std::min(logcount, shift - ((ANS_LOG_TAB_SIZE - logcount) >> 1)); +} + +// Returns a histogram where the counts are positive, differ by at most 1, +// and add up to total_count. The bigger counts (if any) are at the beginning +// of the histogram. +std::vector CreateFlatHistogram(int length, int total_count); + +// An alias table implements a mapping from the [0, ANS_TAB_SIZE) range into +// the [0, ANS_MAX_ALPHABET_SIZE) range, satisfying the following conditions: +// - each symbol occurs as many times as specified by any valid distribution +// of frequencies of the symbols. A valid distribution here is an array of +// ANS_MAX_ALPHABET_SIZE that contains numbers in the range [0, ANS_TAB_SIZE], +// and whose sum is ANS_TAB_SIZE. +// - lookups can be done in constant time, and also return how many smaller +// input values map into the same symbol, according to some well-defined order +// of input values. +// - the space used by the alias table is given by a small constant times the +// index of the largest symbol with nonzero probability in the distribution. +// Each of the entries in the table covers a range of `entry_size` values in the +// [0, ANS_TAB_SIZE) range; consecutive entries represent consecutive +// sub-ranges. In the range covered by entry `i`, the first `cutoff` values map +// to symbol `i`, while the others map to symbol `right_value`. +// +// TODO(veluca): consider making the order used for computing offsets easier to +// define - it is currently defined by the algorithm to compute the alias table. +// Beware of breaking the implicit assumption that symbols that come after the +// cutoff value should have an offset at least as big as the cutoff. +static constexpr uint32_t kLogEntrySize = + ANS_LOG_TAB_SIZE - ANS_LOG_MAX_ALPHABET_SIZE; +static constexpr uint32_t kEntrySizeMinus1 = (1 << kLogEntrySize) - 1; + +struct AliasTable { + struct Symbol { + size_t value; + size_t offset; + size_t freq; + }; + +// Working set size matters here (~64 tables x 256 entries). +// offsets0 is always zero (beginning of [0] side among the same symbol). +// offsets1 is an offset of (pos >= cutoff) side decremented by cutoff. +#pragma pack(push, 1) + struct Entry { + uint8_t cutoff; // < kEntrySizeMinus1 when used by ANS. + uint8_t right_value; // < alphabet size. + uint16_t freq0; + + // Only used if `greater` (see Lookup) + uint16_t offsets1; // <= ANS_TAB_SIZE + uint16_t freq1_xor_freq0; // for branchless ternary in Lookup + }; +#pragma pack(pop) + + // Dividing `value` by `entry_size` determines `i`, the entry which is + // responsible for the input. If the remainder is below `cutoff`, then the + // mapped symbol is `i`; since `offsets[0]` stores the number of occurences of + // `i` "before" the start of this entry, the offset of the input will be + // `offsets[0] + remainder`. If the remainder is above cutoff, the mapped + // symbol is `right_value`; since `offsets[1]` stores the number of occurences + // of `right_value` "before" this entry, minus the `cutoff` value, the input + // offset is then `remainder + offsets[1]`. + static HWY_ATTR JXL_INLINE Symbol Lookup(const Entry* JXL_RESTRICT table, + size_t value) { + const size_t i = value >> kLogEntrySize; + const size_t pos = value & kEntrySizeMinus1; + +#if JXL_BYTE_ORDER_LITTLE + uint64_t entry; + memcpy(&entry, &table[i].cutoff, sizeof(entry)); + const size_t cutoff = entry & 0xFF; // = MOVZX + const size_t right_value = (entry >> 8) & 0xFF; // = MOVZX + const size_t freq0 = (entry >> 16) & 0xFFFF; +#else + // Generates multiple loads with complex addressing. + const size_t cutoff = table[i].cutoff; + const size_t right_value = table[i].right_value; + const size_t freq0 = table[i].freq0; +#endif + + const bool greater = pos >= cutoff; + +#if JXL_BYTE_ORDER_LITTLE + const uint64_t conditional = greater ? entry : 0; // = CMOV + const size_t offsets1_or_0 = (conditional >> 32) & 0xFFFF; + const size_t freq1_xor_freq0_or_0 = conditional >> 48; +#else + const size_t offsets1_or_0 = greater ? table[i].offsets1 : 0; + const size_t freq1_xor_freq0_or_0 = greater ? table[i].freq1_xor_freq0 : 0; +#endif + + // WARNING: moving this code may interfere with CMOV heuristics. + Symbol s; + s.value = greater ? right_value : i; + s.offset = offsets1_or_0 + pos; + s.freq = freq0 ^ freq1_xor_freq0_or_0; // = greater ? freq1 : freq0 + // XOR avoids implementation-defined conversion from unsigned to signed. + // Alternatives considered: BEXTR is 2 cycles on HSW, SET+shift causes + // spills, simple ternary has a long dependency chain. + + return s; + } + + static HWY_ATTR HWY_INLINE void Prefetch(const Entry* JXL_RESTRICT table, + size_t value) { + const size_t i = value >> kLogEntrySize; + hwy::Prefetch(table + i); + } +}; + +// Computes an alias table for a given distribution. +void InitAliasTable(std::vector distribution, int range, + AliasTable::Entry* JXL_RESTRICT a); + +} // namespace jxl + +#endif // JXL_ANS_COMMON_H_ diff --git a/jxl/ans_common_test.cc b/jxl/ans_common_test.cc new file mode 100644 index 000000000..e832ed4cf --- /dev/null +++ b/jxl/ans_common_test.cc @@ -0,0 +1,48 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/ans_common.h" + +#include + +#include "gtest/gtest.h" +#include "jxl/ans_params.h" + +namespace jxl { +namespace { + +HWY_ATTR void VerifyAliasDistribution(const std::vector& distribution, + int range) { + AliasTable::Entry table[ANS_MAX_ALPHA_SIZE]; + InitAliasTable(distribution, range, table); + std::vector> offsets(distribution.size()); + for (int i = 0; i < range; i++) { + AliasTable::Symbol s = AliasTable::Lookup(table, i); + offsets[s.value].push_back(s.offset); + } + for (int i = 0; i < distribution.size(); i++) { + ASSERT_EQ(distribution[i], offsets[i].size()); + std::sort(offsets[i].begin(), offsets[i].end()); + for (int j = 0; j < offsets[i].size(); j++) ASSERT_EQ(offsets[i][j], j); + } +} + +TEST(ANSCommonTest, AliasDistributionSmoke) { + VerifyAliasDistribution({ANS_TAB_SIZE / 2, ANS_TAB_SIZE / 2}, ANS_TAB_SIZE); + VerifyAliasDistribution({ANS_TAB_SIZE}, ANS_TAB_SIZE); + VerifyAliasDistribution({0, 0, 0, ANS_TAB_SIZE, 0}, ANS_TAB_SIZE); +} + +} // namespace +} // namespace jxl diff --git a/jxl/ans_params.h b/jxl/ans_params.h new file mode 100644 index 000000000..24a993675 --- /dev/null +++ b/jxl/ans_params.h @@ -0,0 +1,68 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_ANS_PARAMS_H_ +#define JXL_ANS_PARAMS_H_ + +// Common parameters that are needed for both the ANS entropy encoding and +// decoding methods. + +#include +#include + +#include "jxl/enc_params.h" + +namespace jxl { + +// TODO(veluca): decide if 12 is the best constant here (valid range is up to +// 16). This requires recomputing the Huffman tables in {enc,dec}_ans.cc +// 14 gives a 0.2% improvement at d1 and makes d8 slightly worse. This is +// likely not worth the increase in encoder complexity. +#define ANS_LOG_TAB_SIZE 12u +#define ANS_TAB_SIZE (1 << ANS_LOG_TAB_SIZE) +#define ANS_TAB_MASK (ANS_TAB_SIZE - 1) + +// 7 is sufficient for 30 bits using ReadHybridUint. Working set is proportional +// to ANS_MAX_ALPHA_SIZE, and does not fit in L2 cache for larger values. +#define ANS_LOG_MAX_ALPHABET_SIZE 7 +#define ANS_MAX_ALPHA_SIZE (1 << ANS_LOG_MAX_ALPHABET_SIZE) +#define ANS_MAX_ALPHA_MASK (ANS_MAX_ALPHA_SIZE - 1) + +#define ANS_SIGNATURE 0x13 // Initial state, used as CRC. + +struct HistogramParams { + enum class ClusteringType { + kFastest, // Cluster everything together + kFast, + kBest, + }; + + HistogramParams() = default; + + HistogramParams(SpeedTier tier, size_t num_ctx) { + if (tier == SpeedTier::kFalcon) { + clustering = ClusteringType::kFastest; + } else if (tier > SpeedTier::kTortoise) { + clustering = ClusteringType::kFast; + } else { + clustering = ClusteringType::kBest; + } + } + + ClusteringType clustering = ClusteringType::kBest; +}; + +} // namespace jxl + +#endif // JXL_ANS_PARAMS_H_ diff --git a/jxl/ans_test.cc b/jxl/ans_test.cc new file mode 100644 index 000000000..db7b06935 --- /dev/null +++ b/jxl/ans_test.cc @@ -0,0 +1,177 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/ans_params.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/span.h" +#include "jxl/dec_ans.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/enc_ans.h" +#include "jxl/enc_bit_writer.h" + +namespace jxl { +namespace { + +HWY_ATTR void RoundtripTestcase(int n_histograms, int alphabet_size, + const std::vector& input_values) { + constexpr uint16_t kMagic1 = 0x9e33; + constexpr uint16_t kMagic2 = 0x8b04; + + BitWriter writer; + // Space for magic bytes. + BitWriter::Allotment allotment_magic1(&writer, 16); + writer.Write(16, kMagic1); + ReclaimAndCharge(&writer, &allotment_magic1, 0, nullptr); + + std::vector context_map; + EntropyEncodingData codes; + std::vector> input_values_vec; + input_values_vec.push_back(input_values); + + BuildAndEncodeHistograms(HistogramParams(), n_histograms, input_values_vec, + &codes, &context_map, &writer, 0, nullptr); + WriteTokens(input_values_vec[0], codes, context_map, &writer, 0, nullptr); + + // Magic bytes + padding + BitWriter::Allotment allotment_magic2(&writer, 24); + writer.Write(16, kMagic2); + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment_magic2, 0, nullptr); + + // We do not truncate the output. Reading past the end reads out zeroes + // anyway. + BitReader br(writer.GetSpan()); + + ASSERT_EQ(br.ReadBits(16), kMagic1); + + std::vector dec_context_map; + ANSCode decoded_codes; + ASSERT_TRUE(DecodeHistograms(&br, n_histograms, ANS_MAX_ALPHA_SIZE, + &decoded_codes, &dec_context_map)); + ASSERT_EQ(dec_context_map, context_map); + ANSSymbolReader reader(&decoded_codes, &br); + + for (const Token& symbol : input_values) { + uint32_t read_symbol = + reader.ReadSymbol(dec_context_map[symbol.context], &br); + ASSERT_EQ(read_symbol, symbol.symbol); + ASSERT_EQ(br.ReadBits(symbol.nbits), symbol.bits); + } + ASSERT_TRUE(reader.CheckANSFinalState()); + + ASSERT_EQ(br.ReadBits(16), kMagic2); + EXPECT_TRUE(br.Close()); +} + +TEST(ANSTest, EmptyRoundtrip) { + RoundtripTestcase(2, ANS_MAX_ALPHA_SIZE, std::vector()); +} + +TEST(ANSTest, SingleSymbolRoundtrip) { + for (uint32_t i = 0; i < ANS_MAX_ALPHA_SIZE; i++) { + RoundtripTestcase(2, ANS_MAX_ALPHA_SIZE, {{0, i, 0, 0}}); + } + for (uint32_t i = 0; i < ANS_MAX_ALPHA_SIZE; i++) { + RoundtripTestcase(2, ANS_MAX_ALPHA_SIZE, + std::vector(1024, {0, i, 0, 0})); + } +} + +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) +constexpr size_t kReps = 10; +#else +constexpr size_t kReps = 100; +#endif + +void RoundtripRandomStream(int alphabet_size, size_t reps = kReps, + size_t num = 1 << 18) { + constexpr int kNumHistograms = 3; + std::mt19937_64 rng; + for (size_t i = 0; i < reps; i++) { + std::vector symbols; + for (size_t j = 0; j < num; j++) { + int context = std::uniform_int_distribution<>(0, kNumHistograms - 1)(rng); + int value = std::uniform_int_distribution<>(0, alphabet_size - 1)(rng); + int nbits = std::uniform_int_distribution<>(0, 16)(rng); + int bits = std::uniform_int_distribution<>(0, (1 << nbits) - 1)(rng); + symbols.emplace_back(context, value, nbits, bits); + } + RoundtripTestcase(kNumHistograms, alphabet_size, symbols); + } +} + +void RoundtripRandomUnbalancedStream(int alphabet_size) { + constexpr int kNumHistograms = 3; + constexpr int kPrecision = 1 << 10; + std::mt19937_64 rng; + for (int i = 0; i < 100; i++) { + std::vector distributions[kNumHistograms]; + for (int j = 0; j < kNumHistograms; j++) { + distributions[j].resize(kPrecision); + int symbol = 0; + int remaining = 1; + for (int k = 0; k < kPrecision; k++) { + if (remaining == 0) { + if (symbol < alphabet_size - 1) symbol++; + // There is no meaning behind this distribution: it's anything that + // will create a nonuniform distribution and won't have too few + // symbols usually. Also we want different distributions we get to be + // sufficiently dissimilar. + remaining = + std::uniform_int_distribution<>(0, (kPrecision - k) / 1)(rng); + } + distributions[j][k] = symbol; + remaining--; + } + } + std::vector symbols; + for (int j = 0; j < 1 << 18; j++) { + int context = std::uniform_int_distribution<>(0, kNumHistograms - 1)(rng); + int value = distributions[context][std::uniform_int_distribution<>( + 0, kPrecision - 1)(rng)]; + int nbits = std::uniform_int_distribution<>(0, 16)(rng); + int bits = std::uniform_int_distribution<>(0, (1 << nbits) - 1)(rng); + symbols.emplace_back(context, value, nbits, bits); + } + RoundtripTestcase(kNumHistograms + 1, alphabet_size, symbols); + } +} + +TEST(ANSTest, RandomStreamRoundtrip3Small) { RoundtripRandomStream(3, 1, 16); } + +TEST(ANSTest, RandomStreamRoundtrip3) { RoundtripRandomStream(3); } + +TEST(ANSTest, RandomStreamRoundtripBig) { + RoundtripRandomStream(ANS_MAX_ALPHA_SIZE); +} + +TEST(ANSTest, RandomUnbalancedStreamRoundtrip3) { + RoundtripRandomUnbalancedStream(3); +} + +TEST(ANSTest, RandomUnbalancedStreamRoundtripBig) { + RoundtripRandomUnbalancedStream(ANS_MAX_ALPHA_SIZE); +} + +} // namespace +} // namespace jxl diff --git a/jxl/ar_control_field.cc b/jxl/ar_control_field.cc new file mode 100644 index 000000000..2959520bf --- /dev/null +++ b/jxl/ar_control_field.cc @@ -0,0 +1,136 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/ar_control_field.h" + +#include +#include + +#include + +#include "jxl/ac_strategy.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/status.h" +#include "jxl/chroma_from_luma.h" +#include "jxl/common.h" +#include "jxl/enc_adaptive_quantization.h" +#include "jxl/enc_params.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/image_ops.h" +#include "jxl/quant_weights.h" +#include "jxl/quantizer.h" + +namespace jxl { + +void FindBestArControlField(const Image3F& opsin, PassesEncoderState* enc_state, + ThreadPool* pool) { + constexpr size_t N = kBlockDim; + const size_t xsize_blocks = enc_state->shared.frame_dim.xsize_blocks; + const size_t ysize_blocks = enc_state->shared.frame_dim.ysize_blocks; + ImageB* JXL_RESTRICT epf_sharpness = &enc_state->shared.epf_sharpness; + JXL_ASSERT(epf_sharpness->xsize() == xsize_blocks && + epf_sharpness->ysize() == ysize_blocks); + + // TODO(veluca): choose a speed tier at which the heuristics here should be + // enabled, once they are enabled at all. + + FillImage(uint8_t(4), epf_sharpness); + return; + // TODO(veluca) - the following code ends up returning only 7. + + constexpr float kChannelWeights[3] = {50.0f, 2.0f, 0.4f}; + const size_t sharpness_stride = + static_cast(epf_sharpness->PixelsPerRow()); + const size_t opsin_stride = static_cast(opsin.PixelsPerRow()); + + const float* JXL_RESTRICT in_row[3] = {opsin.ConstPlaneRow(0, 0), + opsin.ConstPlaneRow(1, 0), + opsin.ConstPlaneRow(2, 0)}; + + const auto process_row = [&](size_t by, int _) { + AcStrategyRow acs_row = enc_state->shared.ac_strategy.ConstRow(by); + uint8_t* JXL_RESTRICT out_row = epf_sharpness->Row(by); + for (size_t bx = 0; bx < xsize_blocks; bx++) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + // Find the max Laplacian in a block (for example 32x32 dct). + // Then find max Laplacian in two 4x4 pixel grids, offset by 2,2 pixels. + // The 1st max indicates amplitude of the artefacts. + // The 4x4 pixel grid Laplacian indicates masking of the artefacts. + // If masking is high or amplitude is low, then no smoothing is needed. + float maxval = 0; + float maxarray[64] = {0}; + float maxarray2[81] = {0}; + float total_abs = 0; + float total_2 = 0; + for (size_t iy = 0; iy < acs.covered_blocks_y() * N; iy++) { + const size_t cy = by * N + iy; + const size_t prevY = cy >= 1 ? cy - 1 : cy; + const size_t nextY = cy + 1 < opsin.ysize() ? cy + 1 : cy; + for (size_t ix = 0; ix < acs.covered_blocks_x() * N; ix++) { + const size_t cx = bx * N + ix; + const size_t prevX = cx >= 1 ? cx - 1 : cx; + const size_t nextX = cx + 1 < opsin.xsize() ? cx + 1 : cx; + float sum = 0; + for (size_t c = 0; c < 3; c++) { + float v = in_row[c][cy * opsin_stride + cx] - + 0.125f * (in_row[c][cy * opsin_stride + prevX] + + in_row[c][cy * opsin_stride + nextX] + + in_row[c][nextY * opsin_stride + prevX] + + in_row[c][nextY * opsin_stride + cx] + + in_row[c][nextY * opsin_stride + nextX] + + in_row[c][prevY * opsin_stride + prevX] + + in_row[c][prevY * opsin_stride + cx] + + in_row[c][prevY * opsin_stride + nextX]); + v *= kChannelWeights[c]; + sum += std::abs(v); + } + maxarray[(iy / 4) * 8 + (ix / 4)] += sum * (1.0f / 16); + maxarray2[((iy + 2) / 4) * 9 + ((ix + 2) / 4)] += sum * (1.0f / 16); + total_2 += sum * sum * + (1.0f / (N * N * acs.covered_blocks_x() * + acs.covered_blocks_y())); + total_abs += sum * (1.0f / (N * N * acs.covered_blocks_x() * + acs.covered_blocks_y())); + } + } + maxval = std::sqrt(total_2); + for (size_t iy = 0; iy < acs.covered_blocks_y(); iy++) { + for (size_t ix = 0; ix < acs.covered_blocks_x(); ix++) { + // Four 4x4 blocks for masking estimation. + const float minval = (maxarray[(2 * iy) * 8 + 2 * ix] + + maxarray[(2 * iy) * 8 + 2 * ix + 1] + + maxarray[(2 * iy + 1) * 8 + 2 * ix] + + maxarray[(2 * iy + 1) * 8 + 2 * ix + 1] + + maxarray2[(2 * iy + 1) * 9 + 2 * (ix + 1)]) * + 0.2f; + // Larger kBias, less smoothing for low intensity changes. + float kBias = 0.0005f; + float delta = (maxval + kBias) / (minval + kBias); + int sharpness1 = 7 + (delta - 0.5f) * 0.0; + int sharpness2 = 7; // maxval * 10; + out_row[bx + sharpness_stride * iy + ix] = + std::max(0, std::min(7, std::min(sharpness1, sharpness2))); + } + } + } + }; + + RunOnPool(pool, 0, ysize_blocks, ThreadPool::SkipInit(), process_row, + "AR CF"); +} + +} // namespace jxl diff --git a/jxl/ar_control_field.h b/jxl/ar_control_field.h new file mode 100644 index 000000000..7383c75fb --- /dev/null +++ b/jxl/ar_control_field.h @@ -0,0 +1,34 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_AR_CONTROL_FIELD_H_ +#define JXL_AR_CONTROL_FIELD_H_ + +#include "jxl/ac_strategy.h" +#include "jxl/base/data_parallel.h" +#include "jxl/chroma_from_luma.h" +#include "jxl/common.h" +#include "jxl/enc_cache.h" +#include "jxl/enc_params.h" +#include "jxl/image.h" +#include "jxl/quant_weights.h" + +namespace jxl { + +void FindBestArControlField(const Image3F& opsin, PassesEncoderState* enc_state, + ThreadPool* pool); + +} // namespace jxl + +#endif // JXL_AR_CONTROL_FIELD_H_ diff --git a/jxl/aux_out.cc b/jxl/aux_out.cc new file mode 100644 index 000000000..34818044a --- /dev/null +++ b/jxl/aux_out.cc @@ -0,0 +1,128 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/aux_out.h" + +#include + +#include // accumulate + +#include "jxl/aux_out_fwd.h" +#include "jxl/enc_bit_writer.h" + +namespace jxl { + +namespace { + +static const char* kBlockTypes[uint8_t(BlockType::kNumBlockTypes)] = { + "add", "black", "white"}; +} // namespace + +void AuxOut::Print(size_t num_inputs) const { + if (num_inputs == 0) return; + + LayerTotals all_layers; + for (size_t i = 0; i < layers.size(); ++i) { + all_layers.Assimilate(layers[i]); + } + + printf("Average butteraugli iters: %10.2f\n", + num_butteraugli_iters * 1.0 / num_inputs); + + for (size_t i = 0; i < layers.size(); ++i) { + if (layers[i].total_bits != 0) { + printf("Total layer bits %-10s\t", LayerName(i)); + printf("%10f%%", 100.0 * layers[i].total_bits / all_layers.total_bits); + layers[i].Print(num_inputs); + } + } + printf("Total image size "); + all_layers.Print(num_inputs); + + const uint32_t dc_pred_total = + std::accumulate(dc_pred_usage.begin(), dc_pred_usage.end(), 0u); + const uint32_t dc_pred_total_xb = + std::accumulate(dc_pred_usage_xb.begin(), dc_pred_usage_xb.end(), 0u); + if (dc_pred_total + dc_pred_total_xb != 0) { + printf("\nDC pred Y XB:\n"); + for (size_t i = 0; i < dc_pred_usage.size(); ++i) { + printf(" %6u (%5.2f%%) %6u (%5.2f%%)\n", dc_pred_usage[i], + 100.0 * dc_pred_usage[i] / dc_pred_total, dc_pred_usage_xb[i], + 100.0 * dc_pred_usage_xb[i] / dc_pred_total_xb); + } + } + + size_t total_blocks = 0; + size_t total_positions = 0; + for (uint8_t block_type = 0; block_type < uint8_t(BlockType::kNumBlockTypes); + block_type++) { + total_blocks += num_block_types[block_type]; + total_positions += num_position_types[block_type]; + } + if (total_blocks != 0 && total_positions != 0) { + printf("\n\t\t Blocks\t\tPositions\t\t\tBlocks/Position\n"); + for (uint8_t block_type = 0; + block_type < uint8_t(BlockType::kNumBlockTypes); block_type++) { + printf(" %-5s:", kBlockTypes[block_type]); + + printf( + "\t%10f%% %7zu\t %10f%% %7zu\t\t\t%10f%%\n", + 100.0 * num_block_types[block_type] / total_blocks, + num_block_types[block_type], + 100.0 * num_position_types[block_type] / total_positions, + num_position_types[block_type], + 100.0 * num_block_types[block_type] / num_position_types[block_type]); + } + printf(" Total:\t\t %7zu\t\t %7zu \t\t\t%10f%%\n\n", total_blocks, + total_positions, 100.0 * total_blocks / total_positions); + } +} + +void AuxOut::DumpCoeffImage(const char* label, + const Image3S& coeff_image) const { + JXL_ASSERT(coeff_image.xsize() % 64 == 0); + Image3S reshuffled(coeff_image.xsize() / 8, coeff_image.ysize() * 8); + for (size_t c = 0; c < 3; c++) { + for (size_t y = 0; y < coeff_image.ysize(); y++) { + for (size_t x = 0; x < coeff_image.xsize(); x += 64) { + for (size_t i = 0; i < 64; i++) { + reshuffled.PlaneRow(c, 8 * y + i / 8)[x / 8 + i % 8] = + coeff_image.PlaneRow(c, y)[x + i]; + } + } + } + } + DumpImage(label, reshuffled); +} + +void ReclaimAndCharge(BitWriter* JXL_RESTRICT writer, + BitWriter::Allotment* JXL_RESTRICT allotment, + size_t layer, AuxOut* JXL_RESTRICT aux_out) { + size_t used_bits, unused_bits; + allotment->PrivateReclaim(writer, &used_bits, &unused_bits); + +#if 0 + printf("Layer %s bits: max %zu used %zu unused %zu\n", LayerName(layer), + allotment->MaxBits(), used_bits, unused_bits); +#endif + + // This may be a nested call with aux_out == null. Whenever we know that + // aux_out is null, we can call ReclaimUnused directly. + if (aux_out != nullptr) { + aux_out->layers[layer].total_bits += used_bits; + aux_out->layers[layer].histogram_bits += allotment->HistogramBits(); + } +} + +} // namespace jxl diff --git a/jxl/aux_out.h b/jxl/aux_out.h new file mode 100644 index 000000000..0bb101703 --- /dev/null +++ b/jxl/aux_out.h @@ -0,0 +1,332 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_AUX_OUT_H_ +#define JXL_AUX_OUT_H_ + +// Optional output information for debugging and analyzing size usage. + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "jxl/aux_out_fwd.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" +#include "jxl/codec_in_out.h" +#include "jxl/color_management.h" +#include "jxl/common.h" +#include "jxl/dec_xyb.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/image_ops.h" +#include "jxl/jxl_inspection.h" + +namespace jxl { + +// For LayerName and AuxOut::layers[] index. Order does not matter. +enum { + kLayerHeader = 0, + kLayerTOC, + kLayerNoise, + kLayerQuant, + kLayerDequantTables, + kLayerOrder, + kLayerDownsampledDC, + kLayerDC, + kLayerAC, + kLayerACTokens, + kLayerDictionary, + kLayerDots, + kLayerSplines, + kLayerLossless, + kLayerModularGlobal, + kLayerModularDcGroup, + kLayerModularAcGroup, + kLayerAlpha, + kLayerDepth, + kLayerExtraChannels, + kNumImageLayers +}; + +static inline const char* LayerName(size_t layer) { + switch (layer) { + case kLayerHeader: + return "headers"; + case kLayerTOC: + return "TOC"; + case kLayerNoise: + return "noise"; + case kLayerQuant: + return "quantizer"; + case kLayerDequantTables: + return "quant tables"; + case kLayerOrder: + return "order"; + case kLayerDC: + return "DC+Group"; + case kLayerAC: + return "AC"; + case kLayerACTokens: + return "ACTokens"; + case kLayerDictionary: + return "dictionary"; + case kLayerDots: + return "dots"; + case kLayerSplines: + return "splines"; + case kLayerLossless: + return "lossless"; + case kLayerModularGlobal: + return "modularGlobal"; + case kLayerModularDcGroup: + return "modularDcGroup"; + case kLayerModularAcGroup: + return "modularAcGroup"; + case kLayerAlpha: + return "alpha"; + case kLayerDepth: + return "depth"; + case kLayerExtraChannels: + return "extra channels"; + default: + JXL_ABORT("Invalid layer %zu\n", layer); + } +} + +struct TestingAux { + Image3F* dc = nullptr; + Image3F* decoded = nullptr; +}; + +// Statistics gathered during compression or decompression. +struct AuxOut { + private: + struct LayerTotals { + void Assimilate(const LayerTotals& victim) { + num_clustered_histograms += victim.num_clustered_histograms; + histogram_bits += victim.histogram_bits; + extra_bits += victim.extra_bits; + total_bits += victim.total_bits; + clustered_entropy += victim.clustered_entropy; + } + void Print(size_t num_inputs) const { + printf("%10zd", total_bits); + if (histogram_bits != 0) { + printf(" [%6.2f %8zd %8zd %12.3f", + num_clustered_histograms * 1.0 / num_inputs, histogram_bits >> 3, + extra_bits >> 3, + (histogram_bits + clustered_entropy + extra_bits) / 8.0); + printf("]"); + } + printf("\n"); + } + size_t num_clustered_histograms = 0; + size_t extra_bits = 0; + + // Set via BitsWritten below + size_t histogram_bits = 0; + size_t total_bits = 0; + + double clustered_entropy = 0.0; + }; + + public: + AuxOut() = default; + AuxOut(const AuxOut&) = default; + + void Assimilate(const AuxOut& victim) { + for (size_t i = 0; i < layers.size(); ++i) { + layers[i].Assimilate(victim.layers[i]); + } + num_blocks += victim.num_blocks; + num_dct2_blocks += victim.num_dct2_blocks; + num_dct4_blocks += victim.num_dct4_blocks; + num_dct4x8_blocks += victim.num_dct4x8_blocks; + num_afv_blocks += victim.num_afv_blocks; + num_dct8_blocks += victim.num_dct8_blocks; + num_dct8x16_blocks += victim.num_dct8x16_blocks; + num_dct8x32_blocks += victim.num_dct8x32_blocks; + num_dct16_blocks += victim.num_dct16_blocks; + num_dct16x32_blocks += victim.num_dct16x32_blocks; + num_dct32_blocks += victim.num_dct32_blocks; + num_butteraugli_iters += victim.num_butteraugli_iters; + for (size_t i = 0; i < dc_pred_usage.size(); ++i) { + dc_pred_usage[i] += victim.dc_pred_usage[i]; + dc_pred_usage_xb[i] += victim.dc_pred_usage_xb[i]; + } + for (uint8_t block_type = 0; + block_type < uint8_t(BlockType::kNumBlockTypes); block_type++) { + num_block_types[block_type] += victim.num_block_types[block_type]; + num_position_types[block_type] += victim.num_position_types[block_type]; + } + } + + void Print(size_t num_inputs) const; + + template + void DumpImage(const char* label, const Image3& image) const { + if (!dump_image) return; + if (debug_prefix.empty()) return; + std::ostringstream pathname; + pathname << debug_prefix << label << ".png"; + CodecInOut io; + io.metadata.bits_per_sample = sizeof(T) * kBitsPerByte; + io.metadata.color_encoding = ColorManagement::SRGB(); + io.SetFromImage(StaticCastImage3(image), io.metadata.color_encoding); + (void)dump_image(io, pathname.str()); + } + template + void DumpImage(const char* label, const Plane& image) { + DumpImage(label, + Image3(CopyImage(image), CopyImage(image), CopyImage(image))); + } + + template + void DumpXybImage(const char* label, const Image3& image) const { + if (!dump_image) return; + if (debug_prefix.empty()) return; + std::ostringstream pathname; + pathname << debug_prefix << label << ".png"; + + Image3F linear(image.xsize(), image.ysize()); + OpsinParams opsin_params; + opsin_params.Init(); + OpsinToLinear(image, Rect(linear), nullptr, &linear, opsin_params); + + CodecInOut io; + io.metadata.bits_per_sample = sizeof(T) * kBitsPerByte; + io.metadata.color_encoding = ColorManagement::LinearSRGB(); + io.SetFromImage(std::move(linear), io.metadata.color_encoding); + + (void)dump_image(io, pathname.str()); + } + + // Normalizes all the channels to range 0-255, creating a false-color image + // which allows seeing the information from non-RGB channels in an RGB debug + // image. + template + void DumpImageNormalized(const char* label, const Image3& image) const { + std::array min; + std::array max; + Image3MinMax(image, &min, &max); + Image3B normalized(image.xsize(), image.ysize()); + for (size_t c = 0; c < 3; ++c) { + float mul = min[c] == max[c] ? 0 : (255.0f / (max[c] - min[c])); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* JXL_RESTRICT row_in = image.ConstPlaneRow(c, y); + uint8_t* JXL_RESTRICT row_out = normalized.PlaneRow(c, y); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = static_cast((row_in[x] - min[c]) * mul); + } + } + } + DumpImage(label, normalized); + } + + template + void DumpPlaneNormalized(const char* label, const Plane& image) const { + T min; + T max; + ImageMinMax(image, &min, &max); + Image3B normalized(image.xsize(), image.ysize()); + for (size_t c = 0; c < 3; ++c) { + float mul = min == max ? 0 : (255.0f / (max - min)); + for (size_t y = 0; y < image.ysize(); ++y) { + const T* JXL_RESTRICT row_in = image.ConstRow(y); + uint8_t* JXL_RESTRICT row_out = normalized.PlaneRow(c, y); + for (size_t x = 0; x < image.xsize(); ++x) { + row_out[x] = static_cast((row_in[x] - min) * mul); + } + } + } + DumpImage(label, normalized); + } + + // This dumps coefficients as a 16-bit PNG with coefficients of a block placed + // in the area that would contain that block in a normal image. To view the + // resulting image manually, rescale intensities by using: + // $ convert -auto-level IMAGE.PNG - | display - + void DumpCoeffImage(const char* label, const Image3S& coeff_image) const; + + void SetInspectorImage3F(const jxl::InspectorImage3F& inspector) { + inspector_image3f_ = inspector; + } + + // Allows hooking intermediate data inspection into various places of the + // processing pipeline. Returns true iff processing should proceed. + bool InspectImage3F(const char* label, const Image3F& image) { + if (inspector_image3f_ != nullptr) { + return inspector_image3f_(label, image); + } + return true; + } + + std::array layers; + size_t num_blocks = 0; + + // Number of blocks that use larger DCT (set by ac_strategy). + size_t num_dct2_blocks = 0; + size_t num_dct4_blocks = 0; + size_t num_dct4x8_blocks = 0; + size_t num_afv_blocks = 0; + size_t num_dct8_blocks = 0; + size_t num_dct8x16_blocks = 0; + size_t num_dct8x32_blocks = 0; + size_t num_dct16_blocks = 0; + size_t num_dct16x32_blocks = 0; + size_t num_dct32_blocks = 0; + + std::array dc_pred_usage = {0}; + std::array dc_pred_usage_xb = {0}; + + int num_butteraugli_iters = 0; + + // If not empty, additional debugging information (e.g. debug images) is + // saved in files with this prefix. + std::string debug_prefix; + + // By how much the decoded image was downsampled relative to the encoded + // image. + size_t downsampling = 1; + + // Number of various types of blocks + size_t num_block_types[uint8_t(BlockType::kNumBlockTypes)] = {0}; + size_t num_position_types[uint8_t(BlockType::kNumBlockTypes)] = {0}; + + jxl::InspectorImage3F inspector_image3f_; + + std::function dump_image = + nullptr; + + // WARNING: this is actually an INPUT to some code, and must be + // copy-initialized from aux_out to aux_outs. + TestingAux testing_aux; +}; + +// Used to skip image creation if they won't be written to debug directory. +static inline bool WantDebugOutput(const AuxOut* aux_out) { + // Need valid pointer and filename. + return aux_out != nullptr && !aux_out->debug_prefix.empty(); +} + +} // namespace jxl + +#endif // JXL_AUX_OUT_H_ diff --git a/jxl/aux_out_fwd.h b/jxl/aux_out_fwd.h new file mode 100644 index 000000000..8d45536c1 --- /dev/null +++ b/jxl/aux_out_fwd.h @@ -0,0 +1,37 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_AUX_OUT_FWD_H_ +#define JXL_AUX_OUT_FWD_H_ + +#include + +#include "jxl/enc_bit_writer.h" + +namespace jxl { + +struct AuxOut; + +// Helper function that ensures the `bits_written` are charged to `layer` in +// `aux_out`. Example usage: +// BitWriter::Allotment allotment(&writer, max_bits); +// writer.Write(..); writer.Write(..); +// ReclaimAndCharge(&writer, &allotment, layer, aux_out); +void ReclaimAndCharge(BitWriter* JXL_RESTRICT writer, + BitWriter::Allotment* JXL_RESTRICT allotment, + size_t layer, AuxOut* JXL_RESTRICT aux_out); + +} // namespace jxl + +#endif // JXL_AUX_OUT_FWD_H_ diff --git a/jxl/base/arch_specific.cc b/jxl/base/arch_specific.cc new file mode 100644 index 000000000..e12aa5203 --- /dev/null +++ b/jxl/base/arch_specific.cc @@ -0,0 +1,355 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/arch_specific.h" + +#include +#include +#include + +#if JXL_ARCH_X64 +#include +#if !JXL_COMPILER_MSVC +#include +#endif +#endif + +#if JXL_ARCH_PPC +#include // __ppc_get_timebase_freq +#endif + +#if JXL_ARCH_ARM +#include // sysconf +#endif + +#include // memcpy + +#include +#include +#include + +#include "jxl/base/bits.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/os_specific.h" +#include "jxl/base/status.h" + +namespace jxl { +namespace { + +#if JXL_ARCH_X64 + +// For Cpuid. +#pragma pack(push, 1) +struct Regs { + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; +}; +#pragma pack(pop) + +// Calls CPUID instruction with eax=level and ecx=count and fills `r`. +// The caller must ensure `level` <= the max supported. +void Cpuid(const uint32_t level, const uint32_t count, Regs* JXL_RESTRICT r) { +#if JXL_COMPILER_MSVC + int regs[4]; + __cpuidex(regs, level, count); + r->a = regs[0]; + r->b = regs[1]; + r->c = regs[2]; + r->d = regs[3]; +#else + // WARNING: avoid using __cpuid_count, which is broken: it lacks volatile and + // clobber "memory", so the compiler caches CPUID results, not realizing that + // CPUID:1b (APIC ID) changes across calls to SetThreadAffinity. + __asm__ __volatile__( + "xchgq %%rbx,%q1\n" + "cpuid\n" + "xchgq %%rbx,%q1" + : "=a"(r->a), "=r"(r->b), "=c"(r->c), "=d"(r->d) + : "0"(level), "2"(count) + : "memory"); +#endif +} + +class Info { + public: + Info() { + Regs r; + Cpuid(0, 0, &r); + max_func_ = r.a; + + char vendor[13]; + // Note unusual order, reverse of ModR/M encoding. + memcpy(&vendor[0], &r.b, 4); + memcpy(&vendor[4], &r.d, 4); + memcpy(&vendor[8], &r.c, 4); + vendor[12] = '\0'; + intel_ = strcmp(vendor, "GenuineIntel") == 0; + amd_ = strcmp(vendor, "AuthenticAMD") == 0; + + Cpuid(0x80000000u, 0, &r); + max_ext_func_ = r.a; + } + + uint32_t MaxFunc() const { return max_func_; } + uint32_t MaxExtFunc() const { return max_ext_func_; } + bool Intel() const { return intel_; } + bool AMD() const { return amd_; } + + std::string BrandString() const { + char brand_string[49]; + Regs r; + + // Check if brand string is supported (it is on all reasonable Intel/AMD) + if (MaxExtFunc() < 0x80000004U) return std::string(); + + for (uint32_t i = 0; i < 3; ++i) { + Cpuid(0x80000002U + i, 0, &r); + memcpy(brand_string + i * 16, &r, sizeof(r)); + } + brand_string[48] = 0; + return brand_string; + } + + private: + uint32_t max_func_; + uint32_t max_ext_func_; + bool intel_; + bool amd_; +}; + +// Detects number of packages/cores/logical processors (hyperthreads). +class X64_Topology { + public: + // Enumerates all APIC IDs and partitions them into fields, or returns false + // if the topology cannot be detected (e.g. due to missing OS support). + static Status Detect(ProcessorTopology* topology) { + const Info info; + const uint32_t core_bits = CoreBits(info); + const uint32_t logical_bits = LogicalBits(info, core_bits); + + uint32_t total_bits = 0; + // Order matters: + Field logical(logical_bits, &total_bits); + Field core(core_bits, &total_bits); + Field package(8, &total_bits); + + // Query ProcessorId on each (accessible) logical processor: + ThreadAffinity* original_affinity = GetThreadAffinity(); + for (int cpu : AvailableCPUs()) { + if (!PinThreadToCPU(cpu)) { + free(original_affinity); + return false; + } + + const uint32_t id = ProcessorId(info); + // xAPIC ID of 255 is invalid. Systems with >= 255 logical processors or + // >= 64 cores require x2APIC (Nehalem) and CPUID:11 detection, which is a + // separate codepath that we have not yet implemented. + if (id >= 255) { + JXL_WARNING("x2APIC ID (%x); TODO: implement CPUID:11", id); + } + + logical.AddValue(id); + core.AddValue(id); + package.AddValue(id); + } + JXL_CHECK(SetThreadAffinity(original_affinity)); + free(original_affinity); + + topology->logical_per_core = logical.NumValues(); + topology->cores_per_package = core.NumValues(); + topology->packages = package.NumValues(); + return true; + } + + public: + // How many bits in the xAPIC ID identify the core (per package). + // #active cores <= (1 << bits). + static uint32_t CoreBits(const Info& info) { + Regs r; + if (info.Intel()) { + if (info.MaxFunc() < 4) return 0; + Cpuid(4, 0, &r); + return static_cast(CeilLog2Nonzero((r.a >> 26) + 1)); + } + + if (info.AMD()) { + if (info.MaxExtFunc() < 0x80000008u) return 0; + Cpuid(0x80000008u, 0, &r); + return static_cast(CeilLog2Nonzero((r.c & 0xFF) + 1)); + } + + return 0; + } + + // How many bits in the xAPIC ID identify the logical processor (per core). + static uint32_t LogicalBits(const Info& info, const uint32_t core_bits) { + Regs r; + Cpuid(1, 0, &r); + + // No hyperthreading + if ((r.d & (1U << 28)) == 0) return 0; + + // Early AMD falsely claim hyperthreading + if (info.AMD() && (r.c & 2)) return 0; + + const uint32_t logical_per_package = (r.b >> 16) & 0xFF; + const uint32_t core_and_logical_bits = + static_cast(CeilLog2Nonzero(logical_per_package)); + JXL_ASSERT(core_and_logical_bits >= core_bits); + return core_and_logical_bits - core_bits; + } + + // Variable-length/position field within an xAPIC ID. Counts the total + // number of values encountered for all given id. + class Field { + public: + Field(const uint32_t bits, uint32_t* JXL_RESTRICT total_bits) + : mask_((1U << bits) - 1), shift_(*total_bits) { + *total_bits += bits; + } + + void AddValue(const uint32_t id) { values_.insert((id >> shift_) & mask_); } + + size_t NumValues() const { return values_.size(); } + + private: + const uint32_t mask_; // zero for zero-width fields + const uint32_t shift_; + std::set values_; + }; + + // Returns initial APIC ID or x2APIC ID, which uniquely identifies the current + // logical processor. Returns 0 on old CPUs. + static uint32_t ProcessorId(const Info& info) { + Regs r; + + // Support 32-bit IDs: we will only use 8 bits, but returning the full ID + // allows the caller to raise a warning when x2APIC is active. + if (info.MaxFunc() >= 11) { + Cpuid(11, 0, &r); + // Ensure we have CPUID:11 (number of enabled logical processors != 0). + if (r.b != 0) { + // r.d is a 32-bit ID; whether or not x2APIC is actually supported and + // enabled, its lower 8 bits match the initial APIC ID (CPUID:1b). + return r.d; + } + } + + // No CPUID:11 => just return initial APIC ID (8-bit). + Cpuid(1, 0, &r); + return r.b >> 24; + } +}; + +double X64_DetectNominalClockRate() { + const Info info; + const std::string& brand_string = info.BrandString(); + // Brand strings include the maximum configured frequency. These prefixes are + // defined by Intel CPUID documentation. + const char* prefixes[3] = {"MHz", "GHz", "THz"}; + const double multipliers[3] = {1E6, 1E9, 1E12}; + for (size_t i = 0; i < 3; ++i) { + const size_t pos_prefix = brand_string.find(prefixes[i]); + if (pos_prefix != std::string::npos) { + const size_t pos_space = brand_string.rfind(' ', pos_prefix - 1); + if (pos_space != std::string::npos) { + const std::string digits = + brand_string.substr(pos_space + 1, pos_prefix - pos_space - 1); + return std::stod(digits) * multipliers[i]; + } + } + } + + return 0.0; +} + +#elif JXL_ARCH_PPC + +double PPC_DetectNominalClockRate() { + double freq = -1; + char line[200]; + char* s; + char* value; + + FILE* f = fopen("/proc/cpuinfo", "r"); + if (f != nullptr) { + while (fgets(line, sizeof(line), f) != nullptr) { + // NOTE: the ':' is the only character we can rely on + if (!(value = strchr(line, ':'))) continue; + // terminate the valuename + *value++ = '\0'; + // skip any leading spaces + while (*value == ' ') value++; + if ((s = strchr(value, '\n'))) *s = '\0'; + + if (!strncasecmp(line, "clock", strlen("clock")) && + sscanf(value, "%lf", &freq) == 1) { + freq *= 1E6; + break; + } + } + fclose(f); + return freq; + } + + return 0.0; +} + +#endif // JXL_ARCH_* + +} // namespace + +Status DetectProcessorTopology(ProcessorTopology* pt) { + if (GetProcessorTopologyFromOS(pt)) return true; +#if JXL_ARCH_X64 + if (X64_Topology::Detect(pt)) return true; +#elif JXL_ARCH_ARM + // TODO(deymo): Actually look up the CPU topology and model the big/small + // core split. + pt->logical_per_core = 1; + pt->cores_per_package = sysconf(_SC_NPROCESSORS_ONLN); + pt->packages = 1; + return true; +#endif + return JXL_FAILURE("Unable to detect processor topology"); +} + +double NominalClockRate() { +// Thread-safe caching - this is called several times. +#if JXL_ARCH_X64 + static const double cycles_per_second = X64_DetectNominalClockRate(); + return cycles_per_second; +#elif JXL_ARCH_PPC + static const double cycles_per_second = PPC_DetectNominalClockRate(); + return cycles_per_second; +#else + return 0.0; +#endif +} + +double InvariantTicksPerSecond() { +#if JXL_ARCH_PPC + static const double cycles_per_second = __ppc_get_timebase_freq(); + return cycles_per_second; +#elif JXL_ARCH_X64 + return NominalClockRate(); +#else + return 1E9; // nanoseconds - matches tsc_timer.h CLOCK_MONOTONIC fallback. +#endif +} + +} // namespace jxl diff --git a/jxl/base/arch_specific.h b/jxl/base/arch_specific.h new file mode 100644 index 000000000..f480ed150 --- /dev/null +++ b/jxl/base/arch_specific.h @@ -0,0 +1,66 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_ARCH_SPECIFIC_H_ +#define JXL_BASE_ARCH_SPECIFIC_H_ + +#include +#include + +#include // kMaxVectorSize + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" + +namespace jxl { + +#if defined(__x86_64__) || defined(_M_X64) +#define JXL_ARCH_X64 1 +#else +#define JXL_ARCH_X64 0 +#endif + +#if defined(__powerpc64__) || defined(_M_PPC) +#define JXL_ARCH_PPC 1 +#else +#define JXL_ARCH_PPC 0 +#endif + +#if defined(__aarch64__) || defined(__arm__) +#define JXL_ARCH_ARM 1 +#else +#define JXL_ARCH_ARM 0 +#endif + +struct ProcessorTopology { + size_t logical_per_core = 1; + size_t cores_per_package = 1; + size_t packages = 1; +}; + +// Relatively expensive, preferably only call once. +Status DetectProcessorTopology(ProcessorTopology* pt); + +// Returns the nominal (without Turbo Boost) CPU clock rate [Hertz]. Useful for +// (roughly) characterizing the CPU speed. +double NominalClockRate(); + +// Returns tsc_timer frequency, useful for converting ticks to seconds. This is +// unaffected by CPU throttling ("invariant"). Thread-safe. Returns timebase +// frequency on PPC, NominalClockRate on X64, otherwise 1E9. +double InvariantTicksPerSecond(); + +} // namespace jxl + +#endif // JXL_BASE_ARCH_SPECIFIC_H_ diff --git a/jxl/base/bits.h b/jxl/base/bits.h new file mode 100644 index 000000000..d7e3bd1ff --- /dev/null +++ b/jxl/base/bits.h @@ -0,0 +1,149 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_BITS_H_ +#define JXL_BASE_BITS_H_ + +// Specialized instructions for processing register-sized bit arrays. + +#ifdef _MSC_VER +#include +#include +#endif + +#include +#include + +#include + +#include "jxl/base/compiler_specific.h" + +namespace jxl { + +static JXL_INLINE size_t PopCount(hwy::SizeTag<4> /* tag */, const uint32_t x) { +#ifdef _MSC_VER + return _mm_popcnt_u32(x); +#else + return static_cast(__builtin_popcount(x)); +#endif +} +static JXL_INLINE size_t PopCount(hwy::SizeTag<8> /* tag */, const uint64_t x) { +#ifdef _MSC_VER + return _mm_popcnt_u64(x); +#else + return static_cast(__builtin_popcountll(x)); +#endif +} +template +static JXL_INLINE size_t PopCount(T x) { + static_assert(!hwy::IsSigned(), "PopCount: use unsigned"); + return PopCount(hwy::SizeTag(), x); +} + +// Undefined results for x == 0. +static JXL_INLINE size_t NumZeroBitsAboveMSBNonzero(hwy::SizeTag<4> /* tag */, + const uint32_t x) { +#ifdef _MSC_VER + unsigned long index; + _BitScanReverse(&index, x); + return 31 - index; +#else + return static_cast(__builtin_clz(x)); +#endif +} +static JXL_INLINE size_t NumZeroBitsAboveMSBNonzero(hwy::SizeTag<8> /* tag */, + const uint64_t x) { +#ifdef _MSC_VER + unsigned long index; + _BitScanReverse64(&index, x); + return 63 - index; +#else + return static_cast(__builtin_clzll(x)); +#endif +} +template +static JXL_INLINE size_t NumZeroBitsAboveMSBNonzero(const T x) { + static_assert(!hwy::IsSigned(), + "NumZeroBitsAboveMSBNonzero: use unsigned"); + return NumZeroBitsAboveMSBNonzero(hwy::SizeTag(), x); +} + +// Undefined results for x == 0. +static JXL_INLINE size_t NumZeroBitsBelowLSBNonzero(hwy::SizeTag<4> /* tag */, + const uint32_t x) { +#ifdef _MSC_VER + unsigned long index; + _BitScanForward(&index, x); + return index; +#else + return static_cast(__builtin_ctz(x)); +#endif +} +static JXL_INLINE size_t NumZeroBitsBelowLSBNonzero(hwy::SizeTag<8> /* tag */, + const uint64_t x) { +#ifdef _MSC_VER + unsigned long index; + _BitScanForward64(&index, x); + return index; +#else + return static_cast(__builtin_ctzll(x)); +#endif +} +template +static JXL_INLINE size_t NumZeroBitsBelowLSBNonzero(T x) { + static_assert(!hwy::IsSigned(), + "NumZeroBitsBelowLSBNonzero: use unsigned"); + return NumZeroBitsBelowLSBNonzero(hwy::SizeTag(), x); +} + +// Returns bit width for x == 0. +template +static JXL_INLINE size_t NumZeroBitsAboveMSB(const T x) { + return (x == 0) ? sizeof(T) * 8 : NumZeroBitsAboveMSBNonzero(x); +} + +// Returns bit width for x == 0. +template +static JXL_INLINE size_t NumZeroBitsBelowLSB(const T x) { + return (x == 0) ? sizeof(T) * 8 : NumZeroBitsBelowLSBNonzero(x); +} + +// Returns base-2 logarithm, rounded down. +template +static JXL_INLINE size_t FloorLog2Nonzero(const T x) { + return (sizeof(T) * 8 - 1) ^ NumZeroBitsAboveMSBNonzero(x); +} + +// Returns base-2 logarithm, rounded up. +template +static JXL_INLINE size_t CeilLog2Nonzero(const T x) { + const size_t floor_log2 = FloorLog2Nonzero(x); + if ((x & (x - 1)) == 0) return floor_log2; // power of two + return floor_log2 + 1; +} + +// Reverses bit order. +static JXL_INLINE uint8_t FlipByte(const uint8_t x) { + // TODO(veluca): consider trying out alternative strategies, such as a single + // LUT. + static constexpr uint8_t kNibbleLut[16] = { + 0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0110, 0b1110, + 0b0001, 0b1001, 0b0101, 0b1101, 0b0011, 0b1011, 0b0111, 0b1111, + }; + return (kNibbleLut[x & 0xF] << 4) | kNibbleLut[x >> 4]; +} + +} // namespace jxl + +#endif // JXL_BASE_BITS_H_ diff --git a/jxl/base/byte_order.h b/jxl/base/byte_order.h new file mode 100644 index 000000000..34e7e9f79 --- /dev/null +++ b/jxl/base/byte_order.h @@ -0,0 +1,212 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_BYTE_ORDER_H_ +#define JXL_BASE_BYTE_ORDER_H_ + +#include +#include // memcpy + +#include "jxl/base/compiler_specific.h" + +#if JXL_COMPILER_MSVC +#include // _byteswap_* +#endif + +#if (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) +#define JXL_BYTE_ORDER_LITTLE 1 +#else +// This means that we don't know that the byte order is little endian, in +// this case we use endian-neutral code that works for both little- and +// big-endian. +#define JXL_BYTE_ORDER_LITTLE 0 +#endif + +// Returns whether the system is little-endian (least-significant byte first). +#if JXL_BYTE_ORDER_LITTLE +static constexpr bool IsLittleEndian() { return true; } +#else +static inline bool IsLittleEndian() { + const uint32_t multibyte = 1; + uint8_t byte; + memcpy(&byte, &multibyte, 1); + return byte == 1; +} +#endif + +#if JXL_COMPILER_MSVC +#define JXL_BSWAP32(x) _byteswap_ulong(x) +#define JXL_BSWAP64(x) _byteswap_uint64(x) +#else +#define JXL_BSWAP32(x) __builtin_bswap32(x) +#define JXL_BSWAP64(x) __builtin_bswap64(x) +#endif + +static JXL_INLINE uint32_t LoadBE16(const uint8_t* p) { + const uint32_t byte1 = p[0]; + const uint32_t byte0 = p[1]; + return (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadLE16(const uint8_t* p) { + const uint32_t byte0 = p[0]; + const uint32_t byte1 = p[1]; + return (byte1 << 8) | byte0; +} + +static JXL_INLINE uint32_t LoadBE32(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint32_t big; + memcpy(&big, p, 4); + return JXL_BSWAP32(big); +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint32_t byte3 = p[0]; + const uint32_t byte2 = p[1]; + const uint32_t byte1 = p[2]; + const uint32_t byte0 = p[3]; + return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE uint32_t LoadLE32(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint32_t little; + memcpy(&little, p, 4); + return little; +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint32_t byte0 = p[0]; + const uint32_t byte1 = p[1]; + const uint32_t byte2 = p[2]; + const uint32_t byte3 = p[3]; + return (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE uint64_t LoadLE64(const uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + uint64_t little; + memcpy(&little, p, 8); + return little; +#else + // Byte-order-independent - can't assume this machine is big endian. + const uint64_t byte0 = p[0]; + const uint64_t byte1 = p[1]; + const uint64_t byte2 = p[2]; + const uint64_t byte3 = p[3]; + const uint64_t byte4 = p[4]; + const uint64_t byte5 = p[5]; + const uint64_t byte6 = p[6]; + const uint64_t byte7 = p[7]; + return (byte7 << 56) | (byte6 << 48) | (byte5 << 40) | (byte4 << 32) | + (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0; +#endif +} + +static JXL_INLINE void StoreBE16(const uint32_t native, uint8_t* p) { + p[0] = (native >> 8) & 0xFF; + p[1] = native & 0xFF; +} + +static JXL_INLINE void StoreLE16(const uint32_t native, uint8_t* p) { + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +} + +static JXL_INLINE void StoreBE32(const uint32_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint32_t big = JXL_BSWAP32(native); + memcpy(p, &big, 4); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[0] = native >> 24; + p[1] = (native >> 16) & 0xFF; + p[2] = (native >> 8) & 0xFF; + p[3] = native & 0xFF; +#endif +} + +static JXL_INLINE void StoreLE32(const uint32_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint32_t little = native; + memcpy(p, &little, 4); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[3] = native >> 24; + p[2] = (native >> 16) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +#endif +} + +static JXL_INLINE void StoreLE64(const uint64_t native, uint8_t* p) { +#if JXL_BYTE_ORDER_LITTLE + const uint64_t little = native; + memcpy(p, &little, 8); +#else + // Byte-order-independent - can't assume this machine is big endian. + p[7] = native >> 56; + p[6] = (native >> 48) & 0xFF; + p[5] = (native >> 40) & 0xFF; + p[4] = (native >> 32) & 0xFF; + p[3] = (native >> 24) & 0xFF; + p[2] = (native >> 16) & 0xFF; + p[1] = (native >> 8) & 0xFF; + p[0] = native & 0xFF; +#endif +} + +// Big/Little Endian order. +struct OrderBE {}; +struct OrderLE {}; + +// Wrappers for calling from generic code. +static JXL_INLINE void Store16(OrderBE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreBE16(native, p); +} + +static JXL_INLINE void Store16(OrderLE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreLE16(native, p); +} + +static JXL_INLINE void Store32(OrderBE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreBE32(native, p); +} + +static JXL_INLINE void Store32(OrderLE /*tag*/, const uint32_t native, + uint8_t* p) { + return StoreLE32(native, p); +} + +static JXL_INLINE uint32_t Load16(OrderBE /*tag*/, const uint8_t* p) { + return LoadBE16(p); +} + +static JXL_INLINE uint32_t Load16(OrderLE /*tag*/, const uint8_t* p) { + return LoadLE16(p); +} + +static JXL_INLINE uint32_t Load32(OrderBE /*tag*/, const uint8_t* p) { + return LoadBE32(p); +} + +static JXL_INLINE uint32_t Load32(OrderLE /*tag*/, const uint8_t* p) { + return LoadLE32(p); +} + +#endif // JXL_BASE_BYTE_ORDER_H_ diff --git a/jxl/base/cache_aligned.cc b/jxl/base/cache_aligned.cc new file mode 100644 index 000000000..cc7a3ca3d --- /dev/null +++ b/jxl/base/cache_aligned.cc @@ -0,0 +1,156 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/cache_aligned.h" + +#include +#include + +// Disabled: slower than malloc + alignment. +#define JXL_USE_MMAP 0 + +#if JXL_USE_MMAP +#include +#endif + +#include // std::max +#include +#include // for kMaxVectorSize + +#include "jxl/base/status.h" + +namespace jxl { +namespace { + +#pragma pack(push, 1) +struct AllocationHeader { + void* allocated; + size_t allocated_size; + uint8_t left_padding[kMaxVectorSize]; +}; +#pragma pack(pop) + +std::atomic num_allocations{0}; +std::atomic bytes_in_use{0}; +std::atomic max_bytes_in_use{0}; + +} // namespace + +void CacheAligned::PrintStats() { + printf("Allocations: %zu (max bytes in use: %E)\n", + size_t(num_allocations.load(std::memory_order_relaxed)), + double(max_bytes_in_use.load(std::memory_order_relaxed))); +} + +size_t CacheAligned::NextOffset() { + static std::atomic next{0}; + constexpr uint32_t kGroups = CacheAligned::kAlias / CacheAligned::kAlignment; + const uint32_t group = next.fetch_add(1, std::memory_order_relaxed) % kGroups; + return CacheAligned::kAlignment * group; +} + +void* CacheAligned::Allocate(const size_t payload_size, size_t offset) { + JXL_ASSERT(payload_size <= std::numeric_limits::max() / 2); + JXL_ASSERT((offset % kAlignment == 0) && offset <= kAlias); + + // What: | misalign | unused | AllocationHeader |payload + // Size: |<= kAlias | offset | |payload_size + // ^allocated.^aligned.^header............^payload + // The header must immediately precede payload, which must remain aligned. + // To avoid wasting space, the header resides at the end of `unused`, + // which therefore cannot be empty (offset == 0). + if (offset == 0) { + offset = kAlignment; // = round_up(sizeof(AllocationHeader), kAlignment) + static_assert(sizeof(AllocationHeader) <= kAlignment, "Else: round up"); + } + +#if JXL_USE_MMAP + const size_t allocated_size = offset + payload_size; + const int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE; + void* allocated = + mmap(nullptr, allocated_size, PROT_READ | PROT_WRITE, flags, -1, 0); + if (allocated == MAP_FAILED) return nullptr; + const uintptr_t aligned = reinterpret_cast(allocated); +#else + const size_t allocated_size = kAlias + offset + payload_size; + void* allocated = malloc(allocated_size); + if (allocated == nullptr) return nullptr; + // Always round up even if already aligned - we already asked for kAlias + // extra bytes and there's no way to give them back. + uintptr_t aligned = reinterpret_cast(allocated) + kAlias; + static_assert((kAlias & (kAlias - 1)) == 0, "kAlias must be a power of 2"); + static_assert(kAlias >= kAlignment, "Cannot align to more than kAlias"); + aligned &= ~(kAlias - 1); +#endif + +#if 0 + // No effect. + uintptr_t page_aligned = reinterpret_cast(allocated); + page_aligned &= ~(4096 - 1); + if (madvise(reinterpret_cast(page_aligned), allocated_size, + MADV_WILLNEED) != 0) { + JXL_NOTIFY_ERROR("madvise failed"); + } +#elif 0 + // INCREASES both first and subsequent decode times. + if (mlock(allocated, allocated_size) != 0) { + JXL_NOTIFY_ERROR("mlock failed"); + } +#endif + + // Update statistics (#allocations and max bytes in use) + num_allocations.fetch_add(1, std::memory_order_relaxed); + const uint64_t prev_bytes = + bytes_in_use.fetch_add(allocated_size, std::memory_order_acq_rel); + uint64_t expected_max = max_bytes_in_use.load(std::memory_order_acquire); + for (;;) { + const uint64_t desired = + std::max(expected_max, prev_bytes + allocated_size); + if (max_bytes_in_use.compare_exchange_strong(expected_max, desired, + std::memory_order_acq_rel)) { + break; + } + } + + const uintptr_t payload = aligned + offset; // still aligned + + // Stash `allocated` and payload_size inside header for use by Free(). + AllocationHeader* header = reinterpret_cast(payload) - 1; + header->allocated = allocated; + header->allocated_size = allocated_size; + + return JXL_ASSUME_ALIGNED(reinterpret_cast(payload), 64); +} + +void CacheAligned::Free(const void* aligned_pointer) { + if (aligned_pointer == nullptr) { + return; + } + const uintptr_t payload = reinterpret_cast(aligned_pointer); + JXL_ASSERT(payload % kAlignment == 0); + const AllocationHeader* header = + reinterpret_cast(payload) - 1; + + // Subtract (2's complement negation). + bytes_in_use.fetch_add(~header->allocated_size + 1, + std::memory_order_acq_rel); + +#if JXL_USE_MMAP + munmap(header->allocated, header->allocated_size); +#else + free(header->allocated); +#endif +} + +} // namespace jxl diff --git a/jxl/base/cache_aligned.h b/jxl/base/cache_aligned.h new file mode 100644 index 000000000..e448d1a00 --- /dev/null +++ b/jxl/base/cache_aligned.h @@ -0,0 +1,84 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_CACHE_ALIGNED_H_ +#define JXL_BASE_CACHE_ALIGNED_H_ + +// Memory allocator with support for alignment + misalignment. + +#include +#include + +#include + +#include "jxl/base/arch_specific.h" +#include "jxl/base/compiler_specific.h" + +namespace jxl { + +// Functions that depend on the cache line size. +class CacheAligned { + public: + static void PrintStats(); + + static constexpr size_t kPointerSize = sizeof(void*); + static constexpr size_t kCacheLineSize = 64; + // To avoid RFOs, match L2 fill size (pairs of lines). + static constexpr size_t kAlignment = 2 * kCacheLineSize; + // Minimum multiple for which cache set conflicts and/or loads blocked by + // preceding stores can occur. + static constexpr size_t kAlias = 2048; + + // Returns a 'random' (cyclical) offset suitable for Allocate. + static size_t NextOffset(); + + // Returns null or memory whose address is congruent to `offset` (mod kAlias). + // This reduces cache conflicts and load/store stalls, especially with large + // allocations that would otherwise have similar alignments. At least + // `payload_size` (which can be zero) bytes will be accessible. + static void* Allocate(size_t payload_size, size_t offset); + + static void* Allocate(const size_t payload_size) { + return Allocate(payload_size, NextOffset()); + } + + static void Free(const void* aligned_pointer); +}; + +// Avoids the need for a function pointer (deleter) in CacheAlignedUniquePtr. +struct CacheAlignedDeleter { + void operator()(uint8_t* aligned_pointer) const { + return CacheAligned::Free(aligned_pointer); + } +}; + +using CacheAlignedUniquePtr = std::unique_ptr; + +// Does not invoke constructors. +static inline CacheAlignedUniquePtr AllocateArray(const size_t bytes) { + return CacheAlignedUniquePtr( + static_cast(CacheAligned::Allocate(bytes)), + CacheAlignedDeleter()); +} + +static inline CacheAlignedUniquePtr AllocateArray(const size_t bytes, + const size_t offset) { + return CacheAlignedUniquePtr( + static_cast(CacheAligned::Allocate(bytes, offset)), + CacheAlignedDeleter()); +} + +} // namespace jxl + +#endif // JXL_BASE_CACHE_ALIGNED_H_ diff --git a/jxl/base/compiler_specific.h b/jxl/base/compiler_specific.h new file mode 100644 index 000000000..fac035b72 --- /dev/null +++ b/jxl/base/compiler_specific.h @@ -0,0 +1,144 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_COMPILER_SPECIFIC_H_ +#define JXL_BASE_COMPILER_SPECIFIC_H_ + +// Macros for compiler version + nonstandard keywords, e.g. __builtin_expect. + +#include + +// #if is shorter and safer than #ifdef. *_VERSION are zero if not detected, +// otherwise 100 * major + minor version. Note that other packages check for +// #ifdef COMPILER_MSVC, so we cannot use that same name. + +#ifdef _MSC_VER +#define JXL_COMPILER_MSVC _MSC_VER +#else +#define JXL_COMPILER_MSVC 0 +#endif + +#ifdef __GNUC__ +#define JXL_COMPILER_GCC (__GNUC__ * 100 + __GNUC_MINOR__) +#else +#define JXL_COMPILER_GCC 0 +#endif + +#ifdef __clang__ +#define JXL_COMPILER_CLANG (__clang_major__ * 100 + __clang_minor__) +// Clang pretends to be GCC for compatibility. +#undef JXL_COMPILER_GCC +#define JXL_COMPILER_GCC 0 +#else +#define JXL_COMPILER_CLANG 0 +#endif + +#if JXL_COMPILER_MSVC +#define JXL_RESTRICT __restrict +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_RESTRICT __restrict__ +#else +#define JXL_RESTRICT +#endif + +#if JXL_COMPILER_MSVC +#define JXL_INLINE __forceinline +#define JXL_NOINLINE __declspec(noinline) +#else +#define JXL_INLINE inline __attribute__((always_inline)) +#define JXL_NOINLINE __attribute__((noinline)) +#endif + +#if JXL_COMPILER_MSVC +#define JXL_NORETURN __declspec(noreturn) +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_NORETURN __attribute__((noreturn)) +#endif + +#if JXL_COMPILER_MSVC +#define JXL_UNREACHABLE __assume(false) +#elif JXL_COMPILER_CLANG || JXL_COMPILER_GCC >= 405 +#define JXL_UNREACHABLE __builtin_unreachable() +#else +#define JXL_UNREACHABLE +#endif + +#if JXL_COMPILER_MSVC +// Unsupported, __assume is not the same. +#define JXL_LIKELY(expr) expr +#define JXL_UNLIKELY(expr) expr +#else +#define JXL_LIKELY(expr) __builtin_expect(!!(expr), 1) +#define JXL_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#endif + +#if JXL_COMPILER_MSVC +#include + +#pragma intrinsic(_ReadWriteBarrier) +#define JXL_COMPILER_FENCE _ReadWriteBarrier() +#elif JXL_COMPILER_GCC || JXL_COMPILER_CLANG +#define JXL_COMPILER_FENCE asm volatile("" : : : "memory") +#else +#define JXL_COMPILER_FENCE +#endif + +// Returns a void* pointer which the compiler then assumes is N-byte aligned. +// Example: float* JXL_RESTRICT aligned = (float*)JXL_ASSUME_ALIGNED(in, 32); +// +// The assignment semantics are required by GCC/Clang. ICC provides an in-place +// __assume_aligned, whereas MSVC's __assume appears unsuitable. +#if JXL_COMPILER_CLANG +// Early versions of Clang did not support __builtin_assume_aligned. +#define JXL_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) +#elif JXL_COMPILER_GCC +#define JXL_HAS_ASSUME_ALIGNED 1 +#else +#define JXL_HAS_ASSUME_ALIGNED 0 +#endif + +#if JXL_HAS_ASSUME_ALIGNED +#define JXL_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) +#else +#define JXL_ASSUME_ALIGNED(ptr, align) (ptr) /* not supported */ +#endif + +#ifdef __has_attribute +#define JXL_HAVE_ATTRIBUTE(x) __has_attribute(x) +#else +#define JXL_HAVE_ATTRIBUTE(x) 0 +#endif + +// Raises warnings if the function return value is unused. Should appear as the +// first part of a function definition/declaration. +#if JXL_HAVE_ATTRIBUTE(nodiscard) +#define JXL_MUST_USE_RESULT [[nodiscard]] +#elif JXL_COMPILER_CLANG && JXL_HAVE_ATTRIBUTE(warn_unused_result) +#define JXL_MUST_USE_RESULT __attribute__((warn_unused_result)) +#else +#define JXL_MUST_USE_RESULT +#endif + +#if JXL_HAVE_ATTRIBUTE(__format__) +#define JXL_FORMAT(idx_fmt, idx_arg) \ + __attribute__((__format__(__printf__, idx_fmt, idx_arg))) +#else +#define JXL_FORMAT(idx_fmt, idx_arg) +#endif + +#if JXL_COMPILER_MSVC +using ssize_t = intptr_t; +#endif + +#endif // JXL_BASE_COMPILER_SPECIFIC_H_ diff --git a/jxl/base/data_parallel.cc b/jxl/base/data_parallel.cc new file mode 100644 index 000000000..3020e2b9a --- /dev/null +++ b/jxl/base/data_parallel.cc @@ -0,0 +1,46 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/data_parallel.h" + +#define DATA_PARALLEL_TRACE 0 + +namespace jxl { + +// static +JpegxlParallelRetCode ThreadPool::SequentialRunnerStatic( + void* runner_opaque, void* jpegxl_opaque, JpegxlParallelRunInit init, + JpegxlParallelRunFunction func, uint32_t start_range, uint32_t end_range) { + JpegxlParallelRetCode init_ret = (*init)(jpegxl_opaque, 1); + if (init_ret != 0) return init_ret; + + for (uint32_t i = start_range; i < end_range; i++) { + (*func)(jpegxl_opaque, i, 0); + } + return 0; +} + +#if DATA_PARALLEL_TRACE +void TraceRunBegin(const char* /*caller*/, double* t0) { *t0 = Now(); } + +void TraceRunEnd(const char* caller, double t0) { + const double elapsed = Now() - t0; + fprintf(stderr, "%27s: %5.1f ms\n", caller, elapsed * 1E3); +} +#else +void TraceRunBegin(const char* /*caller*/, double* /*t0*/) {} +void TraceRunEnd(const char* /*caller*/, double /*t0*/) {} +#endif + +} // namespace jxl diff --git a/jxl/base/data_parallel.h b/jxl/base/data_parallel.h new file mode 100644 index 000000000..957ee1aec --- /dev/null +++ b/jxl/base/data_parallel.h @@ -0,0 +1,170 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_DATA_PARALLEL_H_ +#define JXL_BASE_DATA_PARALLEL_H_ + +// Portable, low-overhead C++11 ThreadPool alternative to OpenMP for +// data-parallel computations. + +#include +#include + +#include "include/jpegxl/parallel_runner.h" +#include "jxl/base/bits.h" +#include "jxl/base/status.h" + +namespace jxl { + +class ThreadPool { + public: + // Use this type as an InitFunc to skip the initialization step in Run(). + // When this is used the return value of Run() is always true and does not + // need to be checked. + struct SkipInit {}; + + ThreadPool(JpegxlParallelRunner runner, void* runner_opaque) + : runner_(runner ? runner : &ThreadPool::SequentialRunnerStatic), + runner_opaque_(runner ? runner_opaque : static_cast(this)) {} + + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator&(const ThreadPool&) = delete; + + // Runs init_func(num_threads) followed by data_func(task, thread) on worker + // thread(s) for every task in [begin, end). init_func() must return a Status + // indicating whether the initialization succeeded. + // "thread" is an integer smaller than num_threads. + // Not thread-safe - no two calls to Run may overlap. + // Subsequent calls will reuse the same threads. + // + // Precondition: begin <= end. + template + Status Run(uint32_t begin, uint32_t end, const InitFunc& init_func, + const DataFunc& data_func, const char* caller = "") { + JXL_ASSERT(begin <= end); + if (begin == end) return true; + RunCallState call_state(init_func, data_func); + // The runner_ uses the C convention and returns 0 in case of error, so we + // convert it to an Status. + return (*runner_)(runner_opaque_, static_cast(&call_state), + &call_state.CallInitFunc, &call_state.CallDataFunc, begin, + end) == 0; + } + + // Specialization that returns bool when SkipInit is used. + template + bool Run(uint32_t begin, uint32_t end, const SkipInit /* tag */, + const DataFunc& data_func, const char* caller = "") { + return Run(begin, end, ReturnTrueInit, data_func, caller); + } + + private: + static Status ReturnTrueInit(size_t num_threads) { return true; } + + // class holding the state of a Run() call to pass to the runner_ as an + // opaque_jpegxl pointer. + template + class RunCallState final { + public: + RunCallState(const InitFunc& init_func, const DataFunc& data_func) + : init_func_(init_func), data_func_(data_func) {} + + // JpegxlParallelRunInit interface. + static int CallInitFunc(void* jpegxl_opaque, size_t num_threads) { + const auto* self = + static_cast*>(jpegxl_opaque); + // Returns -1 when the internal init function returns false Status to + // indicate an error. + return self->init_func_(num_threads) ? 0 : -1; + } + + // JpegxlParallelRunFunction interface. + static void CallDataFunc(void* jpegxl_opaque, uint32_t value, + size_t thread_id) { + const auto* self = + static_cast*>(jpegxl_opaque); + return self->data_func_(value, thread_id); + } + + private: + const InitFunc& init_func_; + const DataFunc& data_func_; + }; + + // Default JpegxlParallelRunner used when no runner is provided by the + // caller. This runner doesn't use any threading and thread_id is always 0. + static JpegxlParallelRetCode SequentialRunnerStatic( + void* runner_opaque, void* jpegxl_opaque, JpegxlParallelRunInit init, + JpegxlParallelRunFunction func, uint32_t start_range, uint32_t end_range); + + // The caller supplied runner function and its opaque void*. + const JpegxlParallelRunner runner_; + void* const runner_opaque_; +}; + +void TraceRunBegin(const char* caller, double* t0); +void TraceRunEnd(const char* caller, double t0); + +// TODO(deymo): Convert the return value to a Status when not using SkipInit. +template +bool RunOnPool(ThreadPool* pool, const uint32_t begin, const uint32_t end, + const InitFunc& init_func, const DataFunc& data_func, + const char* caller) { + Status ret = true; + double t0; + TraceRunBegin(caller, &t0); + if (pool == nullptr) { + ThreadPool default_pool(nullptr, nullptr); + ret = default_pool.Run(begin, end, init_func, data_func, caller); + } else { + ret = pool->Run(begin, end, init_func, data_func, caller); + } + TraceRunEnd(caller, t0); + return ret; +} + +// Accelerates multiple unsigned 32-bit divisions with the same divisor by +// precomputing a multiplier. This is useful for splitting a contiguous range of +// indices (the task index) into 2D indices. Exhaustively tested on dividends +// up to 4M with non-power of two divisors up to 2K. +class Divider { + public: + // "d" is the divisor (what to divide by). + explicit Divider(const uint32_t d) : shift_(FloorLog2Nonzero(d)) { + // Power of two divisors (including 1) are not supported because it is more + // efficient to special-case them at a higher level. + JXL_ASSERT((d & (d - 1)) != 0); + + // ceil_log2 = floor_log2 + 1 because we ruled out powers of two above. + const uint64_t next_pow2 = 1ULL << (shift_ + 1); + + mul_ = ((next_pow2 - d) << 32) / d + 1; + } + + // "n" is the numerator (what is being divided). + inline uint32_t operator()(const uint32_t n) const { + // Algorithm from "Division by Invariant Integers using Multiplication". + // Its "sh1" is hardcoded to 1 because we don't need to handle d=1. + const uint32_t hi = (uint64_t(mul_) * n) >> 32; + return (hi + ((n - hi) >> 1)) >> shift_; + } + + private: + uint32_t mul_; + const int shift_; +}; + +} // namespace jxl + +#endif // JXL_BASE_DATA_PARALLEL_H_ diff --git a/jxl/base/descriptive_statistics.cc b/jxl/base/descriptive_statistics.cc new file mode 100644 index 000000000..7f3a74f92 --- /dev/null +++ b/jxl/base/descriptive_statistics.cc @@ -0,0 +1,111 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/descriptive_statistics.h" + +#include + +#include "jxl/base/status.h" + +namespace jxl { + +void Stats::Assimilate(const Stats& other) { + const int64_t total_n = n_ + other.n_; + if (total_n == 0) return; // Nothing to do; prevents div by zero. + + min_ = std::min(min_, other.min_); + max_ = std::max(max_, other.max_); + + product_ *= other.product_; + + const double product_n = n_ * other.n_; + const double n2 = n_ * n_; + const double other_n2 = other.n_ * other.n_; + // Warning: multiplying int64 can overflow here. + const double total_n2 = static_cast(total_n) * total_n; + const double total_n3 = static_cast(total_n2) * total_n; + // Precompute reciprocal for speed - used at least twice. + const double inv_total_n = 1.0 / total_n; + const double inv_total_n2 = 1.0 / total_n2; + + const double delta = other.m1_ - m1_; + const double delta2 = delta * delta; + const double delta3 = delta * delta2; + const double delta4 = delta2 * delta2; + + m1_ = (n_ * m1_ + other.n_ * other.m1_) * inv_total_n; + + const double new_m2 = m2_ + other.m2_ + delta2 * product_n * inv_total_n; + + const double new_m3 = + m3_ + other.m3_ + delta3 * product_n * (n_ - other.n_) * inv_total_n2 + + 3.0 * delta * (n_ * other.m2_ - other.n_ * m2_) * inv_total_n; + + m4_ += other.m4_ + + delta4 * product_n * (n2 - product_n + other_n2) / total_n3 + + 6.0 * delta2 * (n2 * other.m2_ + other_n2 * m2_) * inv_total_n2 + + 4.0 * delta * (n_ * other.m3_ - other.n_ * m3_) * inv_total_n; + + m2_ = new_m2; + m3_ = new_m3; + n_ = total_n; +} + +std::string Stats::ToString(int exclude) const { + if (Count() == 0) return std::string("(none)"); + + char buf[300]; + size_t pos = 0; + int ret; // snprintf - bytes written or negative for error. + + if ((exclude & kNoCount) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Count=%6zu ", + static_cast(Count())); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoMeanSD) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Mean=%9.6f SD=%8.5f ", Mean(), + StandardDeviation()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoMinMax) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Min=%8.5f Max=%8.5f ", Min(), + Max()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoSkewKurt) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "Skew=%5.2f Kurt=%7.2f ", + Skewness(), Kurtosis()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + if ((exclude & kNoGeomean) == 0) { + ret = snprintf(buf + pos, sizeof(buf) - pos, "GeoMean=%9.6f ", + GeometricMean()); + JXL_ASSERT(ret > 0); + pos += ret; + } + + JXL_ASSERT(pos < sizeof(buf)); + return buf; +} + +} // namespace jxl diff --git a/jxl/base/descriptive_statistics.h b/jxl/base/descriptive_statistics.h new file mode 100644 index 000000000..f116440bc --- /dev/null +++ b/jxl/base/descriptive_statistics.h @@ -0,0 +1,135 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_DESCRIPTIVE_STATISTICS_H_ +#define JXL_BASE_DESCRIPTIVE_STATISTICS_H_ + +// For analyzing the range/distribution of scalars. + +#include + +#include +#include +#include + +namespace jxl { + +// Descriptive statistics of a variable (4 moments). +class Stats { + public: + void Notify(const float x) { + ++n_; + + min_ = std::min(min_, x); + max_ = std::max(max_, x); + + product_ *= x; + + // Online moments. Reference: https://goo.gl/9ha694 + const double d = x - m1_; + const double d_div_n = d / n_; + const double d2n1_div_n = d * (n_ - 1) * d_div_n; + const int64_t n_poly = n_ * n_ - 3 * n_ + 3; + m1_ += d_div_n; + m4_ += d_div_n * (d_div_n * (d2n1_div_n * n_poly + 6.0 * m2_) - 4.0 * m3_); + m3_ += d_div_n * (d2n1_div_n * (n_ - 2) - 3.0 * m2_); + m2_ += d2n1_div_n; + } + + void Assimilate(const Stats& other); + + int64_t Count() const { return n_; } + + float Min() const { return min_; } + float Max() const { return max_; } + + double GeometricMean() const { + return n_ == 0 ? 0.0 : pow(product_, 1.0 / n_); + } + + double Mean() const { return m1_; } + // Same as Mu2. Assumes n_ is large. + double SampleVariance() const { + return n_ == 0 ? 0.0 : m2_ / static_cast(n_); + } + // Unbiased estimator for population variance even for smaller n_. + double Variance() const { + if (n_ == 0) return 0.0; + if (n_ == 1) return m2_; + return m2_ / static_cast(n_ - 1); + } + double StandardDeviation() const { return std::sqrt(Variance()); } + // Near zero for normal distributions; if positive on a unimodal distribution, + // the right tail is fatter. Assumes n_ is large. + double SampleSkewness() const { + if (std::abs(m2_) < 1E-7) return 0.0; + return m3_ * std::sqrt(static_cast(n_)) / std::pow(m2_, 1.5); + } + // Corrected for bias (same as Wikipedia and Minitab but not Excel). + double Skewness() const { + if (n_ == 0) return 0.0; + const double biased = SampleSkewness(); + const double r = (n_ - 1.0) / n_; + return biased * std::pow(r, 1.5); + } + // Near zero for normal distributions; smaller values indicate fewer/smaller + // outliers and larger indicates more/larger outliers. Assumes n_ is large. + double SampleKurtosis() const { + if (std::abs(m2_) < 1E-7) return 0.0; + return m4_ * n_ / (m2_ * m2_); + } + // Corrected for bias (same as Wikipedia and Minitab but not Excel). + double Kurtosis() const { + if (n_ == 0) return 0.0; + const double biased = SampleKurtosis(); + const double r = (n_ - 1.0) / n_; + return biased * r * r; + } + + // Central moments, useful for "method of moments"-based parameter estimation + // of a mixture of two Gaussians. Assumes Count() != 0. + double Mu1() const { return m1_; } + double Mu2() const { return m2_ / static_cast(n_); } + double Mu3() const { return m3_ / static_cast(n_); } + double Mu4() const { return m4_ / static_cast(n_); } + + // Which statistics to EXCLUDE in ToString + enum { + kNoCount = 1, + kNoMeanSD = 2, + kNoMinMax = 4, + kNoSkewKurt = 8, + kNoGeomean = 16 + }; + + std::string ToString(int exclude = 0) const; + + private: + int64_t n_ = 0; // signed for faster conversion + safe subtraction + + float min_ = 1E30f; + float max_ = -1E30f; + + double product_ = 1.0; + + // Moments + double m1_ = 0.0; + double m2_ = 0.0; + double m3_ = 0.0; + double m4_ = 0.0; +}; + +} // namespace jxl + +#endif // JXL_BASE_DESCRIPTIVE_STATISTICS_H_ diff --git a/jxl/base/fast_log.h b/jxl/base/fast_log.h new file mode 100644 index 000000000..2f85efc8d --- /dev/null +++ b/jxl/base/fast_log.h @@ -0,0 +1,133 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_FAST_LOG_H_ +#define JXL_BASE_FAST_LOG_H_ + +#include + +#include + +namespace jxl { + +// A lookup table for small values of log2(int) to be used in entropy +// computation. +// +// ", ".join(["%.16ff" % x for x in [0.0]+[log2(x) for x in range(1, 256)]]) +static const float kLog2Table[] = { + 0.0000000000000000f, 0.0000000000000000f, 1.0000000000000000f, + 1.5849625007211563f, 2.0000000000000000f, 2.3219280948873622f, + 2.5849625007211561f, 2.8073549220576042f, 3.0000000000000000f, + 3.1699250014423126f, 3.3219280948873626f, 3.4594316186372978f, + 3.5849625007211565f, 3.7004397181410922f, 3.8073549220576037f, + 3.9068905956085187f, 4.0000000000000000f, 4.0874628412503400f, + 4.1699250014423122f, 4.2479275134435852f, 4.3219280948873626f, + 4.3923174227787607f, 4.4594316186372973f, 4.5235619560570131f, + 4.5849625007211570f, 4.6438561897747244f, 4.7004397181410926f, + 4.7548875021634691f, 4.8073549220576037f, 4.8579809951275728f, + 4.9068905956085187f, 4.9541963103868758f, 5.0000000000000000f, + 5.0443941193584534f, 5.0874628412503400f, 5.1292830169449664f, + 5.1699250014423122f, 5.2094533656289501f, 5.2479275134435852f, + 5.2854022188622487f, 5.3219280948873626f, 5.3575520046180838f, + 5.3923174227787607f, 5.4262647547020979f, 5.4594316186372973f, + 5.4918530963296748f, 5.5235619560570131f, 5.5545888516776376f, + 5.5849625007211570f, 5.6147098441152083f, 5.6438561897747244f, + 5.6724253419714961f, 5.7004397181410926f, 5.7279204545631996f, + 5.7548875021634691f, 5.7813597135246599f, 5.8073549220576046f, + 5.8328900141647422f, 5.8579809951275719f, 5.8826430493618416f, + 5.9068905956085187f, 5.9307373375628867f, 5.9541963103868758f, + 5.9772799234999168f, 6.0000000000000000f, 6.0223678130284544f, + 6.0443941193584534f, 6.0660891904577721f, 6.0874628412503400f, + 6.1085244567781700f, 6.1292830169449672f, 6.1497471195046822f, + 6.1699250014423122f, 6.1898245588800176f, 6.2094533656289510f, + 6.2288186904958804f, 6.2479275134435861f, 6.2667865406949019f, + 6.2854022188622487f, 6.3037807481771031f, 6.3219280948873617f, + 6.3398500028846252f, 6.3575520046180847f, 6.3750394313469254f, + 6.3923174227787598f, 6.4093909361377026f, 6.4262647547020979f, + 6.4429434958487288f, 6.4594316186372982f, 6.4757334309663976f, + 6.4918530963296748f, 6.5077946401986964f, 6.5235619560570131f, + 6.5391588111080319f, 6.5545888516776376f, 6.5698556083309478f, + 6.5849625007211561f, 6.5999128421871278f, 6.6147098441152092f, + 6.6293566200796095f, 6.6438561897747253f, 6.6582114827517955f, + 6.6724253419714952f, 6.6865005271832185f, 6.7004397181410917f, + 6.7142455176661224f, 6.7279204545631988f, 6.7414669864011465f, + 6.7548875021634691f, 6.7681843247769260f, 6.7813597135246599f, + 6.7944158663501062f, 6.8073549220576037f, 6.8201789624151887f, + 6.8328900141647422f, 6.8454900509443757f, 6.8579809951275719f, + 6.8703647195834048f, 6.8826430493618416f, 6.8948177633079437f, + 6.9068905956085187f, 6.9188632372745955f, 6.9307373375628867f, + 6.9425145053392399f, 6.9541963103868758f, 6.9657842846620879f, + 6.9772799234999168f, 6.9886846867721664f, 7.0000000000000000f, + 7.0112272554232540f, 7.0223678130284544f, 7.0334230015374501f, + 7.0443941193584534f, 7.0552824355011898f, 7.0660891904577721f, + 7.0768155970508317f, 7.0874628412503400f, 7.0980320829605272f, + 7.1085244567781700f, 7.1189410727235076f, 7.1292830169449664f, + 7.1395513523987937f, 7.1497471195046822f, 7.1598713367783891f, + 7.1699250014423130f, 7.1799090900149345f, 7.1898245588800176f, + 7.1996723448363644f, 7.2094533656289492f, 7.2191685204621621f, + 7.2288186904958804f, 7.2384047393250794f, 7.2479275134435861f, + 7.2573878426926521f, 7.2667865406949019f, 7.2761244052742384f, + 7.2854022188622487f, 7.2946207488916270f, 7.3037807481771031f, + 7.3128829552843557f, 7.3219280948873617f, 7.3309168781146177f, + 7.3398500028846243f, 7.3487281542310781f, 7.3575520046180847f, + 7.3663222142458151f, 7.3750394313469254f, 7.3837042924740528f, + 7.3923174227787607f, 7.4008794362821844f, 7.4093909361377026f, + 7.4178525148858991f, 7.4262647547020979f, 7.4346282276367255f, + 7.4429434958487288f, 7.4512111118323299f, 7.4594316186372973f, + 7.4676055500829976f, 7.4757334309663976f, 7.4838157772642564f, + 7.4918530963296748f, 7.4998458870832057f, 7.5077946401986964f, + 7.5156998382840436f, 7.5235619560570131f, 7.5313814605163119f, + 7.5391588111080319f, 7.5468944598876373f, 7.5545888516776376f, + 7.5622424242210728f, 7.5698556083309478f, 7.5774288280357487f, + 7.5849625007211561f, 7.5924570372680806f, 7.5999128421871278f, + 7.6073303137496113f, 7.6147098441152075f, 7.6220518194563764f, + 7.6293566200796095f, 7.6366246205436488f, 7.6438561897747244f, + 7.6510516911789290f, 7.6582114827517955f, 7.6653359171851765f, + 7.6724253419714952f, 7.6794800995054464f, 7.6865005271832185f, + 7.6934869574993252f, 7.7004397181410926f, 7.7073591320808825f, + 7.7142455176661224f, 7.7210991887071856f, 7.7279204545631996f, + 7.7347096202258392f, 7.7414669864011465f, 7.7481928495894596f, + 7.7548875021634691f, 7.7615512324444795f, 7.7681843247769260f, + 7.7747870596011737f, 7.7813597135246608f, 7.7879025593914317f, + 7.7944158663501062f, 7.8008998999203047f, 7.8073549220576037f, + 7.8137811912170374f, 7.8201789624151887f, 7.8265484872909159f, + 7.8328900141647422f, 7.8392037880969445f, 7.8454900509443757f, + 7.8517490414160571f, 7.8579809951275719f, 7.8641861446542798f, + 7.8703647195834048f, 7.8765169465650002f, 7.8826430493618425f, + 7.8887432488982601f, 7.8948177633079446f, 7.9008668079807496f, + 7.9068905956085187f, 7.9128893362299619f, 7.9188632372745955f, + 7.9248125036057813f, 7.9307373375628867f, 7.9366379390025719f, + 7.9425145053392399f, 7.9483672315846778f, 7.9541963103868758f, + 7.9600019320680806f, 7.9657842846620870f, 7.9715435539507720f, + 7.9772799234999168f, 7.9829935746943104f, 7.9886846867721664f, + 7.9943534368588578f}; + +// Faster logarithm for small integers, with the property of log2(0) == 0. +static inline float FastLog2(uint32_t v) { + if (v < (sizeof(kLog2Table) / sizeof(kLog2Table[0]))) { + return kLog2Table[v]; + } + return static_cast(log2(v)); +} + +static inline float FastLog2f(float f) { + int exp; + float fr = std::frexp(f, &exp); + // TODO(veluca): improve constants. + return exp + (-1.34752046f * fr + 3.98979143f) * fr - 2.64898502f; +} + +} // namespace jxl + +#endif // JXL_BASE_FAST_LOG_H_ diff --git a/jxl/base/file_io.h b/jxl/base/file_io.h new file mode 100644 index 000000000..7a2c795d7 --- /dev/null +++ b/jxl/base/file_io.h @@ -0,0 +1,107 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_FILE_IO_H_ +#define JXL_BASE_FILE_IO_H_ + +// Helper functions for reading/writing files. + +#include + +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Returns extension including the dot, or empty string if none. Assumes +// filename is not a hidden file (e.g. ".bashrc"). May be called with a pathname +// if the filename contains a dot and/or no other path component does. +static inline std::string Extension(const std::string& filename) { + const size_t pos = filename.rfind('.'); + if (pos == std::string::npos) return std::string(); + return filename.substr(pos); +} + +// RAII, ensures files are closed even when returning early. +class FileWrapper { + public: + FileWrapper(const FileWrapper& other) = delete; + FileWrapper& operator=(const FileWrapper& other) = delete; + + explicit FileWrapper(const std::string& pathname, const char* mode) + : file_(fopen(pathname.c_str(), mode)) {} + + ~FileWrapper() { + if (file_ != nullptr) { + const int err = fclose(file_); + JXL_CHECK(err == 0); + } + } + + // We intend to use FileWrapper as a replacement of FILE. + // NOLINTNEXTLINE(google-explicit-constructor) + operator FILE*() const { return file_; } + + private: + FILE* const file_; +}; + +template +static inline Status ReadFile(const std::string& pathname, + ContainerType* JXL_RESTRICT bytes) { + FileWrapper f(pathname, "rb"); + if (f == nullptr) return JXL_FAILURE("Failed to open file for reading"); + + if (fseek(f, 0, SEEK_END) != 0) return JXL_FAILURE("Failed to seek end"); + bytes->resize(ftell(f)); + if (bytes->empty()) return JXL_FAILURE("Zero-length file"); + if (fseek(f, 0, SEEK_SET) != 0) return JXL_FAILURE("Failed to seek set"); + + size_t pos = 0; + while (pos < bytes->size()) { + // Needed in case ContainerType is std::string, whose data() is const. + char* bytes_writable = reinterpret_cast(&(*bytes)[0]); + const size_t bytes_read = + fread(bytes_writable + pos, 1, bytes->size() - pos, f); + if (bytes_read == 0) return JXL_FAILURE("Failed to read"); + pos += bytes_read; + } + JXL_ASSERT(pos == bytes->size()); + return true; +} + +template +static inline Status WriteFile(const ContainerType& bytes, + const std::string& pathname) { + FileWrapper f(pathname, "wb"); + if (f == nullptr) return JXL_FAILURE("Failed to open file for writing"); + + size_t pos = 0; + while (pos < bytes.size()) { + const size_t bytes_written = + fwrite(bytes.data() + pos, 1, bytes.size() - pos, f); + if (bytes_written == 0) return JXL_FAILURE("Failed to write"); + pos += bytes_written; + } + JXL_ASSERT(pos == bytes.size()); + + return true; +} + +} // namespace jxl + +#endif // JXL_BASE_FILE_IO_H_ diff --git a/jxl/base/iaca.h b/jxl/base/iaca.h new file mode 100644 index 000000000..83d0d0674 --- /dev/null +++ b/jxl/base/iaca.h @@ -0,0 +1,74 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_IACA_H_ +#define JXL_BASE_IACA_H_ + +#include + +// IACA (Intel's Code Analyzer) analyzes instruction latencies, but only for +// code between special markers. These functions embed such markers in an +// executable, but only for reading via IACA - they deliberately trigger a +// crash if executed to ensure they are removed in normal builds. + +#ifndef JXL_IACA_ENABLED +#define JXL_IACA_ENABLED 0 +#endif + +namespace jxl { + +// Call before the region of interest. +static HWY_INLINE void BeginIACA() { +#if JXL_IACA_ENABLED && (JXL_COMPILER_GCC || JXL_COMPILER_CLANG) + asm volatile( + // UD2 "instruction" raises an invalid opcode exception. + ".byte 0x0F, 0x0B\n\t" + // Magic sequence recognized by IACA (MOV + addr32 fs:NOP). This actually + // clobbers EBX, but we don't care because the code won't be run, and we + // want IACA to observe the same code the compiler would have generated + // without this marker. + "movl $111, %%ebx\n\t" + ".byte 0x64, 0x67, 0x90\n\t" + : + : + // (Allegedly) clobbering memory may prevent reordering. + : "memory"); +#endif +} + +// Call after the region of interest. +static HWY_INLINE void EndIACA() { +#if JXL_IACA_ENABLED && (JXL_COMPILER_GCC || JXL_COMPILER_CLANG) + asm volatile( + // See above. + "movl $222, %%ebx\n\t" + ".byte 0x64, 0x67, 0x90\n\t" + // UD2 + ".byte 0x0F, 0x0B\n\t" + : + : + // (Allegedly) clobbering memory may prevent reordering. + : "memory"); +#endif +} + +// Add to a scope to mark a region. +struct ScopeIACA { + HWY_INLINE ScopeIACA() { BeginIACA(); } + HWY_INLINE ~ScopeIACA() { EndIACA(); } +}; + +} // namespace jxl + +#endif // JXL_BASE_IACA_H_ diff --git a/jxl/base/os_specific.cc b/jxl/base/os_specific.cc new file mode 100644 index 000000000..b581ee6b1 --- /dev/null +++ b/jxl/base/os_specific.cc @@ -0,0 +1,366 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/os_specific.h" + +#include +#include +#include + +#include +#include +#include + +#include "jxl/base/arch_specific.h" // ProcessorTopology + +#if defined(_WIN32) || defined(_WIN64) +#define OS_WIN 1 +#define NOMINMAX +#include +#else +#define OS_WIN 0 +#endif + +#ifdef __linux__ +#define OS_LINUX 1 +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#else +#define OS_LINUX 0 +#endif + +#ifdef __MACH__ +#define OS_MAC 1 +#include +#include +#include +#include +#include +#include +#include +#include +#else +#define OS_MAC 0 +#endif + +#ifdef __FreeBSD__ +#define OS_FREEBSD 1 +#include +#include +#include +#include +#include +#else +#define OS_FREEBSD 0 +#endif + +namespace jxl { + +double Now() { +#if OS_WIN + LARGE_INTEGER counter; + (void)QueryPerformanceCounter(&counter); + LARGE_INTEGER freq; + (void)QueryPerformanceFrequency(&freq); + return double(counter.QuadPart) / freq.QuadPart; +#elif OS_MAC + const auto t = mach_absolute_time(); + // On OSX/iOS platform the elapsed time is cpu time unit + // We have to query the time base information to convert it back + // See https://developer.apple.com/library/mac/qa/qa1398/_index.html + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) { + (void)mach_timebase_info(&timebase); + } + return double(t) * timebase.numer / timebase.denom * 1E-9; +#else + timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1E-9; +#endif +} + +// Emulate Linux type (cpu_set_t) + interface on other platforms + +#if OS_FREEBSD +using cpu_set_t = cpuset_t; +#elif OS_WIN || OS_MAC +using cpu_set_t = uint64_t; + +static inline void CPU_ZERO(cpu_set_t* set) { *set = 0; } + +static inline int CPU_ISSET(int cpu, const cpu_set_t* set) { + return (*set & (1ULL << cpu)) != 0; +} + +static inline void CPU_SET(int cpu, cpu_set_t* set) { *set |= (1ULL << cpu); } +#endif + +struct ThreadAffinity { + cpu_set_t set; +}; + +#if OS_MAC +namespace { +Status GetSystemValue(const char* name, size_t* value) { + int64_t value_i64 = 0; + size_t size = sizeof(value_i64); + const int err = sysctlbyname(name, &value_i64, &size, nullptr, 0); + if (err != 0) return JXL_FAILURE("sysctl packages failed"); + JXL_ASSERT(value_i64 >= 0); + *value = static_cast(value_i64); + return true; +} + +// Returns mask with the lowest N bits set, one per logical processor. +cpu_set_t SetOfAllLogicalProcessors() { + size_t logical; + // On failure, assume there is at least one logical processor. + if (!GetSystemValue("machdep.cpu.thread_count", &logical)) return 1; + + if (logical > 64) { + printf("Warning: more than 64 logical processors, update cpu_set_t"); + return ~0ull; + } + if (logical == 64) return ~0ull; + + return (1ull << logical) - 1; +} +} // namespace +#endif + +Status GetProcessorTopologyFromOS(ProcessorTopology* pt) { +#if OS_MAC + size_t packages, cores, logical; // totals, not per package/core! + JXL_RETURN_IF_ERROR(GetSystemValue("hw.packages", &packages)); + JXL_RETURN_IF_ERROR(GetSystemValue("machdep.cpu.core_count", &cores)); + JXL_RETURN_IF_ERROR(GetSystemValue("machdep.cpu.thread_count", &logical)); + + // All succeeded: now set `pt` + pt->packages = packages; + pt->cores_per_package = cores / packages; + pt->logical_per_core = logical / cores; + + return true; +#else + // Not needed on X64 if the affinity APIs work (DetectProcessorTopology will + // succeed) + return false; +#endif +} + +ThreadAffinity* GetThreadAffinity() { + ThreadAffinity* affinity = + static_cast(malloc(sizeof(ThreadAffinity))); +#if OS_WIN + DWORD_PTR process_affinity, system_affinity; + const BOOL ok = GetProcessAffinityMask(GetCurrentProcess(), &process_affinity, + &system_affinity); + JXL_CHECK(ok); + affinity->set = process_affinity; +#elif OS_LINUX + CPU_ZERO(&affinity->set); + const int err = + pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &affinity->set); + JXL_CHECK(err == 0); +#elif OS_FREEBSD + const pid_t pid = getpid(); // current thread + const int err = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, + sizeof(cpuset_t), &affinity->set); + JXL_CHECK(err == 0); +#elif OS_MAC + static cpu_set_t all = SetOfAllLogicalProcessors(); + affinity->set = all; +#endif + return affinity; +} + +namespace { + +ThreadAffinity* OriginalThreadAffinity() { + static ThreadAffinity* original = GetThreadAffinity(); + return original; +} + +} // namespace + +Status SetThreadAffinity(ThreadAffinity* affinity) { + // Ensure original is initialized before changing. + const ThreadAffinity* const original = OriginalThreadAffinity(); + JXL_CHECK(original != nullptr); + +#if OS_WIN + const HANDLE hThread = GetCurrentThread(); + const DWORD_PTR prev = SetThreadAffinityMask(hThread, affinity->set); + if (prev == 0) return JXL_FAILURE("SetThreadAffinityMask failed"); + return true; +#elif OS_LINUX + const int err = + pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &affinity->set); + if (err != 0) return JXL_FAILURE("pthread_setaffinity_np failed"); + return true; +#elif OS_FREEBSD + const pid_t pid = getpid(); // current thread + const int err = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, pid, + sizeof(cpuset_t), &affinity->set); + if (err != 0) return JXL_FAILURE("cpuset_setaffinity failed"); + return true; +#elif OS_MAC + // As of 2019-03 we are unaware of a way to reliably restrict a thread to + // core(s); THREAD_AFFINITY_POLICY is only a hint. + (void)affinity; + return false; +#else + printf("Don't know how to SetThreadAffinity on this platform.\n"); + return false; +#endif +} + +std::vector AvailableCPUs() { + std::vector cpus; + cpus.reserve(128); +#if OS_WIN || OS_LINUX || OS_FREEBSD || OS_MAC + const ThreadAffinity* const affinity = OriginalThreadAffinity(); + for (int cpu = 0; cpu < static_cast(sizeof(cpu_set_t)) * 8; ++cpu) { + if (CPU_ISSET(cpu, &affinity->set)) { + cpus.push_back(static_cast(cpu)); + } + } +#else + cpus.push_back(0); +#endif + return cpus; +} + +Status PinThreadToCPU(const int cpu) { +#if OS_WIN || OS_LINUX || OS_FREEBSD || OS_MAC + ThreadAffinity affinity; + CPU_ZERO(&affinity.set); + CPU_SET(cpu, &affinity.set); + return SetThreadAffinity(&affinity); +#else + return false; +#endif +} + +Status PinThreadToRandomCPU() { + std::vector cpus = AvailableCPUs(); + + // Remove first two CPUs because interrupts are often pinned to them. + JXL_CHECK(cpus.size() > 2); + cpus.erase(cpus.begin(), cpus.begin() + 2); + + // Random choice to prevent burning up the same core. + std::random_device device; + std::ranlux48 generator(device()); + std::shuffle(cpus.begin(), cpus.end(), generator); + const int cpu = cpus.front(); + + return PinThreadToCPU(cpu); +} + +namespace { + +size_t DetectTotalMemoryMiB() { +#if OS_LINUX || OS_FREEBSD || OS_MAC + const long page_size = sysconf(_SC_PAGESIZE); + const long num_pages = sysconf(_SC_PHYS_PAGES); + if (page_size == -1 || num_pages == -1) { + JXL_WARNING("Failed to detect page size (%ld) and/or num pages (%ld)", + page_size, num_pages); + return 0; + } + JXL_ASSERT(page_size > 0 && num_pages > 0); + const uint64_t bytes = + static_cast(num_pages) * static_cast(page_size); + return bytes >> 20; +#elif OS_WIN + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) { + JXL_WARNING("Failed to get memory status"); + return 0; + } + const uint64_t bytes = ms.ullTotalPhys; + // `bytes` excludes nonpaged pool reserved during boot; round up to whole MiB + // to improve the estimate. + return (bytes + (1U << 20) - 1) >> 20; +#else + JXL_WARNING("Implement DetectTotalMemoryMiB for this platform"); + return 0; +#endif +} + +} // namespace + +size_t TotalMemoryMiB() { + static size_t mib = DetectTotalMemoryMiB(); + return mib; +} + +/* +Status RunCommand(const std::vector& args) { +#if _POSIX_VERSION >= 200112L + // Avoid system(), but do not try to be over-zealous about not passing along + // some special resources further (such as: inherited-not-marked-FD_CLOEXEC + // file descriptors). + std::vector c_args; + c_args.reserve(args.size() + 1); + for (size_t i = 0; i < args.size(); ++i) { + c_args.push_back(args[i].c_str()); + } + c_args.push_back(nullptr); + const pid_t pid = fork(); + if (pid == -1) // fork() failed. + return false; + if (pid != 0) { // Parent process. + int ret_status; + if (pid != waitpid(pid, &ret_status, 0)) { + return false; // waitpid() error. + } + return ret_status == 0; + } else { // Child process. + execvp(c_args[0], + // Address benign-but-annoying execvp() signature weirdness. + const_cast(c_args.data())); + JXL_ABORT("Failed to run command.\n"); + } +#elif OS_WIN + // Synthesize a string for system(). And warn about it. + // TODO(user): Fix this - research the safe way to run a command on Windows. + // Likely, the solution is along these lines: + // https://docs.microsoft.com/en-us/windows/desktop/ProcThread/creating-processes + std::ostringstream cmd; + std::copy(args.begin(), args.end(), + std::ostream_iterator(cmd, " ")); + printf(stderr, "Warning: Using system() on string: %s\n", cmd.str.c_str()); + int ret = system(cmd.str.c_str()); + if (errno != ENOENT && // Windows: Command interpreter not found. + ret == 0) { + return true; + } + return false; +#else +#error Neither a POSIX-1.2001 nor a Windows System. +#endif +} +*/ + +} // namespace jxl diff --git a/jxl/base/os_specific.h b/jxl/base/os_specific.h new file mode 100644 index 000000000..2c0b028c7 --- /dev/null +++ b/jxl/base/os_specific.h @@ -0,0 +1,72 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_OS_SPECIFIC_H_ +#define JXL_BASE_OS_SPECIFIC_H_ + +// OS-specific functions (e.g. timing and thread affinity) + +#include + +#include +#include + +#include "jxl/base/arch_specific.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Returns current time [seconds] from a monotonic clock with unspecified +// starting point - only suitable for computing elapsed time. +double Now(); + +// Called by arch_specific. Returns false if `pt` remains unchanged. Only +// implemented/needed on OSX. +struct ProcessorTopology; + +Status GetProcessorTopologyFromOS(ProcessorTopology* pt); + +// Returns logical processor numbers in [0, N), where N is the number of bits in +// the thread's initial affinity (unaffected by any SetThreadAffinity). +std::vector AvailableCPUs(); + +// Opaque. +struct ThreadAffinity; + +// Returns current affinity; useful for restoring the original value. +// Caller must free() the pointer - dynamic allocation is required because +// ThreadAffinity is an incomplete type. +ThreadAffinity* GetThreadAffinity(); + +// Restores a previous affinity returned by GetThreadAffinity. +Status SetThreadAffinity(ThreadAffinity* affinity); + +// Ensures the thread is running on the specified cpu, and no others. +// Useful for reducing nanobenchmark variability (fewer context switches). +// Calls SetThreadAffinity. +Status PinThreadToCPU(int cpu); + +// Random choice of CPU avoids overloading any one core. Calls PinThreadToCPU. +Status PinThreadToRandomCPU(); + +// Returns total physical memory size [MiB], or 0 if unknown. This function +// returns a cached value initialized on the first call. +size_t TotalMemoryMiB(); + +// Executes a command in a subprocess. +//Status RunCommand(const std::vector& args); + +} // namespace jxl + +#endif // JXL_BASE_OS_SPECIFIC_H_ diff --git a/jxl/base/override.h b/jxl/base/override.h new file mode 100644 index 000000000..e0a59b3f2 --- /dev/null +++ b/jxl/base/override.h @@ -0,0 +1,38 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_OVERRIDE_H_ +#define JXL_BASE_OVERRIDE_H_ + +// 'Trool' for command line arguments: force enable/disable, or use default. + +namespace jxl { + +// No effect if kDefault, otherwise forces a feature (typically a FrameHeader +// flag) on or off. +enum class Override : int { kOn = 1, kOff = 0, kDefault = -1 }; + +static inline Override OverrideFromBool(bool flag) { + return flag ? Override::kOn : Override::kOff; +} + +static inline bool ApplyOverride(Override o, bool default_condition) { + if (o == Override::kOn) return true; + if (o == Override::kOff) return false; + return default_condition; +} + +} // namespace jxl + +#endif // JXL_BASE_OVERRIDE_H_ diff --git a/jxl/base/padded_bytes.cc b/jxl/base/padded_bytes.cc new file mode 100644 index 000000000..e907125d5 --- /dev/null +++ b/jxl/base/padded_bytes.cc @@ -0,0 +1,47 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/padded_bytes.h" + +#include + +namespace jxl { + +void PaddedBytes::IncreaseCapacityTo(size_t capacity) { + JXL_ASSERT(capacity > capacity_); + + // BitWriter writes up to 7 bytes past the end. + CacheAlignedUniquePtr new_data = AllocateArray(capacity + 8); + if (new_data == nullptr) { + // Allocation failed, discard all data to ensure this is noticed. + size_ = capacity_ = 0; + return; + } + + if (data_ == nullptr) { + // First allocation: ensure first byte is initialized (won't be copied). + new_data[0] = 0; + } else { + // Subsequent resize: copy existing data to new location. + memcpy(new_data.get(), data_.get(), size_); + // Ensure that the first new byte is initialized, to allow write_bits to + // safely append to the newly-resized PaddedBytes. + new_data[size_] = 0; + } + + capacity_ = capacity; + std::swap(new_data, data_); +} + +} // namespace jxl diff --git a/jxl/base/padded_bytes.h b/jxl/base/padded_bytes.h new file mode 100644 index 000000000..4dfac380f --- /dev/null +++ b/jxl/base/padded_bytes.h @@ -0,0 +1,203 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_PADDED_BYTES_H_ +#define JXL_BASE_PADDED_BYTES_H_ + +// std::vector replacement with padding to reduce bounds checks in WriteBits + +#include +#include +#include + +#include // std::max +#include +#include + +#include "jxl/base/cache_aligned.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Provides a subset of the std::vector interface with some differences: +// - allows BitWriter to write 64 bits at a time without bounds checking; +// - ONLY zero-initializes the first byte (required by BitWriter); +// - ensures cache-line alignment. +class PaddedBytes { + public: + // Required for output params. + PaddedBytes() : size_(0), capacity_(0) {} + + explicit PaddedBytes(size_t size) : size_(size), capacity_(0) { + if (size != 0) IncreaseCapacityTo(size); + } + + PaddedBytes(size_t size, uint8_t value) : size_(size), capacity_(0) { + if (size != 0) { + IncreaseCapacityTo(size); + } + if (size_ != 0) { + memset(data(), value, size); + } + } + + PaddedBytes(const PaddedBytes& other) : size_(other.size_), capacity_(0) { + if (size_ != 0) IncreaseCapacityTo(size_); + if (data() != nullptr) memcpy(data(), other.data(), size_); + } + PaddedBytes& operator=(const PaddedBytes& other) { + // Self-assignment is safe. + resize(other.size()); + if (data() != nullptr) memmove(data(), other.data(), size_); + return *this; + } + + // default is not OK - need to set other.size_ to 0! + PaddedBytes(PaddedBytes&& other) noexcept + : size_(other.size_), + capacity_(other.capacity_), + data_(std::move(other.data_)) { + other.size_ = other.capacity_ = 0; + } + PaddedBytes& operator=(PaddedBytes&& other) noexcept { + size_ = other.size_; + capacity_ = other.capacity_; + data_ = std::move(other.data_); + + if (&other != this) { + other.size_ = other.capacity_ = 0; + } + return *this; + } + + void swap(PaddedBytes& other) { + std::swap(size_, other.size_); + std::swap(capacity_, other.capacity_); + std::swap(data_, other.data_); + } + + void reserve(size_t capacity) { + if (capacity > capacity_) IncreaseCapacityTo(capacity); + } + // NOTE: unlike vector, this does not initialize the new data! + // However, we guarantee that write_bits can safely append after + // the resize, as we zero-initialize the first new byte of data. + void resize(size_t size) { + if (size > capacity_) IncreaseCapacityTo(size); + size_ = (data() == nullptr) ? 0 : size; + } + + // Explicitely initializes the new data with the value. + void resize(size_t size, uint8_t value) { + size_t old_size = size_; + resize(size); + if (size_ > old_size) { + memset(data() + old_size, value, size_ - old_size); + } + } + + // Amortized constant complexity due to exponential growth. + void push_back(uint8_t x) { + if (size_ == capacity_) { + IncreaseCapacityTo(std::max(3 * capacity_ / 2, 64)); + if (data() == nullptr) return; + } + + data_[size_++] = x; + } + + size_t size() const { return size_; } + size_t capacity() const { return capacity_; } + + uint8_t* data() { return data_.get(); } + const uint8_t* data() const { return data_.get(); } + + // std::vector operations implemented in terms of the public interface above. + + void clear() { resize(0); } + bool empty() const { return size() == 0; } + + void assign(std::initializer_list il) { + resize(il.size()); + memcpy(data(), il.begin(), il.size()); + } + + void assign(const uint8_t* begin, const uint8_t* end) { + resize(end - begin); + memcpy(data(), begin, end - begin); + } + + uint8_t* begin() { return data(); } + const uint8_t* begin() const { return data(); } + uint8_t* end() { return begin() + size(); } + const uint8_t* end() const { return begin() + size(); } + + uint8_t& operator[](const size_t i) { + BoundsCheck(i); + return data()[i]; + } + const uint8_t& operator[](const size_t i) const { + BoundsCheck(i); + return data()[i]; + } + + uint8_t& back() { + JXL_ASSERT(size() != 0); + return data()[size() - 1]; + } + const uint8_t& back() const { + JXL_ASSERT(size() != 0); + return data()[size() - 1]; + } + + template + void append(const T& other) { + size_t old_size = size(); + resize(size() + other.size()); + memcpy(data() + old_size, other.data(), other.size()); + } + + void append(const uint8_t* begin, const uint8_t* end) { + size_t old_size = size(); + resize(size() + end - begin); + memcpy(data() + old_size, begin, end - begin); + } + + private: + void BoundsCheck(size_t i) const { + // <= is safe due to padding and required by BitWriter. + JXL_ASSERT(i <= size()); + } + + // Copies existing data to newly allocated "data_". If allocation fails, + // data() == nullptr and size_ = capacity_ = 0. + void IncreaseCapacityTo(size_t capacity); + + size_t size_; + size_t capacity_; + CacheAlignedUniquePtr data_; +}; + +template +static inline void Append(const T& s, PaddedBytes* out, + size_t* JXL_RESTRICT byte_pos) { + memcpy(out->data() + *byte_pos, s.data(), s.size()); + *byte_pos += s.size(); + JXL_CHECK(*byte_pos <= out->size()); +} + +} // namespace jxl + +#endif // JXL_BASE_PADDED_BYTES_H_ diff --git a/jxl/base/profiler.h b/jxl/base/profiler.h new file mode 100644 index 000000000..37418a220 --- /dev/null +++ b/jxl/base/profiler.h @@ -0,0 +1,753 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_PROFILER_H_ +#define JXL_BASE_PROFILER_H_ + +// High precision, low overhead time measurements. Returns exact call counts and +// total elapsed time for user-defined 'zones' (code regions, i.e. C++ scopes). +// +// Usage: add this header to BUILD srcs; instrument regions of interest: +// { PROFILER_ZONE("name"); /*code*/ } or +// void FuncToMeasure() { PROFILER_FUNC; /*code*/ }. +// After all threads have exited any zones, invoke PROFILER_PRINT_RESULTS() to +// print call counts and average durations [CPU cycles] to stdout, sorted in +// descending order of total duration. + +// Configuration settings: + +// If zero, this file has no effect and no measurements will be recorded. +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif + +// How many mebibytes to allocate (if PROFILER_ENABLED) per thread that +// enters at least one zone. Once this buffer is full, the thread will analyze +// and discard packets, thus temporarily adding some observer overhead. +// Each zone occupies 16 bytes. +#ifndef PROFILER_THREAD_STORAGE +#define PROFILER_THREAD_STORAGE 32ULL +#endif + +#if PROFILER_ENABLED + +#define PROFILER_PRINT_OVERHEAD 0 + +#include +#include +#include +#include // memcpy + +#include // min/max +#include +#include // PRIu64 +#include +#include + +#include "jxl/base/arch_specific.h" +#include "jxl/base/cache_aligned.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/robust_statistics.h" +#include "jxl/base/status.h" +#include "jxl/base/tsc_timer.h" + +// Non-portable aspects: +// - SSE2 128-bit load/store (write-combining, UpdateOrAdd) +// - RDTSCP timestamps (serializing, high-resolution) +// - assumes string literals are stored within an 8 MiB range +// - compiler-specific annotations (restrict, alignment, fences) +#if JXL_ARCH_X64 +#include +#if JXL_COMPILER_MSVC +#include +#else +#include +#endif +#endif + +namespace jxl { + +// Upper bounds for various fixed-size data structures (guarded via JXL_ASSERT): + +// How many unique threads can enter a zone (those that don't do not count). +// Memory use is about kMaxThreads * PROFILER_THREAD_STORAGE MiB. +// WARNING: fiber libraries and multiple ThreadPool can spawn >100 threads. +static constexpr size_t kMaxThreads = 256; + +// Maximum nesting of zones. +static constexpr size_t kMaxDepth = 64; + +// Total number of zones. +static constexpr size_t kMaxZones = 256; + +// Represents zone entry/exit events. Stores a full-resolution timestamp plus +// an offset (representing zone name or identifying exit packets). POD. +class Packet { + public: + // If offsets do not fit, UpdateOrAdd will overrun our heap allocation + // (governed by kMaxZones). We have seen ~100 MiB static binaries. + static constexpr size_t kOffsetBits = 27; + static constexpr uintptr_t kOffsetBias = 1ULL << (kOffsetBits - 1); + + // We need full-resolution timestamps; at an effective rate of 4 GHz, + // this permits 34 second zone durations (for longer durations, split into + // multiple zones). Wraparound is handled by masking. + static constexpr size_t kTimestampBits = 64 - kOffsetBits; + static constexpr uintptr_t kTimestampMask = (1ULL << kTimestampBits) - 1; + + static Packet Make(const uintptr_t biased_offset, const uint64_t timestamp) { + JXL_ASSERT(biased_offset < (1ULL << kOffsetBits)); + + Packet packet; + packet.bits_ = + (biased_offset << kTimestampBits) + (timestamp & kTimestampMask); + return packet; + } + + uint64_t Timestamp() const { return bits_ & kTimestampMask; } + + uintptr_t BiasedOffset() const { return (bits_ >> kTimestampBits); } + + private: + uint64_t bits_; +}; +static_assert(sizeof(Packet) == 8, "Wrong Packet size"); + +// Returns the address of a string literal. Assuming zone names are also +// literals and stored nearby, we can represent them as offsets, which are +// faster to compute than hashes or even a static index. +// +// This function must not be static - each call (even from other translation +// units) must return the same value. +inline uintptr_t StringOrigin() { + // Chosen such that no zone name is a prefix nor suffix of this string + // to ensure they aren't merged (offset 0 identifies zone-exit packets). + static const char* string_origin = "__#Origin#__"; + + return reinterpret_cast(string_origin) - Packet::kOffsetBias; +} + +// Representation of an active zone, stored in a stack. Used to deduct +// child duration from the parent's self time. POD. +struct ProfilerNode { + Packet packet; + uint64_t child_total; +}; + +// Holds statistics for all zones with the same name. POD. +struct Accumulator { + static constexpr size_t kNumCallBits = 64 - Packet::kOffsetBits; + + uintptr_t BiasedOffset() const { return num_calls >> kNumCallBits; } + uint64_t NumCalls() const { return num_calls & ((1ULL << kNumCallBits) - 1); } + + // UpdateOrAdd relies upon this layout. + uint64_t num_calls = 0; // upper bits = biased_offset. + uint64_t total_duration = 0; +}; +#if JXL_ARCH_X64 +static_assert(sizeof(Accumulator) == 2 * sizeof(uint64_t), "Accumulator size"); +#endif + +template +inline T ClampedSubtract(const T minuend, const T subtrahend) { + if (subtrahend > minuend) { + return 0; + } + return minuend - subtrahend; +} + +// Per-thread call graph (stack) and Accumulator for each zone. +class Results { + public: + Results() { + // Zero-initialize first accumulator to avoid a check for num_zones_ == 0. + memset(zones_, 0, sizeof(Accumulator)); + } + + // Used for computing overhead when this thread encounters its first Zone. + // This has no observable effect apart from increasing "analyze_elapsed_". + uint64_t ZoneDuration(const Packet* packets) { + JXL_CHECK(depth_ == 0); + JXL_CHECK(num_zones_ == 0); + AnalyzePackets(packets, 2); + const uint64_t duration = zones_[0].total_duration; + zones_[0].num_calls = 0; + zones_[0].total_duration = 0; + JXL_CHECK(depth_ == 0); + num_zones_ = 0; + return duration; + } + + void SetSelfOverhead(const uint64_t self_overhead) { + self_overhead_ = self_overhead; + } + + void SetChildOverhead(const uint64_t child_overhead) { + child_overhead_ = child_overhead; + } + + // Draw all required information from the packets, which can be discarded + // afterwards. Called whenever this thread's storage is full. + void AnalyzePackets(const Packet* packets, const size_t num_packets) { + const uint64_t t0 = TicksBefore(); + + for (size_t i = 0; i < num_packets; ++i) { + const Packet p = packets[i]; + // Entering a zone + if (p.BiasedOffset() != Packet::kOffsetBias) { + JXL_ASSERT(depth_ < kMaxDepth); + nodes_[depth_].packet = p; + nodes_[depth_].child_total = 0; + ++depth_; + continue; + } + + JXL_ASSERT(depth_ != 0); + const ProfilerNode& node = nodes_[depth_ - 1]; + // Masking correctly handles unsigned wraparound. + const uint64_t duration = + (p.Timestamp() - node.packet.Timestamp()) & Packet::kTimestampMask; + const uint64_t self_duration = ClampedSubtract( + duration, self_overhead_ + child_overhead_ + node.child_total); + + UpdateOrAdd(node.packet.BiasedOffset(), 1, self_duration); + --depth_; + + // Deduct this nested node's time from its parent's self_duration. + if (depth_ != 0) { + nodes_[depth_ - 1].child_total += duration + child_overhead_; + } + } + + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0; + } + + // Incorporates results from another thread. Call after all threads have + // exited any zones. + void Assimilate(const Results& other) { + const uint64_t t0 = TicksBefore(); + JXL_ASSERT(depth_ == 0); + JXL_ASSERT(other.depth_ == 0); + + for (size_t i = 0; i < other.num_zones_; ++i) { + const Accumulator& zone = other.zones_[i]; + UpdateOrAdd(zone.BiasedOffset(), zone.NumCalls(), zone.total_duration); + } + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0 + other.analyze_elapsed_; + } + + // Single-threaded. + void Print() { + const uint64_t t0 = TicksBefore(); + MergeDuplicates(); + + // Sort by decreasing total (self) cost. + std::sort(zones_, zones_ + num_zones_, + [](const Accumulator& r1, const Accumulator& r2) { + return r1.total_duration > r2.total_duration; + }); + + const uintptr_t string_origin = StringOrigin(); + uint64_t total_visible_duration = 0; + for (size_t i = 0; i < num_zones_; ++i) { + const Accumulator& r = zones_[i]; + const uint64_t num_calls = r.NumCalls(); + const char* name = + reinterpret_cast(string_origin + r.BiasedOffset()); + if (name[0] != '@') { + total_visible_duration += r.total_duration; + printf("%-40s: %10" PRIu64 " x %15" PRIu64 "= %15" PRIu64 "\n", name, + num_calls, r.total_duration / num_calls, r.total_duration); + } + } + + const uint64_t t1 = TicksAfter(); + analyze_elapsed_ += t1 - t0; + printf("Total clocks during analysis: %" PRIu64 "\n", analyze_elapsed_); + printf("Total clocks measured: %" PRIu64 "\n", total_visible_duration); + } + + // Single-threaded. Clears all results as if no zones had been recorded. + void Reset() { + analyze_elapsed_ = 0; + JXL_CHECK(depth_ == 0); + num_zones_ = 0; + memset(nodes_, 0, sizeof(nodes_)); + memset(zones_, 0, sizeof(zones_)); + } + + private: +#if JXL_ARCH_X64 + static bool SameOffset(const __m128i zone, const uintptr_t biased_offset) { + const uint64_t num_calls = _mm_cvtsi128_si64(zone); + return (num_calls >> Accumulator::kNumCallBits) == biased_offset; + } +#endif + + // Updates an existing Accumulator (uniquely identified by biased_offset) or + // adds one if this is the first time this thread analyzed that zone. + // Uses a self-organizing list data structure, which avoids dynamic memory + // allocations and is far faster than unordered_map. Loads, updates and + // stores the entire Accumulator with vector instructions. + void UpdateOrAdd(const uintptr_t biased_offset, const uint64_t num_calls, + const uint64_t duration) { + JXL_ASSERT(biased_offset < (1ULL << Packet::kOffsetBits)); + +#if JXL_ARCH_X64 + const __m128i num_calls_64 = _mm_cvtsi64_si128(num_calls); + const __m128i duration_64 = _mm_cvtsi64_si128(duration); + const __m128i add_duration_call = + _mm_unpacklo_epi64(num_calls_64, duration_64); + + __m128i* const JXL_RESTRICT zones = reinterpret_cast<__m128i*>(zones_); + + // Special case for first zone: (maybe) update, without swapping. + __m128i prev = _mm_load_si128(zones); + if (SameOffset(prev, biased_offset)) { + prev = _mm_add_epi64(prev, add_duration_call); + JXL_ASSERT(SameOffset(prev, biased_offset)); + _mm_store_si128(zones, prev); + return; + } + + // Look for a zone with the same offset. + for (size_t i = 1; i < num_zones_; ++i) { + __m128i zone = _mm_load_si128(zones + i); + if (SameOffset(zone, biased_offset)) { + zone = _mm_add_epi64(zone, add_duration_call); + JXL_ASSERT(SameOffset(zone, biased_offset)); + // Swap with predecessor (more conservative than move to front, + // but at least as successful). + _mm_store_si128(zones + i - 1, zone); + _mm_store_si128(zones + i, prev); + return; + } + prev = zone; + } + + // Not found; create a new Accumulator. + const __m128i biased_offset_64 = _mm_slli_epi64( + _mm_cvtsi64_si128(biased_offset), Accumulator::kNumCallBits); + const __m128i zone = _mm_add_epi64(biased_offset_64, add_duration_call); + JXL_ASSERT(SameOffset(zone, biased_offset)); + + JXL_ASSERT(num_zones_ < kMaxZones); + _mm_store_si128(zones + num_zones_, zone); + ++num_zones_; +#else + // Special case for first zone: (maybe) update, without swapping. + if (zones_[0].BiasedOffset() == biased_offset) { + zones_[0].total_duration += duration; + zones_[0].num_calls += num_calls; + JXL_ASSERT(zones_[0].BiasedOffset() == biased_offset); + return; + } + + // Look for a zone with the same offset. + for (size_t i = 1; i < num_zones_; ++i) { + if (zones_[i].BiasedOffset() == biased_offset) { + zones_[i].total_duration += duration; + zones_[i].num_calls += num_calls; + JXL_ASSERT(zones_[i].BiasedOffset() == biased_offset); + // Swap with predecessor (more conservative than move to front, + // but at least as successful). + const Accumulator prev = zones_[i - 1]; + zones_[i - 1] = zones_[i]; + zones_[i] = prev; + return; + } + } + + // Not found; create a new Accumulator. + JXL_ASSERT(num_zones_ < kMaxZones); + Accumulator* JXL_RESTRICT zone = zones_ + num_zones_; + zone->num_calls = (biased_offset << Accumulator::kNumCallBits) + num_calls; + zone->total_duration = duration; + JXL_ASSERT(zone->BiasedOffset() == biased_offset); + ++num_zones_; +#endif + } + + // Each instantiation of a function template seems to get its own copy of + // __func__ and GCC doesn't merge them. An N^2 search for duplicates is + // acceptable because we only expect a few dozen zones. + void MergeDuplicates() { + const uintptr_t string_origin = StringOrigin(); + for (size_t i = 0; i < num_zones_; ++i) { + const uintptr_t biased_offset = zones_[i].BiasedOffset(); + const char* name = + reinterpret_cast(string_origin + biased_offset); + // Separate num_calls from biased_offset so we can add them together. + uint64_t num_calls = zones_[i].NumCalls(); + + // Add any subsequent duplicates to num_calls and total_duration. + for (size_t j = i + 1; j < num_zones_;) { + if (!strcmp(name, reinterpret_cast( + string_origin + zones_[j].BiasedOffset()))) { + num_calls += zones_[j].NumCalls(); + zones_[i].total_duration += zones_[j].total_duration; + // Fill hole with last item. + zones_[j] = zones_[--num_zones_]; + } else { // Name differed, try next Accumulator. + ++j; + } + } + + JXL_ASSERT(num_calls < (1ULL << Accumulator::kNumCallBits)); + + // Re-pack regardless of whether any duplicates were found. + zones_[i].num_calls = + (biased_offset << Accumulator::kNumCallBits) + num_calls; + } + } + + uint64_t analyze_elapsed_ = 0; + uint64_t self_overhead_ = 0; + uint64_t child_overhead_ = 0; + + size_t depth_ = 0; // Number of active zones. + size_t num_zones_ = 0; // Number of retired zones. + + alignas(64) ProfilerNode nodes_[kMaxDepth]; // Stack + alignas(64) Accumulator zones_[kMaxZones]; // Self-organizing list +}; + +// Per-thread packet storage, allocated via CacheAligned. +class ThreadSpecific { + static constexpr size_t kBufferCapacity = + CacheAligned::kCacheLineSize / sizeof(Packet); + + public: + // "name" is used to sanity-check offsets fit in kOffsetBits. + explicit ThreadSpecific(const char* name) + : packets_(static_cast( + CacheAligned::Allocate(PROFILER_THREAD_STORAGE << 20))), + num_packets_(0), + max_packets_(PROFILER_THREAD_STORAGE << 17), + string_origin_(StringOrigin()) { + // Even in optimized builds (with NDEBUG), verify that this zone's name + // offset fits within the allotted space. If not, UpdateOrAdd is likely to + // overrun zones_[]. We also JXL_ASSERT(), but users often do not run debug + // builds. Checking here on the cold path (only reached once per thread) + // is cheap, but it only covers one zone. + const uintptr_t biased_offset = + reinterpret_cast(name) - string_origin_; + JXL_CHECK(biased_offset <= (1ULL << Packet::kOffsetBits)); + } + + ~ThreadSpecific() { CacheAligned::Free(packets_); } + + // Depends on Zone => defined below. + void ComputeOverhead(); + + void WriteEntry(const char* name, const uint64_t timestamp) { + const uintptr_t biased_offset = + reinterpret_cast(name) - string_origin_; + Write(Packet::Make(biased_offset, timestamp)); + } + + void WriteExit(const uint64_t timestamp) { + const uintptr_t biased_offset = Packet::kOffsetBias; + Write(Packet::Make(biased_offset, timestamp)); + } + + void AnalyzeRemainingPackets() { +#if JXL_ARCH_X64 + // Ensures prior weakly-ordered streaming stores are globally visible. + _mm_sfence(); + + // Storage full => empty it. + if (num_packets_ + buffer_size_ > max_packets_) { + results_.AnalyzePackets(packets_, num_packets_); + num_packets_ = 0; + } + memcpy(packets_ + num_packets_, buffer_, buffer_size_ * sizeof(Packet)); + num_packets_ += buffer_size_; + buffer_size_ = 0; +#endif + + results_.AnalyzePackets(packets_, num_packets_); + num_packets_ = 0; + } + + Results& GetResults() { return results_; } + + private: +#if JXL_ARCH_X64 && HWY_BITS != 0 + // Overwrites `to` without loading it into cache (read-for-ownership). + // Copies kCacheLineSize bytes from/to naturally aligned addresses. + template + static HWY_ATTR void StreamCacheLine(const T* JXL_RESTRICT from, + T* JXL_RESTRICT to) { + static_assert(16 % sizeof(T) == 0, "T must fit in a lane"); + constexpr size_t kLanes = 16 / sizeof(T); + const hwy::Desc d; + JXL_COMPILER_FENCE; + const auto v0 = Load(d, from + 0 * kLanes); + const auto v1 = Load(d, from + 1 * kLanes); + const auto v2 = Load(d, from + 2 * kLanes); + const auto v3 = Load(d, from + 3 * kLanes); + static_assert(sizeof(v0) * 4 == 64, "Wrong #vectors"); + // Fences prevent the compiler from reordering loads/stores, which may + // interfere with write-combining. + JXL_COMPILER_FENCE; + Stream(v0, d, to + 0 * kLanes); + Stream(v1, d, to + 1 * kLanes); + Stream(v2, d, to + 2 * kLanes); + Stream(v3, d, to + 3 * kLanes); + JXL_COMPILER_FENCE; + } +#endif + + // Write packet to buffer/storage, emptying them as needed. + void Write(const Packet packet) { +#if JXL_ARCH_X64 && HWY_BITS != 0 + // Buffer full => copy to storage. + if (buffer_size_ == kBufferCapacity) { + // Storage full => empty it. + if (num_packets_ + kBufferCapacity > max_packets_) { + results_.AnalyzePackets(packets_, num_packets_); + num_packets_ = 0; + } + // This buffering halves observer overhead and decreases the overall + // runtime by about 3%. + StreamCacheLine(buffer_, packets_ + num_packets_); + num_packets_ += kBufferCapacity; + buffer_size_ = 0; + } + buffer_[buffer_size_] = packet; + ++buffer_size_; +#else + // Write directly to storage. + if (num_packets_ >= max_packets_) { + results_.AnalyzePackets(packets_, num_packets_); + num_packets_ = 0; + } + packets_[num_packets_] = packet; + ++num_packets_; +#endif + } + + // Write-combining buffer to avoid cache pollution. Must be the first + // non-static member to ensure cache-line alignment. +#if JXL_ARCH_X64 + Packet buffer_[kBufferCapacity]; + size_t buffer_size_ = 0; +#endif + + // Contiguous storage for zone enter/exit packets. + Packet* const JXL_RESTRICT packets_; + size_t num_packets_; + const size_t max_packets_; + // Cached here because we already read this cache line on zone entry/exit. + uintptr_t string_origin_; + Results results_; +}; + +class ThreadList { + public: + // Thread-safe. + void Add(ThreadSpecific* const ts) { + const uint32_t index = num_threads_.fetch_add(1, std::memory_order_relaxed); + JXL_CHECK(index < kMaxThreads); + threads_[index] = ts; + } + + // Single-threaded. + void PrintResults() { + const uint32_t num_threads = num_threads_.load(std::memory_order_relaxed); + for (uint32_t i = 0; i < num_threads; ++i) { + threads_[i]->AnalyzeRemainingPackets(); + } + + // Combine all threads into a single Result. + for (uint32_t i = 1; i < num_threads; ++i) { + threads_[0]->GetResults().Assimilate(threads_[i]->GetResults()); + } + + if (num_threads != 0) { + threads_[0]->GetResults().Print(); + + for (uint32_t i = 0; i < num_threads; ++i) { + threads_[i]->GetResults().Reset(); + } + } + } + + private: + // Owning pointers. + alignas(64) ThreadSpecific* threads_[kMaxThreads]; + std::atomic num_threads_{0}; +}; + +// RAII zone enter/exit recorder constructed by the ZONE macro; also +// responsible for initializing ThreadSpecific. +class Zone { + public: + // "name" must be a string literal (see StringOrigin). + JXL_NOINLINE explicit Zone(const char* name) { + JXL_COMPILER_FENCE; + ThreadSpecific* JXL_RESTRICT thread_specific = StaticThreadSpecific(); + if (JXL_UNLIKELY(thread_specific == nullptr)) { + void* mem = CacheAligned::Allocate(sizeof(ThreadSpecific)); + thread_specific = new (mem) ThreadSpecific(name); + // Must happen before ComputeOverhead, which re-enters this ctor. + Threads().Add(thread_specific); + StaticThreadSpecific() = thread_specific; + thread_specific->ComputeOverhead(); + } + + // (Capture timestamp ASAP, not inside WriteEntry.) + JXL_COMPILER_FENCE; + const uint64_t timestamp = TicksBefore(); + thread_specific->WriteEntry(name, timestamp); + } + + JXL_NOINLINE ~Zone() { + JXL_COMPILER_FENCE; + const uint64_t timestamp = TicksAfter(); + StaticThreadSpecific()->WriteExit(timestamp); + JXL_COMPILER_FENCE; + } + + // Call exactly once after all threads have exited all zones. + static void PrintResults() { Threads().PrintResults(); } + + private: + // Returns reference to the thread's ThreadSpecific pointer (initially null). + // Function-local static avoids needing a separate definition. + static ThreadSpecific*& StaticThreadSpecific() { + static thread_local ThreadSpecific* thread_specific; + return thread_specific; + } + + // Returns the singleton ThreadList. Non time-critical. + static ThreadList& Threads() { + static ThreadList threads_; + return threads_; + } +}; + +// Creates a zone starting from here until the end of the current scope. +// Timestamps will be recorded when entering and exiting the zone. +// "name" must be a string literal, which is ensured by merging with "". +#define PROFILER_ZONE(name) \ + JXL_COMPILER_FENCE; \ + const ::jxl::Zone zone("" name); \ + JXL_COMPILER_FENCE + +// Creates a zone for an entire function (when placed at its beginning). +// Shorter/more convenient than ZONE. +#define PROFILER_FUNC \ + JXL_COMPILER_FENCE; \ + const ::jxl::Zone zone(__func__); \ + JXL_COMPILER_FENCE + +#define PROFILER_PRINT_RESULTS ::jxl::Zone::PrintResults + +inline void ThreadSpecific::ComputeOverhead() { + // Delay after capturing timestamps before/after the actual zone runs. Even + // with frequency throttling disabled, this has a multimodal distribution, + // including 32, 34, 48, 52, 59, 62. + uint64_t self_overhead; + { + const size_t kNumSamples = 32; + uint32_t samples[kNumSamples]; + for (size_t idx_sample = 0; idx_sample < kNumSamples; ++idx_sample) { + const size_t kNumDurations = 1024; + uint32_t durations[kNumDurations]; + + for (size_t idx_duration = 0; idx_duration < kNumDurations; + ++idx_duration) { + { + PROFILER_ZONE("Dummy Zone (never shown)"); + } +#if JXL_ARCH_X64 + const uint64_t duration = results_.ZoneDuration(buffer_); + buffer_size_ = 0; +#else + const uint64_t duration = results_.ZoneDuration(packets_); + num_packets_ = 0; +#endif + durations[idx_duration] = static_cast(duration); + JXL_CHECK(num_packets_ == 0); + } + CountingSort(durations, durations + kNumDurations); + samples[idx_sample] = HalfSampleMode()(durations, kNumDurations); + } + // Median. + CountingSort(samples, samples + kNumSamples); + self_overhead = samples[kNumSamples / 2]; +#if PROFILER_PRINT_OVERHEAD + printf("Overhead: %zu\n", self_overhead); +#endif + results_.SetSelfOverhead(self_overhead); + } + + // Delay before capturing start timestamp / after end timestamp. + const size_t kNumSamples = 32; + uint32_t samples[kNumSamples]; + for (size_t idx_sample = 0; idx_sample < kNumSamples; ++idx_sample) { + const size_t kNumDurations = 16; + uint32_t durations[kNumDurations]; + for (size_t idx_duration = 0; idx_duration < kNumDurations; + ++idx_duration) { + const size_t kReps = 10000; + // Analysis time should not be included => must fit within buffer. + JXL_CHECK(kReps * 2 < max_packets_); +#if JXL_ARCH_X64 + _mm_mfence(); +#endif + const uint64_t t0 = TicksBefore(); + for (size_t i = 0; i < kReps; ++i) { + PROFILER_ZONE("Dummy"); + } +#if JXL_ARCH_X64 + _mm_sfence(); +#endif + const uint64_t t1 = TicksAfter(); +#if JXL_ARCH_X64 + JXL_CHECK(num_packets_ + buffer_size_ == kReps * 2); + buffer_size_ = 0; +#else + JXL_CHECK(num_packets_ == kReps * 2); +#endif + num_packets_ = 0; + const uint64_t avg_duration = (t1 - t0 + kReps / 2) / kReps; + durations[idx_duration] = + static_cast(ClampedSubtract(avg_duration, self_overhead)); + } + CountingSort(durations, durations + kNumDurations); + samples[idx_sample] = HalfSampleMode()(durations, kNumDurations); + } + CountingSort(samples, samples + kNumSamples); + const uint64_t child_overhead = samples[9 * kNumSamples / 10]; +#if PROFILER_PRINT_OVERHEAD + printf("Child overhead: %zu\n", child_overhead); +#endif + results_.SetChildOverhead(child_overhead); +} + +} // namespace jxl + +#else // !PROFILER_ENABLED +#define PROFILER_ZONE(name) +#define PROFILER_FUNC +#define PROFILER_PRINT_RESULTS() +#endif + +#endif // JXL_BASE_PROFILER_H_ diff --git a/jxl/base/robust_statistics.h b/jxl/base/robust_statistics.h new file mode 100644 index 000000000..b87b54ba0 --- /dev/null +++ b/jxl/base/robust_statistics.h @@ -0,0 +1,361 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_ROBUST_STATISTICS_H_ +#define JXL_BASE_ROBUST_STATISTICS_H_ + +// Robust statistics: Mode, Median, MedianAbsoluteDeviation. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "jxl/base/arch_specific.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Round up for integers +template ::is_integer>::type* = nullptr> +inline T Half(T x) { + return (x + 1) / 2; +} + +// Mul is faster than div. +template ::is_integer>::type* = nullptr> +inline T Half(T x) { + return x * 0.5; +} + +// Returns the median value. Side effect: values <= median will appear before, +// values >= median after the middle index. +// Guarantees average speed O(num_values). +template +T Median(T* samples, const size_t num_samples) { + JXL_ASSERT(num_samples != 0); + std::nth_element(samples, samples + num_samples / 2, samples + num_samples); + T result = samples[num_samples / 2]; + // If even size, find largest element in the partially sorted vector to + // use as second element to average with + if ((num_samples & 1) == 0) { + T biggest = *std::max_element(samples, samples + num_samples / 2); + result = Half(result + biggest); + } + return result; +} + +template +T Median(std::vector* samples) { + return Median(samples->data(), samples->size()); +} + +template +static inline T Median3(const T a, const T b, const T c) { + return std::max(std::min(a, b), std::min(c, std::max(a, b))); +} + +template +static inline T Median5(const T a, const T b, const T c, const T d, const T e) { + return Median3(e, std::max(std::min(a, b), std::min(c, d)), + std::min(std::max(a, b), std::max(c, d))); +} + +// Returns a robust measure of variability. +template +T MedianAbsoluteDeviation(const T* samples, const size_t num_samples, + const T median) { + JXL_ASSERT(num_samples != 0); + std::vector abs_deviations; + abs_deviations.reserve(num_samples); + for (size_t i = 0; i < num_samples; ++i) { + abs_deviations.push_back(std::abs(samples[i] - median)); + } + return Median(&abs_deviations); +} + +template +T MedianAbsoluteDeviation(const std::vector& samples, const T median) { + return MedianAbsoluteDeviation(samples.data(), samples.size(), median); +} + +// Half{Range/Sample}Mode are implementations of "Robust estimators of the mode +// and skewness of continuous data". The mode is less affected by outliers in +// highly-skewed distributions than the median. + +// Robust estimator of the mode for data given as sorted values. +// O(N*logN), N=num_values. +class HalfSampleMode { + public: + // Returns mode. "sorted" must be in ascending order. + template + T operator()(const T* const JXL_RESTRICT sorted, + const size_t num_values) const { + int64_t center = num_values / 2; + int64_t width = num_values; + + // Zoom in on modal intervals of decreasing width. Stop before we reach + // width=1, i.e. single values, for which there is no "slope". + while (width > 2) { + // Round up so we can still reach the outer edges of odd widths. + width = Half(width); + + center = CenterOfIntervalWithMinSlope(sorted, num_values, center, width); + } + + return sorted[center]; // mode := middle value in modal interval. + } + + private: + // Returns center of the densest region [c-radius, c+radius]. + template + static JXL_INLINE int64_t CenterOfIntervalWithMinSlope( + const T* JXL_RESTRICT sorted, const int64_t total_values, + const int64_t center, const int64_t width) { + const int64_t radius = Half(width); + + auto compute_slope = [radius, total_values, sorted]( + int64_t c, int64_t* actual_center = nullptr) { + // For symmetry, check 2*radius+1 values, i.e. [min, max]. + const int64_t min = std::max(c - radius, int64_t(0)); + const int64_t max = std::min(c + radius, total_values - 1); + JXL_ASSERT(min < max); + JXL_ASSERT(sorted[min] <= + sorted[max] + std::numeric_limits::epsilon()); + const float dx = max - min + 1; + const float slope = (sorted[max] - sorted[min]) / dx; + + if (actual_center != nullptr) { + // c may be out of bounds, so return center of the clamped bounds. + *actual_center = Half(min + max); + } + return slope; + }; + + // First find min_slope for all centers. + float min_slope = std::numeric_limits::max(); + for (int64_t c = center - radius; c <= center + radius; ++c) { + min_slope = std::min(min_slope, compute_slope(c)); + } + + // Candidates := centers with slope ~= min_slope. + std::vector candidates; + for (int64_t c = center - radius; c <= center + radius; ++c) { + int64_t actual_center; + const float slope = compute_slope(c, &actual_center); + if (slope <= min_slope * 1.001f) { + candidates.push_back(actual_center); + } + } + + // Keep the median. + JXL_ASSERT(!candidates.empty()); + if (candidates.size() == 1) return candidates[0]; + return Median(&candidates); + } +}; + +// Robust estimator of the mode for data given as a CDF. +// O(N*logN), N=num_bins. +class HalfRangeMode { + public: + // Returns mode expressed as a histogram bin index. "cdf" must be weakly + // monotonically increasing, e.g. from std::partial_sum. + int operator()(const uint32_t* JXL_RESTRICT cdf, + const size_t num_bins) const { + int center = num_bins / 2; + int width = num_bins; + + // Zoom in on modal intervals of decreasing width. Stop before we reach + // width=1, i.e. original bins, because those are noisy. + while (width > 2) { + // Round up so we can still reach the outer edges of odd widths. + width = Half(width); + + center = CenterOfIntervalWithMaxDensity(cdf, num_bins, center, width); + } + + return center; // mode := midpoint of modal interval. + } + + private: + // Returns center of the densest interval [c-radius, c+radius]. + static JXL_INLINE int CenterOfIntervalWithMaxDensity( + const uint32_t* JXL_RESTRICT cdf, const int total_bins, const int center, + const int width) { + const int radius = Half(width); + + auto compute_density = [radius, total_bins, cdf]( + int c, int* actual_center = nullptr) { + // For symmetry, check 2*radius+1 bins, i.e. [min, max]. + const int min = std::max(c - radius, 1); // for -1 below + const int max = std::min(c + radius, total_bins - 1); + JXL_ASSERT(min < max); + JXL_ASSERT(cdf[min] <= cdf[max - 1]); + const int num_bins = max - min + 1; + // Sum over [min, max] == CDF(max) - CDF(min-1). + const float density = float(cdf[max] - cdf[min - 1]) / num_bins; + + if (actual_center != nullptr) { + // c may be out of bounds, so take center of the clamped bounds. + *actual_center = Half(min + max); + } + return density; + }; + + // First find max_density for all centers. + float max_density = 0.0f; + for (int c = center - radius; c <= center + radius; ++c) { + max_density = std::max(max_density, compute_density(c)); + } + + // Candidates := centers with density ~= max_density. + std::vector candidates; + for (int c = center - radius; c <= center + radius; ++c) { + int actual_center; + const float density = compute_density(c, &actual_center); + if (density >= max_density * 0.999f) { + candidates.push_back(actual_center); + } + } + + // Keep the median. + JXL_ASSERT(!candidates.empty()); + if (candidates.size() == 1) return candidates[0]; + return Median(&candidates); + } +}; + +// Sorts integral values in ascending order. About 3x faster than std::sort for +// input distributions with very few unique values. +template +void CountingSort(T* begin, T* end) { + // Unique values and their frequency (similar to flat_map). + using Unique = std::pair; + std::vector unique; + for (const T* p = begin; p != end; ++p) { + const T value = *p; + const auto pos = + std::find_if(unique.begin(), unique.end(), + [value](const Unique& u) { return u.first == value; }); + if (pos == unique.end()) { + unique.push_back(std::make_pair(*p, 1)); + } else { + ++pos->second; + } + } + + // Sort in ascending order of value (pair.first). + std::sort(unique.begin(), unique.end()); + + // Write that many copies of each unique value to the array. + T* JXL_RESTRICT p = begin; + for (const auto& value_count : unique) { + std::fill(p, p + value_count.second, value_count.first); + p += value_count.second; + } + JXL_ASSERT(p == end); +} + +struct Bivariate { + Bivariate(float x, float y) : x(x), y(y) {} + float x; + float y; +}; + +class Line { + public: + constexpr Line(const float slope, const float intercept) + : slope_(slope), intercept_(intercept) {} + + constexpr float slope() const { return slope_; } + constexpr float intercept() const { return intercept_; } + + // Robust line fit using Siegel's repeated-median algorithm. + explicit Line(const std::vector& points) { + const size_t N = points.size(); + // This straightforward N^2 implementation is OK for small N. + JXL_ASSERT(N < 10 * 1000); + + // One for every point i. + std::vector medians; + medians.reserve(N); + + // One for every j != i. Never cleared to avoid reallocation. + std::vector slopes(N - 1); + + for (size_t i = 0; i < N; ++i) { + // Index within slopes[] (avoids the hole where j == i). + size_t idx_slope = 0; + + for (size_t j = 0; j < N; ++j) { + if (j == i) continue; + + const float dy = points[j].y - points[i].y; + const float dx = points[j].x - points[i].x; + JXL_ASSERT(std::abs(dx) > 1E-7f); // x must be distinct + slopes[idx_slope++] = dy / dx; + } + JXL_ASSERT(idx_slope == N - 1); + + const float median = Median(&slopes); + medians.push_back(median); + } + + slope_ = Median(&medians); + + // Solve for intercept, overwriting medians[]. + for (size_t i = 0; i < N; ++i) { + medians[i] = points[i].y - slope_ * points[i].x; + } + intercept_ = Median(&medians); + } + + constexpr float operator()(float x) const { return x * slope_ + intercept_; } + + private: + float slope_; + float intercept_; +}; + +static inline void EvaluateQuality(const Line& line, + const std::vector& points, + float* JXL_RESTRICT max_l1, + float* JXL_RESTRICT median_abs_deviation) { + // For computing median_abs_deviation. + std::vector abs_deviations; + abs_deviations.reserve(points.size()); + + *max_l1 = 0.0f; + for (const Bivariate& point : points) { + const float l1 = std::abs(line(point.x) - point.y); + *max_l1 = std::max(*max_l1, l1); + abs_deviations.push_back(l1); + } + + *median_abs_deviation = Median(&abs_deviations); +} + +} // namespace jxl + +#endif // JXL_BASE_ROBUST_STATISTICS_H_ diff --git a/jxl/base/span.h b/jxl/base/span.h new file mode 100644 index 000000000..c0b5e4f30 --- /dev/null +++ b/jxl/base/span.h @@ -0,0 +1,67 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_SPAN_H_ +#define JXL_BASE_SPAN_H_ + +// Span (array view) is a non-owning container that provides cheap "cut" +// operations and could be used as "ArrayLike" data source for PaddedBytes. + +#include + +#include "jxl/base/status.h" + +namespace jxl { + +template +class Span { + public: + constexpr Span() noexcept : Span(nullptr, 0) {} + + constexpr Span(T* array, size_t length) noexcept + : ptr_(array), len_(length) {} + + template + explicit constexpr Span(T (&a)[N]) noexcept : Span(a, N) {} + + template + explicit constexpr Span(const ArrayLike& other) noexcept + : Span(reinterpret_cast(other.data()), other.size()) { + static_assert(sizeof(*other.data()) == sizeof(T), + "Incompatible type of source."); + } + + constexpr T* data() const noexcept { return ptr_; } + + constexpr size_t size() const noexcept { return len_; } + + constexpr T& operator[](size_t i) const noexcept { + // MSVC 2015 accepts this as constexpr, but not ptr_[i] + return *(data() + i); + } + + void remove_prefix(size_t n) noexcept { + JXL_ASSERT(size() >= n); + ptr_ += n; + len_ -= n; + } + + private: + T* ptr_; + size_t len_; +}; + +} // namespace jxl + +#endif // JXL_BASE_SPAN_H_ diff --git a/jxl/base/status.cc b/jxl/base/status.cc new file mode 100644 index 000000000..e8e2f9005 --- /dev/null +++ b/jxl/base/status.cc @@ -0,0 +1,57 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/status.h" + +#include +#include +#include + +#include + +#include "jxl/base/os_specific.h" + +namespace jxl { + +bool Debug(const char* format, ...) { +// Show the debug messages in debug or opt mode, not in release mode. +#if JXL_DEBUG_WARNING + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +#endif + return false; +} + +bool Abort(const char* file, int line, const char* format, ...) { + char buf[2000]; + va_list args; + va_start(args, format); + vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + + const std::string call_stack; + + fprintf(stderr, "Abort at %s:%d: %s\n%s\n", file, line, buf, + call_stack.c_str()); +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) + // If compiled with UBSAN, triggering an error gives us a call stack. + printf("Deliberate ubsan div zero %zu\n", 10 / (TotalMemoryMiB() >> 30)); +#endif + abort(); +} + +} // namespace jxl diff --git a/jxl/base/status.h b/jxl/base/status.h new file mode 100644 index 000000000..4dc1d83cb --- /dev/null +++ b/jxl/base/status.h @@ -0,0 +1,185 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_STATUS_H_ +#define JXL_BASE_STATUS_H_ + +// Error handling: Status return type + helper macros. + +#include +#include + +#include "jxl/base/compiler_specific.h" + +namespace jxl { + +// Uncomment to abort when JXL_FAILURE is reached +// #define JXL_CRASH_ON_ERROR + +#ifndef JXL_ENABLE_ASSERT +#define JXL_ENABLE_ASSERT 1 +#endif + +// Pass -DJXL_DEBUG_ON_ERROR at compile time to print debug messages when a +// function returns JXL_FAILURE or calls JXL_NOTIFY_ERROR. Note that this is +// irrelevant if you also pass -DJXL_CRASH_ON_ERROR. +#ifdef JXL_DEBUG_ON_ERROR +#undef JXL_DEBUG_ON_ERROR +#define JXL_DEBUG_ON_ERROR 1 +#else // JXL_DEBUG_ON_ERROR +#ifdef NDEBUG +#define JXL_DEBUG_ON_ERROR 0 +#else // JXL_DEBUG_ON_ERROR +#define JXL_DEBUG_ON_ERROR 1 +#endif // NDEBUG +#endif // JXL_DEBUG_ON_ERROR + +// The Verbose level for the library +#ifndef JXL_DEBUG_V_LEVEL +#define JXL_DEBUG_V_LEVEL 0 +#endif // JXL_DEBUG_V_LEVEL + +// Print a debug message on standard error. You should use the JXL_DEBUG macro +// instead of calling Debug directly. This function returns false, so it can be +// used as a return value in JXL_FAILURE. +JXL_FORMAT(1, 2) +bool Debug(const char* format, ...); + +// Print a debug message on standard error if "enabled" is true. "enabled" is +// normally a macro that evaluates to 0 or 1 at compile time, so the Debug +// function is never called and optimized out in release builds. Note that the +// arguments are compiled but not evaluated when enabled is false. The format +// string must be a explicit string in the call, for example: +// JXL_DEBUG(JXL_DEBUG_MYMODULE, "my module message: %d", some_var); +// Add a header at the top of your module's .cc or .h file (depending on whether +// you have JXL_DEBUG calls from the .h as well) like this: +// #ifndef JXL_DEBUG_MYMODULE +// #define JXL_DEBUG_MYMODULE 0 +// #endif JXL_DEBUG_MYMODULE +#define JXL_DEBUG(enabled, format, ...) \ + do { \ + if (enabled) { \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, \ + ##__VA_ARGS__); \ + } \ + } while (0) + +// JXL_DEBUG version that prints the debug message if the global verbose level +// defined at compile time by JXL_DEBUG_V_LEVEL is greater or equal than the +// passed level. +#define JXL_DEBUG_V(level, ...) \ + JXL_DEBUG(level <= JXL_DEBUG_V_LEVEL, __VA_ARGS__) + +// Warnings (via JXL_WARNING) are enabled by default in debug builds (opt and +// debug). +#ifdef JXL_DEBUG_WARNING +#undef JXL_DEBUG_WARNING +#define JXL_DEBUG_WARNING 1 +#else // JXL_DEBUG_WARNING +#ifdef NDEBUG +#define JXL_DEBUG_WARNING 0 +#else // JXL_DEBUG_WARNING +#define JXL_DEBUG_WARNING 1 +#endif // NDEBUG +#endif // JXL_DEBUG_WARNING +#define JXL_WARNING(...) JXL_DEBUG(JXL_DEBUG_WARNING, __VA_ARGS__) + +// Exits the program after printing file/line plus a formatted string. +JXL_FORMAT(3, 4) +JXL_NORETURN bool Abort(const char* file, int line, const char* format, ...); + +// Exits the program after printing file/line plus a formatted string. +#define JXL_ABORT(...) Abort(__FILE__, __LINE__, __VA_ARGS__) + +// Does not guarantee running the code, use only for debug mode checks. +#if JXL_ENABLE_ASSERT +#define JXL_ASSERT(condition) \ + do { \ + if (!(condition)) { \ + ::jxl::Abort(__FILE__, __LINE__, "Assert %s", #condition); \ + } \ + } while (0) +#else +#define JXL_ASSERT(condition) \ + do { \ + } while (0) +#endif + +// Same as above, but only runs in debug builds (builds where NDEBUG is not +// defined). This is useful for slower asserts that we want to run more rarely +// than usual. These will run on asan, msan and other debug builds, but not in +// opt or release. +#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ + defined(MEMORY_SANITIZER) || defined(THREAD_SANITIZER) +#define JXL_DASSERT(condition) \ + do { \ + if (!(condition)) { \ + ::jxl::Abort(__FILE__, __LINE__, "Debug Assert %s", #condition); \ + } \ + } while (0) +#else +#define JXL_DASSERT(condition) \ + do { \ + } while (0) +#endif + +// Always runs the condition, so can be used for non-debug calls. +#define JXL_CHECK(condition) \ + do { \ + if (!(condition)) { \ + ::jxl::Abort(__FILE__, __LINE__, "Check %s", #condition); \ + } \ + } while (0) + +// Always runs the condition, so can be used for non-debug calls. +#define JXL_RETURN_IF_ERROR(condition) \ + do { \ + if (!(condition)) return false; \ + } while (0) + +// Annotation for the location where an error condition is first noticed. +// Error codes are too unspecific to pinpoint the exact location, so we +// add a build flag that crashes and dumps stack at the actual error source. +#ifdef JXL_CRASH_ON_ERROR +#define JXL_NOTIFY_ERROR(...) \ + (void)::jxl::Abort(__FILE__, __LINE__, __VA_ARGS__) +#define JXL_FAILURE(...) ::jxl::Abort(__FILE__, __LINE__, __VA_ARGS__) +#else // JXL_CRASH_ON_ERROR +#define JXL_NOTIFY_ERROR(...) JXL_DEBUG(JXL_DEBUG_ON_ERROR, __VA_ARGS__) +#define JXL_FAILURE(format, ...) \ + ((JXL_DEBUG_ON_ERROR) && \ + ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__) && \ + false) +#endif // JXL_CRASH_ON_ERROR + +// Drop-in replacement for bool that raises compiler warnings if not used +// after being returned from a function. Example: +// Status LoadFile(...) { return true; } is more compact than +// bool JXL_MUST_USE_RESULT LoadFile(...) { return true; } +class JXL_MUST_USE_RESULT Status { + public: + // We want implicit constructor from bool to allow returning "true" or "false" + // on a function when using Status. + Status(bool ok) : ok_(ok) {} // NOLINT(google-explicit-constructor) + + // We also want implicit cast to bool to check for return values of functions. + operator bool() const { return ok_; } // NOLINT(google-explicit-constructor) + + private: + bool ok_; +}; + +} // namespace jxl + +#endif // JXL_BASE_STATUS_H_ diff --git a/jxl/base/thread_pool_internal.h b/jxl/base/thread_pool_internal.h new file mode 100644 index 000000000..6d37b063f --- /dev/null +++ b/jxl/base/thread_pool_internal.h @@ -0,0 +1,61 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_THREAD_TEST_UTILS_H_ +#define JXL_THREAD_TEST_UTILS_H_ + +#include + +#include + +#include "include/jpegxl/parallel_runner.h" +#include "include/jpegxl/thread_parallel_runner.h" +#include "jxl/base/data_parallel.h" + +namespace jxl { + +// Helper class to pass an internal ThreadPool-like object using threads. This +// is only suitable for tests or tools that access the internal API of JPEG XL. +// In other cases the caller will provide a JpegxlParallelRunner() for handling +// this. This class uses jpegxl::ThreadParallelRunner (from jpegxl_threads +// library). For interface details check jpegxl::ThreadParallelRunner. +class ThreadPoolInternal : public ThreadPool { + public: + // Starts the given number of worker threads and blocks until they are ready. + // "num_worker_threads" defaults to one per hyperthread. If zero, all tasks + // run on the main thread. + explicit ThreadPoolInternal( + int num_worker_threads = std::thread::hardware_concurrency()) + : ThreadPool(&jpegxl::ThreadParallelRunner::Runner, + static_cast(&runner_)), + runner_(num_worker_threads) {} + + ThreadPoolInternal(const ThreadPoolInternal&) = delete; + ThreadPoolInternal& operator&(const ThreadPoolInternal&) = delete; + + size_t NumThreads() const { return runner_.NumThreads(); } + size_t NumWorkerThreads() const { return runner_.NumWorkerThreads(); } + + template + void RunOnEachThread(const Func& func) { + runner_.RunOnEachThread(func); + } + + private: + jpegxl::ThreadParallelRunner runner_; +}; + +} // namespace jxl + +#endif // JXL_THREAD_TEST_UTILS_H_ diff --git a/jxl/base/tsc_timer.h b/jxl/base/tsc_timer.h new file mode 100644 index 000000000..41908777a --- /dev/null +++ b/jxl/base/tsc_timer.h @@ -0,0 +1,142 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BASE_TSC_TIMER_H_ +#define JXL_BASE_TSC_TIMER_H_ + +// High-resolution (~10 ns) timestamps, using fences to prevent reordering and +// ensure exactly the desired regions are measured. + +#include + +#include +#include // LoadFence + +#include "jxl/base/arch_specific.h" +#include "jxl/base/compiler_specific.h" + +namespace jxl { + +// TicksBefore/After return absolute timestamps and must be placed immediately +// before and after the region to measure. The functions are distinct because +// they use different fences. +// +// Background: RDTSC is not 'serializing'; earlier instructions may complete +// after it, and/or later instructions may complete before it. 'Fences' ensure +// regions' elapsed times are independent of such reordering. The only +// documented unprivileged serializing instruction is CPUID, which acts as a +// full fence (no reordering across it in either direction). Unfortunately +// the latency of CPUID varies wildly (perhaps made worse by not initializing +// its EAX input). Because it cannot reliably be deducted from the region's +// elapsed time, it must not be included in the region to measure (i.e. +// between the two RDTSC). +// +// The newer RDTSCP is sometimes described as serializing, but it actually +// only serves as a half-fence with release semantics. Although all +// instructions in the region will complete before the final timestamp is +// captured, subsequent instructions may leak into the region and increase the +// elapsed time. Inserting another fence after the final RDTSCP would prevent +// such reordering without affecting the measured region. +// +// Fortunately, such a fence exists. The LFENCE instruction is only documented +// to delay later loads until earlier loads are visible. However, Intel's +// reference manual says it acts as a full fence (waiting until all earlier +// instructions have completed, and delaying later instructions until it +// completes). AMD assigns the same behavior to MFENCE. +// +// We need a fence before the initial RDTSC to prevent earlier instructions +// from leaking into the region, and arguably another after RDTSC to avoid +// region instructions from completing before the timestamp is recorded. +// When surrounded by fences, the additional RDTSCP half-fence provides no +// benefit, so the initial timestamp can be recorded via RDTSC, which has +// lower overhead than RDTSCP because it does not read TSC_AUX. In summary, +// we define Before = LFENCE/RDTSC/LFENCE; After = RDTSCP/LFENCE. +// +// Using Before+Before leads to higher variance and overhead than After+After. +// However, After+After includes an LFENCE in the region measurements, which +// adds a delay dependent on earlier loads. The combination of Before+After +// is faster than Before+Before and more consistent than Stop+Stop because +// the first LFENCE already delayed subsequent loads before the measured +// region. This combination seems not to have been considered in prior work: +// http://akaros.cs.berkeley.edu/lxr/akaros/kern/arch/x86/rdtsc_test.c +// +// Note: performance counters can measure 'exact' instructions-retired or +// (unhalted) cycle counts. The RDPMC instruction is not serializing and also +// requires fences. Unfortunately, it is not accessible on all OSes and we +// prefer to avoid kernel-mode drivers. Performance counters are also affected +// by several under/over-count errata, so we use the TSC instead. + +// Returns a 64-bit timestamp in unit of 'ticks'; to convert to seconds, +// divide by InvariantTicksPerSecond. Although 32-bit ticks are faster to read, +// they overflow too quickly to measure long regions. +static inline uint64_t TicksBefore() { + uint64_t t; +#if JXL_ARCH_PPC + asm volatile("mfspr %0, %1" : "=r"(t) : "i"(268)); +#elif JXL_ARCH_X64 && JXL_COMPILER_MSVC + LoadFence(); + JXL_COMPILER_FENCE; + t = __rdtsc(); + LoadFence(); + JXL_COMPILER_FENCE; +#elif JXL_ARCH_X64 && (JXL_COMPILER_CLANG || JXL_COMPILER_GCC) + asm volatile( + "lfence\n\t" + "rdtsc\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) + : + // "memory" avoids reordering. rdx = TSC >> 32. + // "cc" = flags modified by SHL. + : "rdx", "memory", "cc"); +#else + // Fall back to OS - unsure how to reliably query cntvct_el0 frequency. + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + t = ts.tv_sec * 1000000000LL + ts.tv_nsec; +#endif + return t; +} + +static inline uint64_t TicksAfter() { + uint64_t t; +#if JXL_ARCH_X64 && JXL_COMPILER_MSVC + JXL_COMPILER_FENCE; + unsigned aux; + t = __rdtscp(&aux); + LoadFence(); + JXL_COMPILER_FENCE; +#elif JXL_ARCH_X64 && (JXL_COMPILER_CLANG || JXL_COMPILER_GCC) + // Use inline asm because __rdtscp generates code to store TSC_AUX (ecx). + asm volatile( + "rdtscp\n\t" + "shl $32, %%rdx\n\t" + "or %%rdx, %0\n\t" + "lfence" + : "=a"(t) + : + // "memory" avoids reordering. rcx = TSC_AUX. rdx = TSC >> 32. + // "cc" = flags modified by SHL. + : "rcx", "rdx", "memory", "cc"); +#else + t = TicksBefore(); // no difference on other platforms. +#endif + return t; +} + +} // namespace jxl + +#endif // JXL_BASE_TSC_TIMER_H_ diff --git a/jxl/bit_reader_test.cc b/jxl/bit_reader_test.cc new file mode 100644 index 000000000..3ab2fb5f2 --- /dev/null +++ b/jxl/bit_reader_test.cc @@ -0,0 +1,276 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/aux_out.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/span.h" +#include "jxl/base/thread_pool_internal.h" +#include "jxl/common.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/enc_bit_writer.h" + +namespace jxl { +namespace { + +HWY_ATTR void BitReaderTest_ExtendsWithZeroes() { + for (size_t size = 4; size < 32; ++size) { + std::vector data(size, 0xff); + + for (size_t n_bytes = 0; n_bytes < size; n_bytes++) { + BitReader br(Span(data.data(), n_bytes)); + // Read all the bits + for (size_t i = 0; i < n_bytes * kBitsPerByte; i++) { + ASSERT_EQ(br.ReadBits(1), 1) << "n_bytes=" << n_bytes << " i=" << i; + } + + // PEEK more than the declared size - all will be zero. Cannot consume. + for (size_t i = 0; i < BitReader::kMaxBitsPerCall; i++) { + ASSERT_EQ(br.PeekBits(i), 0) + << "size=" << size << "n_bytes=" << n_bytes << " i=" << i; + } + + EXPECT_TRUE(br.Close()); + } + } +} +TEST(BitReaderTest, ExtendsWithZeroes) { BitReaderTest_ExtendsWithZeroes(); } + +struct Symbol { + uint32_t num_bits; + uint32_t value; +}; + +// Reading from output gives the same values. +TEST(BitReaderTest, TestRoundTrip) { + ThreadPoolInternal pool(8); + pool.Run(0, 1000, ThreadPool::SkipInit(), + [](const int task, const int /* thread */) HWY_ATTR { + constexpr size_t kMaxBits = 8000; + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + + std::vector symbols; + symbols.reserve(1000); + + std::mt19937 rng(55537 + 129 * task); + std::uniform_int_distribution<> dist(1, 32); // closed interval + + for (;;) { + const uint32_t num_bits = dist(rng); + if (writer.BitsWritten() + num_bits > kMaxBits) break; + const uint32_t value = rng() >> (32 - num_bits); + symbols.push_back({num_bits, value}); + writer.Write(num_bits, value); + } + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + for (const Symbol& s : symbols) { + EXPECT_EQ(s.value, reader.ReadBits(s.num_bits)); + } + EXPECT_TRUE(reader.Close()); + }); +} + +// SkipBits is the same as reading that many bits. +TEST(BitReaderTest, TestSkip) { + ThreadPoolInternal pool(8); + pool.Run( + 0, 96, ThreadPool::SkipInit(), + [](const int task, const int /* thread */) HWY_ATTR { + constexpr size_t kSize = 100; + + for (size_t skip = 0; skip < 128; ++skip) { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kSize * kBitsPerByte); + // Start with "task" 1-bits. + for (int i = 0; i < task; ++i) { + writer.Write(1, 1); + } + + // Write 0-bits that we will skip over + for (size_t i = 0; i < skip; ++i) { + writer.Write(1, 0); + } + + // Write terminator bits '101' + writer.Write(3, 5); + EXPECT_EQ(task + skip + 3, writer.BitsWritten()); + writer.ZeroPadToByte(); + AuxOut aux_out; + ReclaimAndCharge(&writer, &allotment, 0, &aux_out); + EXPECT_LT(aux_out.layers[0].total_bits, kSize * 8); + + BitReader reader1(writer.GetSpan()); + BitReader reader2(writer.GetSpan()); + // Verify initial 1-bits + for (int i = 0; i < task; ++i) { + EXPECT_EQ(1, reader1.ReadBits(1)); + EXPECT_EQ(1, reader2.ReadBits(1)); + } + + // SkipBits or manually read "skip" bits + reader1.SkipBits(skip); + for (size_t i = 0; i < skip; ++i) { + EXPECT_EQ(0, reader2.ReadBits(1)) << " skip=" << skip << " i=" << i; + } + EXPECT_EQ(reader1.TotalBitsConsumed(), reader2.TotalBitsConsumed()); + + // Ensure both readers see the terminator bits. + EXPECT_EQ(5, reader1.ReadBits(3)); + EXPECT_EQ(5, reader2.ReadBits(3)); + + EXPECT_TRUE(reader1.Close()); + EXPECT_TRUE(reader2.Close()); + } + }); +} + +// Verifies byte order and different groupings of bits. +HWY_ATTR void BitReaderTest_TestOrder() { + constexpr size_t kMaxBits = 16; + + // u(1) - bits written into LSBs of first byte + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + for (size_t i = 0; i < 5; ++i) { + writer.Write(1, 1); + } + for (size_t i = 0; i < 5; ++i) { + writer.Write(1, 0); + } + for (size_t i = 0; i < 6; ++i) { + writer.Write(1, 1); + } + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0x1F, reader.ReadFixedBits<8>()); + EXPECT_EQ(0xFC, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // u(8) - get bytes in the same order + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(8, 0xF8); + writer.Write(8, 0x3F); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0xF8, reader.ReadFixedBits<8>()); + EXPECT_EQ(0x3F, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // u(16) - little-endian bytes + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(16, 0xF83F); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0x3F, reader.ReadFixedBits<8>()); + EXPECT_EQ(0xF8, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } + + // Non-byte-aligned, mixed sizes + { + BitWriter writer; + BitWriter::Allotment allotment(&writer, kMaxBits); + writer.Write(1, 1); + writer.Write(3, 6); + writer.Write(8, 0xDB); + writer.Write(4, 8); + + writer.ZeroPadToByte(); + ReclaimAndCharge(&writer, &allotment, 0, nullptr); + BitReader reader(writer.GetSpan()); + EXPECT_EQ(0xBD, reader.ReadFixedBits<8>()); + EXPECT_EQ(0x8D, reader.ReadFixedBits<8>()); + EXPECT_TRUE(reader.Close()); + } +} + +TEST(BitReaderTest, TestOrder) { BitReaderTest_TestOrder(); } + +HWY_ATTR void BitReaderTest_TotalCountersTest() { + uint8_t buf[4] = {1, 2, 3, 4}; + BitReader reader(Span(buf, sizeof(buf))); + + EXPECT_EQ(sizeof(buf), reader.TotalBytes()); + EXPECT_EQ(0, reader.TotalBitsConsumed()); + reader.ReadFixedBits<1>(); + EXPECT_EQ(1, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<10>(); + EXPECT_EQ(11, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<4>(); + EXPECT_EQ(15, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<1>(); + EXPECT_EQ(16, reader.TotalBitsConsumed()); + + reader.ReadFixedBits<16>(); + EXPECT_EQ(32, reader.TotalBitsConsumed()); + + EXPECT_TRUE(reader.Close()); +} + +TEST(BitReaderTest, TotalCountersTest) { BitReaderTest_TotalCountersTest(); } + +HWY_ATTR void BitReaderTest_MoveTest() { + uint8_t buf[4] = {1, 2, 3, 4}; + BitReader reader2; + { + BitReader reader1(Span(buf, sizeof(buf))); + + EXPECT_EQ(0, reader1.TotalBitsConsumed()); + reader1.ReadFixedBits<16>(); + EXPECT_EQ(16, reader1.TotalBitsConsumed()); + + reader2 = std::move(reader1); + // From this point reader1 is invalid, but can continue to access reader2 + // and we don't need to call Close() on reader1. + } + + EXPECT_EQ(16, reader2.TotalBitsConsumed()); + EXPECT_EQ(3U, reader2.ReadFixedBits<8>()); + EXPECT_EQ(24, reader2.TotalBitsConsumed()); + + EXPECT_TRUE(reader2.Close()); +} + +TEST(BitReaderTest, MoveTest) { BitReaderTest_MoveTest(); } + +} // namespace +} // namespace jxl diff --git a/jxl/bits_test.cc b/jxl/bits_test.cc new file mode 100644 index 000000000..9df0c2571 --- /dev/null +++ b/jxl/bits_test.cc @@ -0,0 +1,105 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/bits.h" + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(BitsTest, TestPopCount) { + EXPECT_EQ(0, PopCount(0u)); + EXPECT_EQ(1, PopCount(1u)); + EXPECT_EQ(1, PopCount(2u)); + EXPECT_EQ(2, PopCount(3u)); + EXPECT_EQ(1, PopCount(0x80000000u)); + EXPECT_EQ(31, PopCount(0x7FFFFFFFu)); + EXPECT_EQ(32, PopCount(0xFFFFFFFFu)); + + EXPECT_EQ(1, PopCount(0x80000000ull)); + EXPECT_EQ(31, PopCount(0x7FFFFFFFull)); + EXPECT_EQ(32, PopCount(0xFFFFFFFFull)); + EXPECT_EQ(33, PopCount(0x10FFFFFFFFull)); + EXPECT_EQ(63, PopCount(0xFFFEFFFFFFFFFFFFull)); + EXPECT_EQ(64, PopCount(0xFFFFFFFFFFFFFFFFull)); +} + +TEST(BitsTest, TestNumZeroBits) { + // Zero input is well-defined. + EXPECT_EQ(32, NumZeroBitsAboveMSB(0u)); + EXPECT_EQ(64, NumZeroBitsAboveMSB(0ull)); + EXPECT_EQ(32, NumZeroBitsBelowLSB(0u)); + EXPECT_EQ(64, NumZeroBitsBelowLSB(0ull)); + + EXPECT_EQ(31, NumZeroBitsAboveMSB(1u)); + EXPECT_EQ(30, NumZeroBitsAboveMSB(2u)); + EXPECT_EQ(63, NumZeroBitsAboveMSB(1ull)); + EXPECT_EQ(62, NumZeroBitsAboveMSB(2ull)); + + EXPECT_EQ(0, NumZeroBitsBelowLSB(1u)); + EXPECT_EQ(0, NumZeroBitsBelowLSB(1ull)); + EXPECT_EQ(1, NumZeroBitsBelowLSB(2u)); + EXPECT_EQ(1, NumZeroBitsBelowLSB(2ull)); + + EXPECT_EQ(0, NumZeroBitsAboveMSB(0x80000000u)); + EXPECT_EQ(0, NumZeroBitsAboveMSB(0x8000000000000000ull)); + EXPECT_EQ(31, NumZeroBitsBelowLSB(0x80000000u)); + EXPECT_EQ(63, NumZeroBitsBelowLSB(0x8000000000000000ull)); +} + +TEST(BitsTest, TestFloorLog2) { + // for input = [1, 7] + const int expected[7] = {0, 1, 1, 2, 2, 2, 2}; + for (uint32_t i = 1; i <= 7; ++i) { + EXPECT_EQ(expected[i - 1], FloorLog2Nonzero(i)) << " " << i; + EXPECT_EQ(expected[i - 1], FloorLog2Nonzero(uint64_t(i))) << " " << i; + } + + EXPECT_EQ(31, FloorLog2Nonzero(0x80000000u)); + EXPECT_EQ(31, FloorLog2Nonzero(0x80000001u)); + EXPECT_EQ(31, FloorLog2Nonzero(0xFFFFFFFFu)); + + EXPECT_EQ(31, FloorLog2Nonzero(0x80000000ull)); + EXPECT_EQ(31, FloorLog2Nonzero(0x80000001ull)); + EXPECT_EQ(31, FloorLog2Nonzero(0xFFFFFFFFull)); + + EXPECT_EQ(63, FloorLog2Nonzero(0x8000000000000000ull)); + EXPECT_EQ(63, FloorLog2Nonzero(0x8000000000000001ull)); + EXPECT_EQ(63, FloorLog2Nonzero(0xFFFFFFFFFFFFFFFFull)); +} + +TEST(BitsTest, TestCeilLog2) { + // for input = [1, 7] + const int expected[7] = {0, 1, 2, 2, 3, 3, 3}; + for (uint32_t i = 1; i <= 7; ++i) { + EXPECT_EQ(expected[i - 1], CeilLog2Nonzero(i)) << " " << i; + EXPECT_EQ(expected[i - 1], CeilLog2Nonzero(uint64_t(i))) << " " << i; + } + + EXPECT_EQ(31, CeilLog2Nonzero(0x80000000u)); + EXPECT_EQ(32, CeilLog2Nonzero(0x80000001u)); + EXPECT_EQ(32, CeilLog2Nonzero(0xFFFFFFFFu)); + + EXPECT_EQ(31, CeilLog2Nonzero(0x80000000ull)); + EXPECT_EQ(32, CeilLog2Nonzero(0x80000001ull)); + EXPECT_EQ(32, CeilLog2Nonzero(0xFFFFFFFFull)); + + EXPECT_EQ(63, CeilLog2Nonzero(0x8000000000000000ull)); + EXPECT_EQ(64, CeilLog2Nonzero(0x8000000000000001ull)); + EXPECT_EQ(64, CeilLog2Nonzero(0xFFFFFFFFFFFFFFFFull)); +} + +} // namespace +} // namespace jxl diff --git a/jxl/block.h b/jxl/block.h new file mode 100644 index 000000000..c7da3cd02 --- /dev/null +++ b/jxl/block.h @@ -0,0 +1,221 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BLOCK_H_ +#define JXL_BLOCK_H_ + +// Adapters for DCT input/output: from/to contiguous blocks or image rows. + +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/common.h" + +namespace jxl { + +// Adapters for source/destination. +// +// Block: (x, y) <-> (N * y + x) +// Lines: (x, y) <-> (stride * y + x) +// +// I.e. Block is a specialization of Lines with fixed stride. +// +// FromXXX should implement Read and Load (Read vector). +// ToXXX should implement Write and Store (Write vector). + +template +using BlockDesc = HWY_CAPPED(float, N); + +// Here and in the following, the SZ template parameter specifies the number of +// values to load/store. Needed because we want to handle 4x4 sub-blocks of +// 16x16 blocks. +template +class FromBlock { + public: + explicit FromBlock(const float* block) : block_(block) {} + + FromBlock View(size_t dx, size_t dy) const { + return FromBlock(Address(dx, dy)); + } + + template + HWY_ATTR HWY_INLINE hwy::VT> LoadPart(const size_t row, + size_t i) const { + static_assert(SZ <= COLS, "Requesting more values that are present!"); + return Load(BlockDesc(), block_ + row * COLS + i); + } + + HWY_ATTR HWY_INLINE hwy::VT> LoadVec(const size_t row, + size_t i) const { + return LoadPart(row, i); + } + + HWY_ATTR HWY_INLINE float Read(const size_t row, const size_t i) const { + return *Address(row, i); + } + + constexpr HWY_INLINE const float* Address(const size_t row, + const size_t i) const { + return block_ + row * COLS + i; + } + + private: + const float* block_; +}; + +template +class ToBlock { + public: + explicit ToBlock(float* block) : block_(block) {} + + ToBlock View(size_t dx, size_t dy) const { + return ToBlock(Address(dx, dy)); + } + + template + HWY_ATTR HWY_INLINE void StorePart(const hwy::VT>& v, + const size_t row, const size_t i) const { + static_assert(SZ <= COLS, "Requesting more values that are present!"); + Store(v, BlockDesc(), Address(row, i)); + } + + HWY_ATTR HWY_INLINE void StoreVec(const hwy::VT>& v, + const size_t row, size_t i) const { + return StorePart(v, row, i); + } + + HWY_ATTR HWY_INLINE void Write(float v, const size_t row, + const size_t i) const { + *Address(row, i) = v; + } + + constexpr HWY_INLINE float* Address(const size_t row, const size_t i) const { + return block_ + row * COLS + i; + } + + private: + float* block_; +}; + +// Same as ToBlock, but multiplies result by (N * N) +template +class ScaleToBlock { + public: + explicit HWY_ATTR ScaleToBlock(float* block) : block_(block) {} + + ScaleToBlock View(size_t dx, size_t dy) const { + return ScaleToBlock(Address(dx, dy)); + } + + template + HWY_ATTR HWY_INLINE void StorePart(const hwy::VT>& v, + const size_t row, const size_t i) const { + using BlockDesc = jxl::BlockDesc; + const auto mul_ = Set(BlockDesc(), 1.0f / (COLS * ROWS)); + Store(v * mul_, BlockDesc(), Address(row, i)); + } + + HWY_ATTR HWY_INLINE void StoreVec(const hwy::VT>& v, + const size_t row, size_t i) const { + return StorePart(v, row, i); + } + + HWY_ATTR HWY_INLINE void Write(float v, const size_t row, + const size_t i) const { + constexpr float mul_ = 1.0f / (COLS * ROWS); + *Address(row, i) = v * mul_; + } + + constexpr HWY_INLINE float* Address(const size_t row, const size_t i) const { + return block_ + row * COLS + i; + } + + private: + float* block_; +}; + +template +class FromLines { + public: + FromLines(const float* top_left, size_t stride) + : top_left_(top_left), stride_(stride) {} + + FromLines View(size_t dx, size_t dy) const { + return FromLines(Address(dx, dy), stride_); + } + + template + HWY_ATTR HWY_INLINE hwy::VT> LoadPart(const size_t row, + const size_t i) const { + return Load(BlockDesc(), Address(row, i)); + } + + HWY_ATTR HWY_INLINE hwy::VT> LoadVec(const size_t row, + size_t i) const { + return LoadPart(row, i); + } + + HWY_ATTR HWY_INLINE float Read(const size_t row, const size_t i) const { + return *Address(row, i); + } + + HWY_INLINE const float* Address(const size_t row, const size_t i) const { + return top_left_ + row * stride_ + i; + } + + private: + const float* HWY_RESTRICT top_left_; + size_t stride_; // move to next line by adding this to pointer +}; + +// Pointers are restrict-qualified: assumes we don't use both FromLines and +// ToLines in the same DCT. NOTE: Transpose uses From/ToBlock, not *Lines. +template +class ToLines { + public: + ToLines(float* top_left, size_t stride) + : top_left_(top_left), stride_(stride) {} + + ToLines View(const ToLines& other, size_t dx, size_t dy) const { + return ToLines(Address(dx, dy), stride_); + } + + template + HWY_ATTR HWY_INLINE void StorePart(const hwy::VT>& v, + const size_t row, const size_t i) const { + Store(v, BlockDesc(), Address(row, i)); + } + + HWY_ATTR HWY_INLINE void StoreVec(const hwy::VT>& v, + const size_t row, size_t i) const { + return StorePart(v, row, i); + } + + HWY_ATTR HWY_INLINE void Write(float v, const size_t row, + const size_t i) const { + *Address(row, i) = v; + } + + HWY_INLINE float* Address(const size_t row, const size_t i) const { + return top_left_ + row * stride_ + i; + } + + private: + float* HWY_RESTRICT top_left_; + size_t stride_; // move to next line by adding this to pointer +}; + +} // namespace jxl + +#endif // JXL_BLOCK_H_ diff --git a/jxl/brotli.cc b/jxl/brotli.cc new file mode 100644 index 000000000..f444e02de --- /dev/null +++ b/jxl/brotli.cc @@ -0,0 +1,117 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/brotli.h" + +#include // memcpy + +#include + +#include "brotli/decode.h" +#include "brotli/encode.h" +#include "jxl/base/status.h" + +namespace jxl { + +Status BrotliCompress(int quality, const uint8_t* in, const size_t in_size, + PaddedBytes* JXL_RESTRICT out) { + std::unique_ptr + enc(BrotliEncoderCreateInstance(nullptr, nullptr, nullptr), + BrotliEncoderDestroyInstance); + if (!enc) return JXL_FAILURE("BrotliEncoderCreateInstance failed"); + + BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_QUALITY, quality); + BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_LGWIN, 24); + BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_LGBLOCK, 0); + BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_MODE, BROTLI_MODE_FONT); + + const size_t kBufferSize = 128 * 1024; + PaddedBytes temp_buffer(kBufferSize); + + size_t avail_in = in_size; + const uint8_t* next_in = in; + + size_t total_out = 0; + + while (true) { + size_t out_size; + size_t avail_out = kBufferSize; + uint8_t* next_out = temp_buffer.data(); + if (!BrotliEncoderCompressStream(enc.get(), BROTLI_OPERATION_FINISH, + &avail_in, &next_in, &avail_out, &next_out, + &total_out)) { + return JXL_FAILURE("Brotli compression failed"); + } + out_size = next_out - temp_buffer.data(); + out->resize(out->size() + out_size); + memcpy(out->data() + out->size() - out_size, temp_buffer.data(), out_size); + if (BrotliEncoderIsFinished(enc.get())) break; + } + + return true; +} + +Status BrotliCompress(int quality, const uint8_t* in, const size_t in_size, + uint8_t* JXL_RESTRICT out, + size_t* JXL_RESTRICT total_out_size) { + PaddedBytes temp_buffer; + if (!BrotliCompress(quality, in, in_size, &temp_buffer)) { + return false; + } + *total_out_size = temp_buffer.size(); + memcpy(out, temp_buffer.data(), temp_buffer.size()); + return true; +} + +Status BrotliCompress(int quality, const PaddedBytes& in, + PaddedBytes* JXL_RESTRICT out) { + return BrotliCompress(quality, in.data(), in.size(), out); +} + +Status BrotliDecompress(const Span in, size_t max_output_size, + size_t* JXL_RESTRICT bytes_read, + PaddedBytes* JXL_RESTRICT out) { + std::unique_ptr + s(BrotliDecoderCreateInstance(nullptr, nullptr, nullptr), + BrotliDecoderDestroyInstance); + if (!s) return JXL_FAILURE("BrotliDecoderCreateInstance failed"); + + const size_t kBufferSize = 128 * 1024; + PaddedBytes temp_buffer(kBufferSize); + + size_t avail_in = in.size(); + if (in.size() == 0) return false; + const uint8_t* next_in = in.data(); + BrotliDecoderResult code; + + while (true) { + size_t out_size; + size_t avail_out = kBufferSize; + uint8_t* next_out = temp_buffer.data(); + code = BrotliDecoderDecompressStream(s.get(), &avail_in, &next_in, + &avail_out, &next_out, nullptr); + out_size = next_out - temp_buffer.data(); + out->resize(out->size() + out_size); + if (out->size() > max_output_size) + return JXL_FAILURE("Brotli output too large"); + memcpy(out->data() + out->size() - out_size, temp_buffer.data(), out_size); + if (code != BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) break; + } + if (code != BROTLI_DECODER_RESULT_SUCCESS) + return JXL_FAILURE("Brotli decompression failed"); + *bytes_read += (in.size() - avail_in); + return true; +} + +} // namespace jxl diff --git a/jxl/brotli.h b/jxl/brotli.h new file mode 100644 index 000000000..b1730d595 --- /dev/null +++ b/jxl/brotli.h @@ -0,0 +1,46 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BROTLI_H_ +#define JXL_BROTLI_H_ + +// Convenience functions for Brotli compression/decompression. + +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/span.h" +#include "jxl/base/status.h" + +namespace jxl { + +// Appends to out. +Status BrotliCompress(int quality, const PaddedBytes& in, + PaddedBytes* JXL_RESTRICT out); + +// Appends to out + *total_out_size. +Status BrotliCompress(int quality, const uint8_t* in, const size_t in_size, + uint8_t* JXL_RESTRICT out, + size_t* JXL_RESTRICT total_out_size); + +// Appends to out and ADDS to "bytes_read", which must be pre-initialized. +Status BrotliDecompress(const Span in, size_t max_output_size, + size_t* JXL_RESTRICT bytes_read, + PaddedBytes* JXL_RESTRICT out); + +} // namespace jxl + +#endif // JXL_BROTLI_H_ diff --git a/jxl/brotli_test.cc b/jxl/brotli_test.cc new file mode 100644 index 000000000..eac6fb22c --- /dev/null +++ b/jxl/brotli_test.cc @@ -0,0 +1,78 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/brotli.h" + +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/thread_pool_internal.h" + +namespace jxl { +namespace { + +TEST(BrotliTest, TestCompressEmpty) { + PaddedBytes in; + PaddedBytes out; + EXPECT_TRUE(BrotliCompress(6, in, &out)); + EXPECT_TRUE(in.empty()); +} + +TEST(BrotliTest, TestDecompressEmpty) { + size_t bytes_read = 0; + PaddedBytes in; + PaddedBytes out; + EXPECT_FALSE(BrotliDecompress(Span(in), 1, &bytes_read, &out)); + EXPECT_EQ(0, bytes_read); + EXPECT_TRUE(in.empty()); + EXPECT_TRUE(out.empty()); +} + +TEST(BrotliTest, TestRoundTrip) { + ThreadPoolInternal pool(0); + pool.Run(1, 65, ThreadPool::SkipInit(), [](const int task, const int thread) { + const size_t size = task; + + PaddedBytes in(size); + std::mt19937_64 rng(thread * 65537 + task * 129); + std::generate(in.begin(), in.end(), rng); + PaddedBytes compressed; + PaddedBytes out; + + for (int quality = 1; quality < 7; ++quality) { + compressed.clear(); + EXPECT_TRUE(BrotliCompress(quality, in, &compressed)); + size_t bytes_read = 0; + out.clear(); + EXPECT_TRUE(BrotliDecompress(Span(compressed), size, + &bytes_read, &out)); + EXPECT_EQ(compressed.size(), bytes_read); + EXPECT_EQ(in.size(), out.size()); + for (size_t i = 0; i < in.size(); ++i) { + if (in[i] != out[i]) { + printf("Mismatch at %zu (%zu)\n", i, size); + exit(1); + } + } + } + }); +} + +} // namespace +} // namespace jxl diff --git a/jxl/brunsli.cc b/jxl/brunsli.cc new file mode 100644 index 000000000..803cc5698 --- /dev/null +++ b/jxl/brunsli.cc @@ -0,0 +1,1702 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/brunsli.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "jxl/block.h" +#include "jxl/color_encoding.h" +#include "jxl/color_management.h" +#include "jxl/common.h" +#include "jxl/dct.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/dec_xyb.h" +#include "jxl/gaborish.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/image_ops.h" +#include "third_party/brunsli/c/common/constants.h" +#include "third_party/brunsli/c/common/context.h" +#include "third_party/brunsli/c/dec/state.h" +#include "third_party/brunsli/c/enc/state.h" + +namespace jxl { + +namespace { + +const uint8_t kBrunsliMagic[] = {0x0A, 0x04, 'B', 0xd2, 0xd5, 'N', 0x12}; +constexpr const size_t kBrunsliMagicSize = sizeof(kBrunsliMagic); + +const uint8_t kBrunsliXHdr[] = {'B', 'R', 'N', ':', 'H', 'D', 'R'}; + +const uint8_t kIccProfileTag[] = {'I', 'C', 'C', '_', 'P', 'R', + 'O', 'F', 'I', 'L', 'E', 0x00}; + +// JPEG markers. +constexpr const uint8_t kSof0 = 0xC0; +constexpr const uint8_t kDht = 0xC4; +constexpr const uint8_t kEoi = 0xD9; +constexpr const uint8_t kSos = 0xDA; +constexpr const uint8_t kDqt = 0xDB; +constexpr const uint8_t kApp0 = 0xE0; +constexpr const uint8_t kApp2 = 0xE2; +constexpr const uint8_t kApp9 = 0xE9; + +constexpr const uint32_t kHuffmanCodeDcSlotOffset = 0x00; +constexpr const uint32_t kHuffmanCodeAcSlotOffset = 0x10; + +// JFIF / v1.1 / no units / square pixel / no thumbnail. +const uint8_t kApp0Template[] = {kApp0, 0x00, 0x10, 0x4A, 0x46, 0x49, + 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00}; +constexpr const size_t kApp0TemplateSize = sizeof(kApp0Template); + +/*struct EntropySource { + PaddedBytes payload; + bool active = false; + std::unique_ptr reader; + ANSCode code; + std::vector context_map; + std::unique_ptr decoder; +};*/ + +struct BrunsliExtensions { + std::string hdr_orig_colorspace; + std::string hdr_colorspace; +}; + +using ByteSpan = Span; + +// Transform RGB to YCbCr. +// Could be performed in-place (i.e. Y, Cb and Cr could alias R, B and B). +HWY_ATTR void RgbToYcbcr(const ImageF& r_plane, const ImageF& g_plane, + const ImageF& b_plane, ImageF* y_plane, + ImageF* cb_plane, ImageF* cr_plane) { + const HWY_FULL(float) df; + constexpr size_t S = df.N; // Step. + + const size_t xsize = r_plane.xsize(); + const size_t ysize = r_plane.ysize(); + + const auto cm128 = Set(df, -128.0f); + const auto c05 = Set(df, 0.5f); + const auto cyr = Set(df, 0.299f); + const auto cyg = Set(df, 0.587f); + const auto cyb = Set(df, 0.114f); + const auto ccbr = Set(df, -0.1687f); + const auto ccbg = Set(df, -0.3313f); + const auto& ccbb = c05; + const auto& ccrr = c05; + const auto ccrg = Set(df, -0.4187f); + const auto ccrb = Set(df, -0.0813f); + for (size_t y = 0; y < ysize; ++y) { + const float* JXL_RESTRICT r_row = r_plane.ConstRow(y); + const float* JXL_RESTRICT g_row = g_plane.ConstRow(y); + const float* JXL_RESTRICT b_row = b_plane.ConstRow(y); + float* JXL_RESTRICT y_row = y_plane->Row(y); + float* JXL_RESTRICT cb_row = cb_plane->Row(y); + float* JXL_RESTRICT cr_row = cr_plane->Row(y); + for (size_t x = 0; x < xsize; x += S) { + const auto r_vec = Load(df, r_row + x); + const auto g_vec = Load(df, g_row + x); + const auto b_vec = Load(df, b_row + x); + const auto y_vec = + MulAdd(cyr, r_vec, MulAdd(cyg, g_vec, MulAdd(cyb, b_vec, cm128))); + const auto cb_vec = + MulAdd(ccbr, r_vec, MulAdd(ccbg, g_vec, ccbb * b_vec)); + const auto cr_vec = + MulAdd(ccrr, r_vec, MulAdd(ccrg, g_vec, ccrb * b_vec)); + Store(y_vec, df, y_row + x); + Store(cb_vec, df, cb_row + x); + Store(cr_vec, df, cr_row + x); + } + } +} + +struct GetDcCoeff { + const ::brunsli::coeff_t* JXL_RESTRICT src; + size_t xsize_blocks; + + ::brunsli::coeff_t operator()(size_t u, size_t v) const { + return src[(u + v * xsize_blocks) * kDCTBlockSize]; + } +}; + +struct GetAcCoeff { + const ::brunsli::coeff_t* JXL_RESTRICT src; + size_t xsize_blocks; + + ::brunsli::coeff_t operator()(size_t u, size_t v) const { + return src[(u + v * xsize_blocks) * kDCTBlockSize + 1]; + } +}; + +struct GetDcCoeffTransposed { + const ::brunsli::coeff_t* JXL_RESTRICT src; + size_t xsize_blocks; + + ::brunsli::coeff_t operator()(size_t u, size_t v) const { + return src[(v + u * xsize_blocks) * kDCTBlockSize]; + } +}; + +struct GetAcCoeffTransposed { + const ::brunsli::coeff_t* JXL_RESTRICT src; + size_t xsize_blocks; + + ::brunsli::coeff_t operator()(size_t u, size_t v) const { + return src[(v + u * xsize_blocks) * kDCTBlockSize + 8]; + } +}; + +struct MakeSlope { + template + bool operator()(const GetDcCoeffFn& get_dc_coeff, + const GetAcCoeffFn& get_ac_coeff, float q_ac_dc, size_t y, + size_t x0, size_t x1, size_t xsize, float* JXL_RESTRICT from, + float* JXL_RESTRICT to) { + const float current = static_cast(get_dc_coeff(x0, y)); + float ll = current; + const float lr = current; + if (x0 != 0) { + ll = get_dc_coeff(x0 - 1, y) - q_ac_dc * get_ac_coeff(x0 - 1, y); + } + const float l = (ll + lr) * 0.5f; + + const float rl = current; + float rr = current; + if (x1 != xsize - 1) { + rr = get_dc_coeff(x1 + 1, y) + q_ac_dc * get_ac_coeff(x1 + 1, y); + } + const float r = (rl + rr) * 0.5f; + + const float l_delta = l - current; + const float r_delta = r - current; + const float l_delta_abs = std::abs(l_delta); + const float r_delta_abs = std::abs(r_delta); + static constexpr const float kThreshold = 0.5f; + const float step_threshold = 0.5f; + bool is_sane = (std::abs(l - r) > step_threshold) && + (l_delta_abs <= kThreshold) && (r_delta_abs <= kThreshold); + *from = l; + *to = r; + return is_sane; + } +}; + +// First and last indices (inclusive) of the range of repeated DC values. +struct Range { + size_t x0; + size_t x1; + Range(size_t left, size_t right) : x0(left), x1(right){}; +}; +typedef std::vector RowRanges; +typedef std::vector Ranges; + +template +Ranges FindPlateaux(const GetDcCoeffFn& get_dc_coeff, + const GetAcCoeffFn& get_ac_coeff, size_t xsize, + size_t ysize) { + Ranges result(ysize); + for (size_t y = 0; y < ysize; ++y) { + RowRanges& row = result[y]; + ::brunsli::coeff_t span_dc = get_dc_coeff(0, y); + size_t span_start = 0; + for (size_t x = 0; x <= xsize; ++x) { + bool end = (x == xsize); + ::brunsli::coeff_t dc = end ? (span_dc + 8192) : get_dc_coeff(x, y); + bool step = span_dc != dc; + bool bump = end || (get_ac_coeff(x, y) != 0); + if (step || bump) { + if (span_start < x) row.emplace_back(span_start, x - 1); + span_start = x + (bump ? 1 : 0); + span_dc = dc; + } + } + } + return result; +} + +template +void BuildSlopes(const GetDcCoeffFn& get_dc_coeff, + const GetAcCoeffFn& get_ac_coeff, SlopeFn* make_slope, + const float q_ac_dc, const Ranges& ranges, ImageF* approx, + ImageB* presence, ImageF* slope) { + const size_t xsize = approx->xsize(); + const size_t ysize = approx->ysize(); + + for (size_t y = 0; y < ysize; ++y) { + float* JXL_RESTRICT approx_row = approx->Row(y); + float* JXL_RESTRICT slope_row = slope->Row(y); + uint8_t* JXL_RESTRICT presence_row = presence->Row(y); + std::fill_n(presence_row, xsize, 0); + for (auto& range : ranges[y]) { + const size_t x0 = range.x0; + const size_t x1 = range.x1; + float from; + float to; + bool has_slope = (*make_slope)(get_dc_coeff, get_ac_coeff, q_ac_dc, y, x0, + x1, xsize, &from, &to); + if (!has_slope) continue; + const size_t span = x1 - x0 + 1; + const float delta = to - from; + const float step = delta / span; + const float base = from + 0.5f * step; + for (size_t x = x0; x <= x1; ++x) { + presence_row[x] = 1; + const float diff = static_cast(x - x0); + approx_row[x] = base + diff * step; + // Base function f(u, k) = k * cos(u / Pi) -> f(0, k) = k, f(1, k) = -k. + // To stitch consecutive "steps" we want + // f(0) = -0.5 * step, f(1) = 0.5 * step + slope_row[x] = -0.5f * step; + } + } + } +} + +HWY_ATTR void FixDc(const ::brunsli::coeff_t* JXL_RESTRICT coeffs, int q_dc, + int q_ach, int q_acv, size_t xsize_blocks, + size_t ysize_blocks, float* JXL_RESTRICT dst, + size_t dst_stride) { + constexpr size_t N = kBlockDim; + static_assert(N == 8, "JPEG block dim must be 8"); + static_assert(kDCTBlockSize == N * N, "JPEG block size must be 64"); + const float dequant_mult = + static_cast(q_dc) * IDCTScales()[0] * IDCTScales()[0]; + const float q_ach_dc = static_cast(q_ach) / static_cast(q_dc); + const float q_acv_dc = static_cast(q_acv) / static_cast(q_dc); + + MakeSlope make_slope; + + const GetDcCoeff get_dc_coeff = {coeffs, xsize_blocks}; + const GetAcCoeff get_ac_coeff = {coeffs, xsize_blocks}; + Ranges ranges_h = + FindPlateaux(get_dc_coeff, get_ac_coeff, xsize_blocks, ysize_blocks); + ImageF approx_h(xsize_blocks, ysize_blocks); + ImageF slope_h(xsize_blocks, ysize_blocks); + ImageB has_h(xsize_blocks, ysize_blocks); + BuildSlopes(get_dc_coeff, get_ac_coeff, &make_slope, q_ach_dc, ranges_h, + &approx_h, &has_h, &slope_h); + + const GetDcCoeffTransposed get_dc_t_coeff = {coeffs, xsize_blocks}; + const GetAcCoeffTransposed get_ac_t_coeff = {coeffs, xsize_blocks}; + Ranges ranges_v = + FindPlateaux(get_dc_t_coeff, get_ac_t_coeff, ysize_blocks, xsize_blocks); + // NB: transposed. + ImageF approx_v(ysize_blocks, xsize_blocks); + ImageF slope_v(ysize_blocks, xsize_blocks); + ImageB has_v(ysize_blocks, xsize_blocks); + BuildSlopes(get_dc_t_coeff, get_ac_t_coeff, &make_slope, q_acv_dc, ranges_v, + &approx_v, &has_v, &slope_v); + + for (size_t by = 0; by < ysize_blocks; ++by) { + const ::brunsli::coeff_t* JXL_RESTRICT coeffs_row = + &coeffs[by * xsize_blocks * kDCTBlockSize]; + const uint8_t* JXL_RESTRICT has_h_row = has_h.ConstRow(by); + const float* JXL_RESTRICT approx_h_row = approx_h.ConstRow(by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + const bool has_h_val = (has_h_row[bx] != 0); + const bool has_v_val = (has_v.ConstRow(bx)[by] != 0); + float fixed_val; + if (has_h_val && has_v_val) { + fixed_val = 0.5f * (approx_h_row[bx] + approx_v.ConstRow(bx)[by]); + } else if (has_h_val) { + fixed_val = approx_h_row[bx]; + } else if (has_v_val) { + fixed_val = approx_v.ConstRow(bx)[by]; + } else { + fixed_val = coeffs_row[bx * kDCTBlockSize]; + } + dst[by * dst_stride + bx * kDCTBlockSize] = fixed_val * dequant_mult; + } + } + + for (size_t by = 0; by < ysize_blocks; ++by) { + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + const int16_t* JXL_RESTRICT src_coeffs = + &coeffs[(by * xsize_blocks + bx) * kDCTBlockSize]; + const bool has_v_val = (has_v.ConstRow(bx)[by] != 0); + const float slope_v_val = slope_v.ConstRow(bx)[by]; + if (has_v_val && (src_coeffs[8] == 0)) { + dst[by * dst_stride + bx * kDCTBlockSize + 8] = + slope_v_val * dequant_mult; + } + } + } + for (size_t by = 0; by < ysize_blocks; ++by) { + const uint8_t* JXL_RESTRICT has_h_row = has_h.ConstRow(by); + const float* JXL_RESTRICT slope_h_row = slope_h.ConstRow(by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + const bool has_h_val = (has_h_row[bx] != 0); + const float slope_h_val = slope_h_row[bx]; + const int16_t* JXL_RESTRICT src_coeffs = + &coeffs[(by * xsize_blocks + bx) * kDCTBlockSize]; + if (has_h_val && (src_coeffs[1] == 0)) { + dst[by * dst_stride + bx * kDCTBlockSize + 1] = + slope_h_val * dequant_mult; + } + } + } +} + +HWY_ATTR Status JpegDataToPixels(const brunsli::JPEGData& src, + const BrunsliDecoderOptions& options, + const BrunsliExtensions& extensions, + Image3F* out, bool* is_gray, + ThreadPool* pool) { + constexpr size_t N = kBlockDim; + static_assert(N == 8, "JPEG block dim must be 8"); + static_assert(kDCTBlockSize == N * N, "JPEG block size must be 64"); + const size_t xsize = src.width; + const size_t ysize = src.height; + + const HWY_FULL(float) df; + constexpr size_t S = df.N; // Step. + static_assert(kDCTBlockSize % S == 0, + "Block size should be divisible by SIMD length"); + const HWY_CAPPED(int32_t, S) di32; + const HWY_CAPPED(int16_t, S) di16; + JXL_RETURN_IF_ERROR(src.components.size() == 1 || src.components.size() == 3); + *is_gray = src.components.size() == 1; + + ImageF planes[3]; + + const size_t xmcu = N * src.max_h_samp_factor; + const size_t ymcu = N * src.max_v_samp_factor; + + for (size_t c = 0; c < src.components.size(); ++c) { + const brunsli::JPEGComponent& component = src.components[c]; + const size_t factor_x = src.max_h_samp_factor / component.h_samp_factor; + const size_t factor_y = src.max_v_samp_factor / component.v_samp_factor; + JXL_RETURN_IF_ERROR(src.max_h_samp_factor % component.h_samp_factor == 0); + JXL_RETURN_IF_ERROR(src.max_v_samp_factor % component.v_samp_factor == 0); + JXL_RETURN_IF_ERROR(component.quant_idx < + static_cast(src.quant.size())); + const size_t comp_xsize = component.width_in_blocks * N * factor_x; + const size_t comp_ysize = component.height_in_blocks * N * factor_y; + JXL_RETURN_IF_ERROR(xsize <= comp_xsize); + JXL_RETURN_IF_ERROR(xsize > comp_xsize - xmcu); + JXL_RETURN_IF_ERROR(ysize <= comp_ysize); + JXL_RETURN_IF_ERROR(ysize > comp_ysize - ymcu); + if (c == 0) { + JXL_RETURN_IF_ERROR((factor_x == 1) && (factor_y == 1)); + } else { + JXL_RETURN_IF_ERROR((factor_x <= 2) && (factor_y <= 2)); + } + } + + for (size_t c = 0; c < src.components.size(); ++c) { + const brunsli::JPEGComponent& component = src.components[c]; + const brunsli::JPEGQuantTable& quant_table = src.quant[component.quant_idx]; + const size_t xsize_blocks = component.width_in_blocks; + const size_t ysize_blocks = component.height_in_blocks; + ImageF pixels(xsize_blocks * N, ysize_blocks * N); + const size_t pixels_stride = pixels.PixelsPerRow(); + const size_t factor_x = src.max_h_samp_factor / component.h_samp_factor; + const size_t factor_y = src.max_v_samp_factor / component.v_samp_factor; + + HWY_ALIGN float quant[kDCTBlockSize]; + + for (size_t y = 0; y < N; ++y) { + for (size_t x = 0; x < N; ++x) { + const size_t i = y * N + x; + quant[i] = static_cast(quant_table.values[i]) * + IDCTScales()[x] * IDCTScales()[y]; + } + } + + // Dequantize. + constexpr size_t group_dim = kGroupDimInBlocks; + ImageF dequantized(xsize_blocks * kDCTBlockSize, ysize_blocks); + const size_t xsize_groups = DivCeil(xsize_blocks, group_dim); + const size_t ysize_groups = DivCeil(ysize_blocks, group_dim); + const auto dequantize = [&](int idx, int /* thread */) HWY_ATTR { + HWY_ALIGN int16_t coeffs[kDCTBlockSize]; + const size_t gx = idx % xsize_groups; + const size_t gy = idx / xsize_groups; + const Rect group_rect_blocks(gx * group_dim, gy * group_dim, group_dim, + group_dim, xsize_blocks, ysize_blocks); + const size_t bx0 = group_rect_blocks.x0(); + const size_t bx1 = bx0 + group_rect_blocks.xsize(); + const size_t by0 = group_rect_blocks.y0(); + const size_t by1 = by0 + group_rect_blocks.ysize(); + for (size_t by = by0; by < by1; ++by) { + float* JXL_RESTRICT dequantized_row = dequantized.Row(by); + for (size_t bx = bx0; bx < bx1; ++bx) { + float* JXL_RESTRICT dequantized_block = + dequantized_row + bx * kDCTBlockSize; + const int16_t* JXL_RESTRICT src_coeffs = + &component.coeffs[(by * xsize_blocks + bx) * kDCTBlockSize]; + memcpy(coeffs, src_coeffs, sizeof(int16_t) * kDCTBlockSize); + + for (size_t i = 0; i < kDCTBlockSize; i += S) { + const auto coeff = + ConvertTo(df, ConvertTo(di32, Load(di16, coeffs + i))); + const auto mult = Load(df, quant + i); + Store(coeff * mult, df, dequantized_block + i); + } + } + } + }; + RunOnPool(pool, 0, static_cast(xsize_groups * ysize_groups), + ThreadPool::SkipInit(), dequantize, "Brunsli:Dequantize"); + + // TODO(eustas): turn to pipeline and parallelize. + if (options.fix_dc_staircase) { + const int q_dc = src.quant[component.quant_idx].values[0]; + const int q_ach = src.quant[component.quant_idx].values[1]; + const int q_acv = src.quant[component.quant_idx].values[8]; + FixDc(component.coeffs.data(), q_dc, q_ach, q_acv, xsize_blocks, + ysize_blocks, dequantized.Row(0), dequantized.PixelsPerRow()); + } + + const auto idct = [&](int idx, int /* thread */) HWY_ATTR { + const size_t gx = idx % xsize_groups; + const size_t gy = idx / xsize_groups; + const Rect group_rect_blocks(gx * group_dim, gy * group_dim, group_dim, + group_dim, xsize_blocks, ysize_blocks); + const size_t bx0 = group_rect_blocks.x0(); + const size_t bx1 = bx0 + group_rect_blocks.xsize(); + const size_t by0 = group_rect_blocks.y0(); + const size_t by1 = by0 + group_rect_blocks.ysize(); + for (size_t by = by0; by < by1; ++by) { + const float* JXL_RESTRICT dequantized_row = dequantized.Row(by); + float* JXL_RESTRICT pixels_row = pixels.Row(by * N); + for (size_t bx = bx0; bx < bx1; ++bx) { + ComputeTransposedScaledIDCT()( + FromBlock(dequantized_row + bx * kDCTBlockSize), + ToLines(pixels_row + bx * N, pixels_stride)); + GenericTransposeBlockInplace( + FromLines(pixels_row + bx * N, pixels_stride), + ToLines(pixels_row + bx * N, pixels_stride)); + } + } + }; + RunOnPool(pool, 0, static_cast(xsize_groups * ysize_groups), + ThreadPool::SkipInit(), idct, "Brunsli:IDCT"); + + // TODO: before or after upsampling? + if (options.gaborish) { + ImageF gab_pixels(pixels.xsize(), pixels.ysize()); + ConvolveGaborish(pixels, 0.2, 0.2, pool, &gab_pixels); + for (size_t by = 0; by < ysize_blocks; ++by) { + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + const float* JXL_RESTRICT gab_pixels_row = + gab_pixels.Row(by * N) + bx * N; + float* JXL_RESTRICT pixels_row = pixels.Row(by * N) + bx * N; + size_t count = 0; + for (size_t u = 1; u < N - 1; ++u) { + for (size_t v = 1; v < N - 1; ++v) { + float a = gab_pixels_row[u + pixels_stride * v]; + float b = pixels_row[u + pixels_stride * v]; + if (std::abs(a - b) > 5) count++; + } + } + if (count >= 9) continue; + // TODO: vectorize + for (size_t u = 0; u < N; ++u) { + for (size_t v = 0; v < N; ++v) { + pixels_row[u + pixels_stride * v] = + gab_pixels_row[u + pixels_stride * v]; + } + } + } + } + } + + if (factor_x == 1) { + if (factor_y == 1) { + planes[c] = std::move(pixels); + } else { + planes[c] = UpsampleV2(pixels, pool); + } + } else { + if (factor_y == 1) { + planes[c] = UpsampleH2(pixels, pool); + } else { + planes[c] = UpsampleV2(UpsampleH2(pixels, pool), pool); + } + } + } + + if (*is_gray) { + const auto c128 = Set(df, 128.0f); + for (size_t y = 0; y < ysize; ++y) { + float* JXL_RESTRICT y_row = planes[0].Row(y); + for (size_t x = 0; x < xsize; x += S) { + Store(Load(df, y_row + x) + c128, df, y_row + x); + } + } + } else { + YcbcrToRgb(planes[0], planes[1], planes[2], &planes[0], &planes[1], + &planes[2], pool); + } + + if (*is_gray) { + planes[1] = CopyImage(planes[0]); + planes[2] = CopyImage(planes[0]); + } + Image3F rgb(std::move(planes[0]), std::move(planes[1]), std::move(planes[2])); + rgb.ShrinkTo(xsize, ysize); + *out = std::move(rgb); + + return true; +} + +bool GetMarkerPayload(const std::string& marker, ByteSpan* payload) { + if (marker.size() < 3) { + return false; + } + size_t hi = marker[1]; + size_t lo = marker[2]; + size_t size = (hi << 8u) | lo; + // Second byte of marker is not counted towards size. + if (size != marker.size() - 1) { + return false; + } + // cut second marker byte and "length" from payload. + *payload = ByteSpan(marker); + payload->remove_prefix(3); + return true; +} + +Status ParseChunkedMarker(const brunsli::JPEGData& src, uint8_t marker_type, + const ByteSpan& tag, PaddedBytes* output) { + output->clear(); + + std::vector chunks; + std::vector presence; + size_t expected_number_of_parts = 0; + bool is_first_chunk = true; + for (const std::string& marker : src.app_data) { + if (marker.empty() || marker[0] != marker_type) { + continue; + } + ByteSpan payload; + if (!GetMarkerPayload(marker, &payload)) { + // Something is wrong with this marker; does not care. + continue; + } + if ((payload.size() < tag.size()) || + memcmp(payload.data(), tag.data(), tag.size()) != 0) { + continue; + } + payload.remove_prefix(tag.size()); + if (payload.size() < 2) { + return JXL_FAILURE("Chunk is too small."); + } + uint8_t index = payload[0]; + uint8_t total = payload[1]; + payload.remove_prefix(2); + + JXL_RETURN_IF_ERROR(total != 0); + if (is_first_chunk) { + is_first_chunk = false; + expected_number_of_parts = total; + // 1-based indices; 0-th element is added for convenience. + chunks.resize(total + 1); + presence.resize(total + 1); + } else { + JXL_RETURN_IF_ERROR(expected_number_of_parts == total); + } + + if (index == 0 || index > total) { + return JXL_FAILURE("Invalid chunk index."); + } + + if (presence[index]) { + return JXL_FAILURE("Duplicate chunk."); + } + presence[index] = true; + chunks[index] = payload; + } + + for (size_t i = 0; i < expected_number_of_parts; ++i) { + // 0-th element is not used. + size_t index = i + 1; + if (!presence[index]) { + return JXL_FAILURE("Missing chunk."); + } + output->append(chunks[index]); + } + + return true; +} + +Status AddChunkedMarker(brunsli::JPEGData* out, uint8_t marker_type, + const ByteSpan& tag, const PaddedBytes& payload) { + // 2 bytes encode size itself, 2 bytes are index / total. + const size_t kChunkOverhead = 4 + tag.size(); + // Chunk size is an uint16_t. + constexpr const size_t kChunkSizeBits = 16; + const size_t kMaxChunkSize = (1u << kChunkSizeBits) - kChunkOverhead; + + const size_t full_size = payload.size(); + const size_t num_chunks = DivCeil(full_size, kMaxChunkSize); + if (num_chunks == 0 || num_chunks > 255) { + return false; + } + for (size_t i = 0; i < num_chunks; ++i) { + const size_t start = i * kMaxChunkSize; + const size_t end = std::min(start + kMaxChunkSize, full_size); + const size_t part_length = end - start; + const size_t chunk_length = part_length + kChunkOverhead; + + JXL_ASSERT((chunk_length >> kChunkSizeBits) == 0); + const uint8_t hi = chunk_length >> 8u; + const uint8_t lo = chunk_length & 0xFFu; + + std::string extension_chunk; + extension_chunk.push_back(marker_type); + extension_chunk.push_back(hi); + extension_chunk.push_back(lo); + extension_chunk.append(reinterpret_cast(tag.data()), + tag.size()); + extension_chunk.push_back(i + 1); + extension_chunk.push_back(num_chunks); + extension_chunk.append( + reinterpret_cast(payload.data() + start), part_length); + + // 1 extra for marker byte. + JXL_ASSERT(extension_chunk.size() == (chunk_length + 1)); + out->app_data.push_back(extension_chunk); + out->marker_order.push_back(marker_type); + } + return true; +} + +/*void ParseEntropySource(const brunsli::JPEGData& src, EntropySource* dst, + ByteSpan tag) { + if (!ParseChunkedMarker(src, kApp9, tag, &dst->payload)) { + dst->payload.clear(); + JXL_WARNING("ReJPEG: corrupted extension payload\n"); + return; + } + if (dst->payload.empty()) { + return; + } + + dst->reader = make_unique(dst->payload); + if (!DecodeHistograms(dst->reader.get(), 1, 32, &dst->code, + &dst->context_map)) { + JXL_WARNING("ReJPEG: failed to decode extension histograms\n"); + return; + } + + dst->decoder = make_unique(&dst->code, dst->reader.get()); + dst->active = true; +}*/ + +BrunsliExtensions ParseBrunsliExtensions(const brunsli::JPEGData& src) { + BrunsliExtensions result{}; + /*for (const std::string& marker : src.app_data) { + // Filter out non-APP9 markers. + if (marker.empty() || marker[0] != kApp9) { + continue; + } + ByteSpan payload; + if (!GetMarkerPayload(marker, &payload)) { + // Something is wrong with this marker; does not care. + continue; + } + }*/ + + PaddedBytes hdr_payload; + if (!ParseChunkedMarker(src, kApp9, ByteSpan(kBrunsliXHdr), &hdr_payload)) { + hdr_payload.clear(); + JXL_WARNING("ReJPEG: corrupted hdr extension payload\n"); + } + if (!hdr_payload.empty()) { + std::string colorspaces = std::string( + reinterpret_cast(hdr_payload.data()), hdr_payload.size()); + const size_t pos = colorspaces.find('>', 0); + if (pos != std::string::npos) { + result.hdr_orig_colorspace = colorspaces.substr(0, pos); + result.hdr_colorspace = colorspaces.substr(pos + 1); + } + } + + return result; +} + +// Update |data| to point to the start of the next section. +bool SkipSection(const uint8_t** data, size_t len) { + size_t section_len = 0; + uint64_t b = 0x80; + size_t off = 1; + for (size_t i = 0; (i < 9) && (b & 0x80u); ++i) { + if (off >= len) return false; + b = (*data)[off++]; + section_len |= (b & 0x7Fu) << (i * 7); + } + if ((b & 0x80u) != 0) return false; + off += section_len; + if (off > len) return false; + *data += off; + return true; +} + +} // namespace + +Status BrunsliToPixels(jxl::Span compressed, + jxl::CodecInOut* JXL_RESTRICT io, + const BrunsliDecoderOptions& options, + BrunsliDecoderMeta* metadata, jxl::ThreadPool* pool) { + io->enc_size = compressed.size(); + + ::brunsli::JPEGData jpg; + ::brunsli::BrunsliStatus status = + ::brunsli::BrunsliDecodeJpeg(compressed.data(), compressed.size(), &jpg); + if (status != ::brunsli::BRUNSLI_OK) { + return JXL_FAILURE("Failed to parse Brunsli input."); + } + + BrunsliExtensions extensions = ParseBrunsliExtensions(jpg); + + PaddedBytes icc_profile; + if (!ParseChunkedMarker(jpg, kApp2, ByteSpan(kIccProfileTag), &icc_profile)) { + JXL_WARNING("ReJPEG: corrupted ICC profile\n"); + icc_profile.clear(); + } + + Image3F rgb; + bool is_gray; + JXL_RETURN_IF_ERROR( + JpegDataToPixels(jpg, options, extensions, &rgb, &is_gray, pool)); + + ColorEncoding color_encoding; + if (!extensions.hdr_colorspace.empty()) { + JXL_RETURN_IF_ERROR( + ParseDescription(extensions.hdr_colorspace, &color_encoding)); + JXL_RETURN_IF_ERROR(ColorManagement::CreateProfile(&color_encoding)); + } else { + if (!ColorManagement::SetProfile(std::move(icc_profile), &color_encoding)) { + color_encoding = ColorManagement::SRGB(is_gray); + } + } + + // TODO(eustas): also import EXIF, etc. + metadata->hdr_orig_colorspace = extensions.hdr_orig_colorspace; + + io->metadata.bits_per_sample = + extensions.hdr_orig_colorspace.empty() ? 8 : 32; + io->metadata.color_encoding = color_encoding; + io->SetFromImage(std::move(rgb), color_encoding); + + io->dec_pixels += jpg.width * jpg.height; + + return true; +} + +// TODO(eustas): use VerifySignature from Brunsli library. +BrunsliFileSignature IsBrunsliFile(jxl::Span compressed) { + if (memcmp(compressed.data(), kBrunsliMagic, + std::min(kBrunsliMagicSize, compressed.size())) != 0) { + return BrunsliFileSignature::kInvalid; + } + if (compressed.size() < kBrunsliMagicSize) { + return BrunsliFileSignature::kNotEnoughData; + } + return BrunsliFileSignature::kBrunsli; +} + +HWY_ATTR ImageF Dct(const ImageF& image) { + constexpr size_t N = kBlockDim; + static_assert(N == 8, "JPEG block dim must be 8"); + static_assert(kDCTBlockSize == N * N, "JPEG block size must be 64"); + + JXL_ASSERT(image.xsize() % N == 0); + JXL_ASSERT(image.ysize() % N == 0); + const size_t xsize_blocks = image.xsize() / N; + const size_t ysize_blocks = image.ysize() / N; + ImageF dct(xsize_blocks * kDCTBlockSize, ysize_blocks); + + for (size_t by = 0; by < ysize_blocks; ++by) { + const float* JXL_RESTRICT row_in = image.ConstRow(by * kBlockDim); + float* JXL_RESTRICT row_dct = dct.Row(by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + ComputeTransposedScaledDCT()( + FromLines(row_in + bx * kBlockDim, image.PixelsPerRow()), + ScaleToBlock(row_dct + bx * kDCTBlockSize)); + } + } + return dct; +} + +namespace { + +const uint8_t kDefaultQuantMatrix[2][64] = { + {16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99}, + {17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}}; + +void FillQuantMatrix(bool is_chroma, float scale, std::vector* dst) { + const uint8_t* const in = kDefaultQuantMatrix[is_chroma]; + for (uint32_t i = 0; i < kDCTBlockSize; ++i) { + const uint32_t v = static_cast(in[i]) * scale; + dst->at(i) = (v < 1) ? 1 : (v > 255) ? 255u : v; + } +} + +// Convert YCbCr pixels to JPEGData components. +void ConvertPixels(const Image3F& from, brunsli::JPEGData* to, + const int quant_map[3], jxl::ThreadPool* pool) { + constexpr size_t N = kBlockDim; + // TODO(eustas): consider grayscale / CMYK. + const size_t num_c = 3; + + for (size_t c = 0; c < num_c; ++c) { + // TODO(eustas): use pool. + const ImageF& plane = Dct(from.Plane(c)); + ::brunsli::JPEGComponent component; + component.id = static_cast(c) + 1; // YCbCr + component.h_samp_factor = 1; + component.v_samp_factor = 1; + component.quant_idx = quant_map[c]; + const int* JXL_RESTRICT quant_table = + to->quant[component.quant_idx].values.data(); + const size_t xsize_blocks = component.h_samp_factor * to->MCU_cols; + component.width_in_blocks = xsize_blocks; + const size_t ysize_blocks = component.v_samp_factor * to->MCU_rows; + component.height_in_blocks = ysize_blocks; + const size_t num_blocks = xsize_blocks * ysize_blocks; + component.num_blocks = num_blocks; + std::vector<::brunsli::coeff_t> coeffs(num_blocks * kDCTBlockSize); + // TODO(eustas): use pool. + for (size_t by = 0; by < ysize_blocks; ++by) { + const float* JXL_RESTRICT from_row = plane.ConstRow(by); + ::brunsli::coeff_t* JXL_RESTRICT to_row = + &coeffs[by * xsize_blocks * kDCTBlockSize]; + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + const float* JXL_RESTRICT from_block = &from_row[bx * kDCTBlockSize]; + ::brunsli::coeff_t* JXL_RESTRICT to_block = &to_row[bx * kDCTBlockSize]; + // TODO(eustas): SIMDify + for (size_t u = 0; u < N; ++u) { + for (size_t v = 0; v < N; ++v) { + size_t to_idx = u * N + v; + size_t from_idx = v * N + u; + float raw = (static_cast(N * N) * DCTScales()[u] * + DCTScales()[v]) * + from_block[from_idx]; + raw += std::copysign(0.5f * static_cast(quant_table[to_idx]), + raw); + to_block[to_idx] = raw / static_cast(quant_table[to_idx]); + } + } + } + } + + component.coeffs = std::move(coeffs); + to->components.emplace_back(std::move(component)); + } +} + +} // namespace + +Status PixelsToBrunsli(const jxl::CodecInOut* JXL_RESTRICT io, + jxl::PaddedBytes* compressed, + const BrunsliEncoderOptions& options, + jxl::ThreadPool* pool) { + constexpr size_t N = kBlockDim; + static_assert(N == 8, "JPEG block dim must be 8"); + static_assert(kDCTBlockSize == N * N, "JPEG block size must be 64"); + + const jxl::ImageBundle& ib = io->Main(); + if (!ib.HasColor()) { + return false; + } + const size_t num_c = 3; + + const size_t xsize = io->xsize(); + const size_t ysize = io->ysize(); + + const Image3F& src = PadImageToMultiple(ib.color(), N); + ImageF planes[3]; + for (auto& plane : planes) { + plane = ImageF(src.xsize(), src.ysize()); + } + RgbToYcbcr(src.Plane(0), src.Plane(1), src.Plane(2), &planes[0], &planes[1], + &planes[2]); + + ::brunsli::JPEGData out; + out.width = xsize; + out.height = ysize; + out.version = 0; // regular Brunsli + out.restart_interval = 0; // don't care + out.max_h_samp_factor = 1; + out.max_v_samp_factor = 1; + out.MCU_rows = DivCeil(out.height, out.max_v_samp_factor * N); + out.MCU_cols = DivCeil(out.width, out.max_h_samp_factor * N); + out.has_zero_padding_bit = false; + + out.app_data.emplace_back(reinterpret_cast(kApp0Template), + kApp0TemplateSize); + out.marker_order.push_back(kApp0); + + if (options.hdr_orig_colorspace.empty()) { + // TODO(eustas): use "predefined profile" optimization, when eligible. + if (!AddChunkedMarker(&out, kApp2, ByteSpan(kIccProfileTag), + io->metadata.color_encoding.icc)) { + JXL_ABORT("Brunsli: failed to add ICC profile\n"); + } + } else { + PaddedBytes hdr_payload; + std::string colorspace = Description(ib.c_current()); + std::string payload = options.hdr_orig_colorspace + ">" + colorspace; + hdr_payload.append(payload); + if (!AddChunkedMarker(&out, kApp9, ByteSpan(kBrunsliXHdr), hdr_payload)) { + JXL_ABORT("Brunsli: failed to add HDR extension\n"); + } + } + + ::brunsli::JPEGQuantTable quant_luma; + FillQuantMatrix(false, options.quant_scale, &quant_luma.values); + quant_luma.precision = 0; + quant_luma.index = 0; + quant_luma.is_last = false; + out.quant.emplace_back(std::move(quant_luma)); + ::brunsli::JPEGQuantTable quant_chroma; + FillQuantMatrix(true, options.quant_scale, &quant_chroma.values); + quant_chroma.precision = 0; + quant_chroma.index = 1; + quant_chroma.is_last = true; + out.quant.emplace_back(std::move(quant_chroma)); + out.marker_order.push_back(kDqt); + + { + Image3F pixels(std::move(planes[0]), std::move(planes[1]), + std::move(planes[2])); + constexpr int kQuantMap[] = {0, 1, 1}; + ConvertPixels(pixels, &out, kQuantMap, pool); + } + + out.marker_order.push_back(kSof0); + + ::brunsli::JPEGHuffmanCode dc_huff; + // kJpegDCAlphabetSize + 1 = 13 = 3 + 10; 3/8 + 10/16 = 1 + dc_huff.counts[3] = 3; + dc_huff.counts[4] = 10; + for (size_t i = 0; i < ::brunsli::kJpegDCAlphabetSize; ++i) { + dc_huff.values[i] = i; + } + dc_huff.values[::brunsli::kJpegDCAlphabetSize] = + ::brunsli::kJpegHuffmanAlphabetSize; + dc_huff.slot_id = 0 + kHuffmanCodeDcSlotOffset; + dc_huff.is_last = false; + out.huffman_code.emplace_back(std::move(dc_huff)); + ::brunsli::JPEGHuffmanCode ac_huff; + // kJpegHuffmanAlphabetSize + 1 = 257 = 255 + 2; 255 / 256 + 2 / 512 = 1 + ac_huff.counts[8] = 255; + ac_huff.counts[9] = 2; + for (size_t i = 0; i < ::brunsli::kJpegHuffmanAlphabetSize; ++i) { + ac_huff.values[i] = i; + } + ac_huff.values[::brunsli::kJpegHuffmanAlphabetSize] = + ::brunsli::kJpegHuffmanAlphabetSize; + ac_huff.slot_id = 0 + kHuffmanCodeAcSlotOffset; + ac_huff.is_last = true; + out.huffman_code.emplace_back(std::move(ac_huff)); + out.marker_order.push_back(kDht); + + ::brunsli::JPEGScanInfo scan_info; + scan_info.Ss = 0; + scan_info.Se = 63; + scan_info.Ah = 0; + scan_info.Al = 0; + scan_info.components.resize(num_c); + for (size_t i = 0; i < num_c; ++i) { + scan_info.components[i].comp_idx = i; + scan_info.components[i].dc_tbl_idx = 0; + scan_info.components[i].ac_tbl_idx = 0; + } + out.scan_info.emplace_back(std::move(scan_info)); + out.marker_order.push_back(kSos); + + out.marker_order.push_back(kEoi); + + size_t output_size = ::brunsli::GetMaximumBrunsliEncodedSize(out); + std::vector output(output_size); + // TODO(eustas): introduce streaming API? + if (!::brunsli::BrunsliEncodeJpeg(out, output.data(), &output_size)) { + return false; + } + compressed->append(Span(output.data(), output_size)); + + return true; +} + +class BrunsliFrameEncoderInternal { + public: + BrunsliFrameEncoderInternal(const FrameDimensions& frame_dim, + ThreadPool* pool) + : pool_(pool), dc_states_(nullptr), ac_states_(nullptr) { + jpg_.width = frame_dim.xsize; + jpg_.height = frame_dim.ysize; + jpg_.version = 0; // regular Brunsli + jpg_.restart_interval = 0; // don't care + jpg_.has_zero_padding_bit = false; + } + + bool ReadSourceImage(const ImageBundle* src, + const std::vector& quant_table, + YCbCrChromaSubsampling subsampling) { + if (!src->HasColor()) return false; + + // TODO(eustas): consider grayscale / CMYK. + const size_t num_c = 3; + + constexpr size_t kInvPlaneOrder[] = {1, 0, 2}; + for (size_t c = 0; c < num_c; ++c) { + ::brunsli::JPEGQuantTable quant; + size_t src_q = kInvPlaneOrder[c] * 64; + for (size_t i = 0; i < 64; ++i) { + // TODO(eustas): add a sanity check? + quant.values[i] = quant_table[i + src_q]; + } + quant.precision = 0; + quant.index = c; + quant.is_last = (c + 1 == num_c); + jpg_.quant.emplace_back(std::move(quant)); + } + + constexpr size_t N = kBlockDim; + constexpr int kQuantMap[] = {0, 1, 2}; + + constexpr size_t kPlaneOrder[] = {1, 0, 2}; + + for (size_t c = 0; c < num_c; ++c) { + ::brunsli::JPEGComponent component; + component.id = static_cast(c) + 1; // YCbCr + component.h_samp_factor = 1; + component.v_samp_factor = 1; + component.quant_idx = kQuantMap[c]; + jpg_.components.emplace_back(std::move(component)); + } + if (subsampling == YCbCrChromaSubsampling::k420) { + jpg_.components[0].h_samp_factor = 2; + jpg_.components[0].v_samp_factor = 2; + } else if (subsampling == YCbCrChromaSubsampling::k422) { + jpg_.components[0].h_samp_factor = 2; + } else if (subsampling == YCbCrChromaSubsampling::k411) { + jpg_.components[0].h_samp_factor = 4; + } + jpg_.max_h_samp_factor = jpg_.components[0].h_samp_factor; + jpg_.max_v_samp_factor = jpg_.components[0].v_samp_factor; + jpg_.MCU_rows = DivCeil(jpg_.height, jpg_.max_v_samp_factor * N); + jpg_.MCU_cols = DivCeil(jpg_.width, jpg_.max_h_samp_factor * N); + + for (size_t c = 0; c < num_c; ++c) { + const ImageF& plane = src->color().Plane(kPlaneOrder[c]); + ::brunsli::JPEGComponent& component = jpg_.components[c]; + component.quant_idx = kQuantMap[c]; + const size_t xsize_blocks = component.h_samp_factor * jpg_.MCU_cols; + component.width_in_blocks = xsize_blocks; + const size_t ysize_blocks = component.v_samp_factor * jpg_.MCU_rows; + component.height_in_blocks = ysize_blocks; + const size_t num_blocks = xsize_blocks * ysize_blocks; + component.num_blocks = num_blocks; + std::vector<::brunsli::coeff_t> coeffs(num_blocks * kDCTBlockSize); + const size_t from_stride = plane.PixelsPerRow(); + constexpr size_t group_dim = kGroupDimInBlocks; + const size_t xsize_groups = DivCeil(xsize_blocks, group_dim); + const size_t ysize_groups = DivCeil(ysize_blocks, group_dim); + const auto copy_coeffs = [&](int idx, int /*thread*/) { + const size_t gx = idx % xsize_groups; + const size_t gy = idx / xsize_groups; + const Rect group_rect_blocks(gx * group_dim, gy * group_dim, group_dim, + group_dim, xsize_blocks, ysize_blocks); + const size_t bx0 = group_rect_blocks.x0(); + const size_t bx1 = bx0 + group_rect_blocks.xsize(); + const size_t by0 = group_rect_blocks.y0(); + const size_t by1 = by0 + group_rect_blocks.ysize(); + const size_t bx_max = std::min(DivCeil(plane.xsize(), N), bx1); + const size_t by_max = std::min(DivCeil(plane.ysize(), N), by1); + for (size_t by = by0; by < by_max; ++by) { + const float* JXL_RESTRICT from_row = plane.ConstRow(by * N); + ::brunsli::coeff_t* JXL_RESTRICT to_row = + &coeffs[by * xsize_blocks * kDCTBlockSize]; + for (size_t bx = bx0; bx < bx_max; ++bx) { + const float* JXL_RESTRICT from_block = &from_row[bx * N]; + ::brunsli::coeff_t* JXL_RESTRICT to_block = + &to_row[bx * kDCTBlockSize]; + // TODO(eustas): SIMDify + for (size_t n = 0; n < N * N; ++n) { + to_block[n] = from_block[(n % N) + (n / N) * from_stride]; + } + } + memset(to_row + bx_max * kDCTBlockSize, 0, + (bx1 - bx_max) * kDCTBlockSize * sizeof(::brunsli::coeff_t)); + } + // Complete the MCU, if necessary. + for (size_t by = by_max; by < by1; ++by) { + ::brunsli::coeff_t* JXL_RESTRICT to_row = + &coeffs[by * xsize_blocks * kDCTBlockSize]; + memset(to_row, 0, + xsize_blocks * kDCTBlockSize * sizeof(::brunsli::coeff_t)); + } + }; + RunOnPool(pool_, 0, static_cast(xsize_groups * ysize_groups), + ThreadPool::SkipInit(), copy_coeffs, "Brunsli:CopyCoeffs"); + + component.coeffs = std::move(coeffs); + } + + return true; + } + + bool DoEncode() { + using brunsli::internal::enc::ComponentMeta; + using brunsli::internal::enc::SelectContextBits; + using brunsli::internal::enc::State; + + size_t num_components = jpg_.components.size(); + std::vector approx_total_nonzeros(num_components); + + size_t width_in_blocks = jpg_.MCU_cols * jpg_.max_h_samp_factor; + size_t height_in_blocks = jpg_.MCU_rows * jpg_.max_v_samp_factor; + + size_t w_ac = (width_in_blocks + kGroupDimInBlocks - 1) / kGroupDimInBlocks; + size_t h_ac = + (height_in_blocks + kGroupDimInBlocks - 1) / kGroupDimInBlocks; + + size_t w_dc = + (width_in_blocks + kDcGroupDimInBlocks - 1) / kDcGroupDimInBlocks; + size_t h_dc = + (height_in_blocks + kDcGroupDimInBlocks - 1) / kDcGroupDimInBlocks; + + std::vector> dc_prediction_errors( + num_components); + std::vector> block_state(num_components); + for (size_t i = 0; i < num_components; ++i) { + const brunsli::JPEGComponent& c = jpg_.components[i]; + dc_prediction_errors[i].resize(c.width_in_blocks * c.height_in_blocks); + block_state[i].resize(c.width_in_blocks * c.height_in_blocks); + } + + size_t dc_state_size = w_dc * h_dc; + size_t ac_state_size = w_ac * h_ac; + states_.resize(1 + dc_state_size + ac_state_size); + State& state = states_[0]; + dc_states_ = states_.data() + 1; + ac_states_ = dc_states_ + dc_state_size; + + if (!CalculateMeta(jpg_, &state)) return false; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = state.meta[c]; + m.dc_prediction_errors = dc_prediction_errors[c].data(); + m.block_state = block_state[c].data(); + } + + for (size_t y = 0; y < h_dc; ++y) { + for (size_t x = 0; x < w_dc; ++x) { + State& s = dc_states_[x + y * w_dc]; + std::vector& meta = s.meta; + if (!CalculateMeta(jpg_, &s)) return false; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = meta[c]; + size_t h_group_dim = + m.h_samp * kDcGroupDimInBlocks / jpg_.max_h_samp_factor; + size_t first_x = x * h_group_dim; + size_t last_x = + std::min(first_x + h_group_dim, m.width_in_blocks); + size_t v_group_dim = + m.v_samp * kDcGroupDimInBlocks / jpg_.max_v_samp_factor; + size_t first_y = y * v_group_dim; + size_t last_y = + std::min(first_y + v_group_dim, m.height_in_blocks); + m.ac_coeffs += + first_x * brunsli::kDCTBlockSize + first_y * m.ac_stride; + m.width_in_blocks = last_x - first_x; + m.height_in_blocks = last_y - first_y; + m.dc_prediction_errors = + dc_prediction_errors[c].data() + first_x + first_y * m.dc_stride; + m.block_state = + block_state[c].data() + first_x + first_y * m.b_stride; + } + } + } + + for (size_t y = 0; y < h_ac; ++y) { + for (size_t x = 0; x < w_ac; ++x) { + State& s = ac_states_[x + y * w_ac]; + std::vector& meta = s.meta; + if (!CalculateMeta(jpg_, &s)) return false; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = meta[c]; + size_t h_group_dim = + m.h_samp * kGroupDimInBlocks / jpg_.max_h_samp_factor; + size_t first_x = x * h_group_dim; + size_t last_x = + std::min(first_x + h_group_dim, m.width_in_blocks); + size_t v_group_dim = + m.v_samp * kGroupDimInBlocks / jpg_.max_v_samp_factor; + size_t first_y = y * v_group_dim; + size_t last_y = + std::min(first_y + v_group_dim, m.height_in_blocks); + m.ac_coeffs += + first_x * brunsli::kDCTBlockSize + first_y * m.ac_stride; + m.width_in_blocks = last_x - first_x; + m.height_in_blocks = last_y - first_y; + m.dc_prediction_errors = + dc_prediction_errors[c].data() + first_x + first_y * m.dc_stride; + m.block_state = + block_state[c].data() + first_x + first_y * m.b_stride; + } + } + } + + State* ac_states = ac_states_; + const auto sample_nonzeros = [num_components, ac_states](int idx, + int /* thread */) { + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = ac_states[idx].meta[c]; + m.approx_total_nonzeros = SampleNumNonZeros(&m); + } + }; + RunOnPool(pool_, 0, static_cast(ac_state_size), ThreadPool::SkipInit(), + sample_nonzeros, "Brunsli:SampleNonzeros"); + + // Groups workflow: reduce approx_total_nonzeros. + for (size_t y = 0; y < h_ac; ++y) { + for (size_t x = 0; x < w_ac; ++x) { + for (size_t c = 0; c < num_components; ++c) { + approx_total_nonzeros[c] += + ac_states[x + y * w_ac].meta[c].approx_total_nonzeros; + } + } + } + + int32_t num_contexts = num_components; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = state.meta[c]; + m.context_bits = SelectContextBits(approx_total_nonzeros[c] + 1); + m.context_offset = num_contexts; + num_contexts += brunsli::kNumNonzeroContextSkip[m.context_bits]; + } + state.num_contexts = num_contexts; + + // Groups workflow: distribute context_bits. + for (size_t y = 0; y < h_ac; ++y) { + for (size_t x = 0; x < w_ac; ++x) { + State& s = ac_states[x + y * w_ac]; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = state.meta[c]; + s.meta[c].context_bits = m.context_bits; + s.meta[c].context_offset = m.context_offset; + } + s.num_contexts = state.num_contexts; + } + } + + for (size_t y = 0; y < h_dc; ++y) { + for (size_t x = 0; x < w_dc; ++x) { + State& s = dc_states_[x + y * w_dc]; + for (size_t c = 0; c < num_components; ++c) { + ComponentMeta& m = state.meta[c]; + s.meta[c].context_bits = m.context_bits; + s.meta[c].context_offset = m.context_offset; + } + s.num_contexts = state.num_contexts; + } + } + + std::atomic num_errors{0}; + State* dc_states = dc_states_; + const auto encode_dc = [&num_errors, dc_states](int idx, int /* thread */) { + if (!PredictDCCoeffs(&dc_states[idx])) { + num_errors.fetch_add(1, std::memory_order_relaxed); + return; + } + EncodeDC(&dc_states[idx]); + }; + RunOnPool(pool_, 0, static_cast(dc_state_size), ThreadPool::SkipInit(), + encode_dc, "Brunsli:EncodeDc"); + if (num_errors.load(std::memory_order_relaxed) != 0) return false; + + const auto encode_ac = [ac_states](int idx, int /* thread */) { + EncodeAC(&ac_states[idx]); + }; + RunOnPool(pool_, 0, static_cast(ac_state_size), ThreadPool::SkipInit(), + encode_ac, "Brunsli:EncodeAc"); + + // Groups workflow: merge histograms. + // TODO: SIMDify. + state.entropy_source.Resize(num_contexts); + for (size_t y = 0; y < h_dc; ++y) { + for (size_t x = 0; x < w_dc; ++x) { + state.entropy_source.Merge(dc_states[x + y * w_dc].entropy_source); + } + } + for (size_t y = 0; y < h_ac; ++y) { + for (size_t x = 0; x < w_ac; ++x) { + state.entropy_source.Merge(ac_states[x + y * w_ac].entropy_source); + } + } + + entropy_codes_ = PrepareEntropyCodes(&state); + + return true; + } + + bool SerializeHeader(BitWriter* out) { + // TODO: pull entropy codes serialization "side effect". + auto& state = states_[0]; + state.entropy_codes = entropy_codes_.get(); + size_t part_size = 20480; + + // TODO: take into account histograms. + uint32_t skip_flags = ~((1u << brunsli::kBrunsliQuantDataTag) | + (1u << brunsli::kBrunsliHistogramDataTag)); + PaddedBytes part(part_size); + if (!BrunsliSerialize(&state, jpg_, skip_flags, part.data(), &part_size)) { + return false; + } + + part.resize(part_size); + out->AppendByteAligned(BitWriter(std::move(part))); + + return true; + } + + bool SerializeDcGroup(size_t idx, BitWriter* out, AuxOut* aux_out) { + auto& s = dc_states_[idx]; + // TODO: reduce for subsampled + size_t part_size = + kDcGroupDimInBlocks * kDcGroupDimInBlocks * jpg_.components.size(); + part_size += part_size / 8; + PaddedBytes part(part_size); + s.entropy_codes = entropy_codes_.get(); + uint32_t skip_flags = ~(1u << brunsli::kBrunsliDCDataTag); + if (!BrunsliSerialize(&s, jpg_, skip_flags, part.data(), &part_size)) { + return false; + } + + part.resize(part_size); + out->AppendByteAligned(BitWriter(std::move(part))); + + return true; + } + + bool SerializeAcGroup(size_t idx, BitWriter* out, AuxOut* aux_out) { + auto& s = ac_states_[idx]; + // TODO: reduce for subsampled. + size_t part_size = 32 * 32 * 63 * jpg_.components.size(); + PaddedBytes part(part_size); + s.entropy_codes = entropy_codes_.get(); + uint32_t skip_flags = ~(1u << brunsli::kBrunsliACDataTag); + if (!BrunsliSerialize(&s, jpg_, skip_flags, part.data(), &part_size)) { + return false; + } + + part.resize(part_size); + out->AppendByteAligned(BitWriter(std::move(part))); + + return true; + } + + private: + ThreadPool* pool_; + + brunsli::JPEGData jpg_; + std::vector states_; + brunsli::internal::enc::State* dc_states_; + brunsli::internal::enc::State* ac_states_; + std::unique_ptr entropy_codes_; +}; + +BrunsliFrameEncoder::BrunsliFrameEncoder(const FrameDimensions& frame_dim, + ThreadPool* pool) + : impl_(make_unique(frame_dim, pool)) {} +BrunsliFrameEncoder::~BrunsliFrameEncoder() = default; +bool BrunsliFrameEncoder::ReadSourceImage(const ImageBundle* src, + const std::vector& quant_table, + YCbCrChromaSubsampling subsampling) { + return impl_->ReadSourceImage(src, quant_table, subsampling); +} +bool BrunsliFrameEncoder::DoEncode() { return impl_->DoEncode(); } +bool BrunsliFrameEncoder::SerializeHeader(BitWriter* out) { + return impl_->SerializeHeader(out); +} +bool BrunsliFrameEncoder::SerializeDcGroup(size_t index, BitWriter* out, + AuxOut* aux_out) { + return impl_->SerializeDcGroup(index, out, aux_out); +} +bool BrunsliFrameEncoder::SerializeAcGroup(size_t index, BitWriter* out, + AuxOut* aux_out) { + return impl_->SerializeAcGroup(index, out, aux_out); +} + +class BrunsliFrameDecoderInternal { + public: + explicit BrunsliFrameDecoderInternal(ThreadPool* pool) + : pool_(pool), dc_states_(nullptr), ac_states_(nullptr) {} + + bool ReadHeader(const FrameDimensions* frame_dim, BitReader* src, + YCbCrChromaSubsampling subsampling) { + using brunsli::BrunsliStatus; + using brunsli::JPEGQuantTable; + using brunsli::internal::dec::ComponentMeta; + using brunsli::internal::dec::PrepareMeta; + using brunsli::internal::dec::Stage; + using brunsli::internal::dec::State; + using brunsli::internal::dec::UpdateSubsamplingDerivatives; + + // TODO(eustas): consider grayscale / CMYK. + const size_t num_c = 3; + jpg_.version = 0; + jpg_.width = frame_dim->xsize; + jpg_.height = frame_dim->ysize; + jpg_.components.resize(num_c); + for (size_t c = 0; c < num_c; ++c) { + jpg_.components[c].h_samp_factor = 1; + jpg_.components[c].v_samp_factor = 1; + } + if (subsampling == YCbCrChromaSubsampling::k420) { + jpg_.components[0].h_samp_factor = 2; + jpg_.components[0].v_samp_factor = 2; + } else if (subsampling == YCbCrChromaSubsampling::k422) { + jpg_.components[0].h_samp_factor = 2; + } else if (subsampling == YCbCrChromaSubsampling::k411) { + jpg_.components[0].h_samp_factor = 4; + } + if (!UpdateSubsamplingDerivatives(&jpg_)) return false; + + // == num_c? + const int num_q = 3; + jpg_.quant.resize(num_q); + for (size_t i = 0; i < num_q; ++i) { + JPEGQuantTable& q = jpg_.quant[i]; + q.index = i; + q.is_last = (i == num_q - 1); + q.precision = 0; + } + + const size_t num_dc_groups = + frame_dim->xsize_dc_groups * frame_dim->ysize_dc_groups; + const size_t num_ac_groups = + frame_dim->xsize_groups * frame_dim->ysize_groups; + states_.resize(1 + num_dc_groups + num_ac_groups); + dc_states_ = states_.data() + 1; + ac_states_ = dc_states_ + num_dc_groups; + + size_t pos = src->TotalBitsConsumed(); + if ((pos & 0x7u) != 0) return false; + pos >>= 3u; + + const uint8_t* data = src->FirstByte() + pos; + const uint8_t* data_end = src->FirstByte() + src->TotalBytes(); + const uint8_t* chunk_end = data; + if (!SkipSection(&chunk_end, data_end - chunk_end)) return false; + if (!SkipSection(&chunk_end, data_end - chunk_end)) return false; + const size_t part_length = chunk_end - data; + + State& state = states_[0]; + state.stage = Stage::SECTION; + state.tags_met = ~((1u << brunsli::kBrunsliQuantDataTag) | + (1u << brunsli::kBrunsliHistogramDataTag)); + state.data = data; + state.len = part_length; + PrepareMeta(&jpg_, &state); + + BrunsliStatus status = ProcessJpeg(&state, &jpg_); + if (status != BrunsliStatus::BRUNSLI_OK) return false; + WarmupMeta(&jpg_, &state); + + src->SkipBits(part_length << 3u); + + for (size_t idx = 0; idx < num_dc_groups; ++idx) { + size_t y = idx / frame_dim->xsize_dc_groups; + size_t x = idx % frame_dim->xsize_dc_groups; + State& dc_state = dc_states_[idx]; + dc_state.stage = Stage::SECTION; + dc_state.tags_met = ~(1u << brunsli::kBrunsliDCDataTag); + + dc_state.context_map = state.context_map; + dc_state.entropy_codes = state.entropy_codes; + + std::vector& meta = dc_state.meta; + + PrepareMeta(&jpg_, &dc_state); + dc_state.is_storage_allocated = true; + WarmupMeta(&jpg_, &dc_state); + for (size_t c = 0; c < num_c; ++c) { + ComponentMeta& m = meta[c]; + size_t h_group_dim = + m.h_samp * kDcGroupDimInBlocks / jpg_.max_h_samp_factor; + size_t first_x = x * h_group_dim; + size_t last_x = + std::min(first_x + h_group_dim, m.width_in_blocks); + size_t v_group_dim = + m.v_samp * kDcGroupDimInBlocks / jpg_.max_v_samp_factor; + size_t first_y = y * v_group_dim; + size_t last_y = + std::min(first_y + v_group_dim, m.height_in_blocks); + m.ac_coeffs += first_x * brunsli::kDCTBlockSize + first_y * m.ac_stride; + m.block_state = + state.block_state_[c].data() + first_x + first_y * m.b_stride; + m.width_in_blocks = last_x - first_x; + m.height_in_blocks = last_y - first_y; + } + } + + for (size_t idx = 0; idx < num_ac_groups; ++idx) { + size_t y = idx / frame_dim->xsize_groups; + size_t x = idx % frame_dim->xsize_groups; + State& ac_state = ac_states_[idx]; + ac_state.stage = Stage::SECTION; + ac_state.tags_met = ~(1u << brunsli::kBrunsliACDataTag); + + ac_state.context_map = state.context_map; + ac_state.entropy_codes = state.entropy_codes; + + std::vector& meta = ac_state.meta; + + PrepareMeta(&jpg_, &ac_state); + ac_state.is_storage_allocated = true; + WarmupMeta(&jpg_, &ac_state); + for (size_t c = 0; c < num_c; ++c) { + ComponentMeta& m = meta[c]; + size_t h_group_dim = + m.h_samp * kGroupDimInBlocks / jpg_.max_h_samp_factor; + size_t first_x = x * h_group_dim; + size_t last_x = + std::min(first_x + h_group_dim, m.width_in_blocks); + size_t v_group_dim = + m.v_samp * kGroupDimInBlocks / jpg_.max_v_samp_factor; + size_t first_y = y * v_group_dim; + size_t last_y = + std::min(first_y + v_group_dim, m.height_in_blocks); + m.context_bits = state.meta[c].context_bits; + m.context_offset = state.meta[c].context_offset; + m.ac_coeffs += first_x * brunsli::kDCTBlockSize + first_y * m.ac_stride; + m.block_state = + state.block_state_[c].data() + first_x + first_y * m.b_stride; + m.width_in_blocks = last_x - first_x; + m.height_in_blocks = last_y - first_y; + } + } + + return true; + } + + bool DecodeDcGroup(int idx, jxl::BitReader* src) { + using brunsli::BrunsliStatus; + using brunsli::internal::dec::ProcessJpeg; + using brunsli::internal::dec::Stage; + using brunsli::internal::dec::State; + + size_t pos = src->TotalBitsConsumed(); + if ((pos & 0x7u) != 0) return false; + pos >>= 3u; + + const uint8_t* data = src->FirstByte() + pos; + const uint8_t* data_end = src->FirstByte() + src->TotalBytes(); + const uint8_t* chunk_end = data; + if (!SkipSection(&chunk_end, data_end - chunk_end)) return false; + const size_t part_length = chunk_end - data; + + State& dc_state = dc_states_[idx]; + dc_state.data = data; + dc_state.len = part_length; + + BrunsliStatus status = ProcessJpeg(&dc_state, &jpg_); + if (status != brunsli::BRUNSLI_OK) return false; + + src->SkipBits(part_length << 3u); + + return true; + } + + bool DecodeAcGroup(int idx, jxl::BitReader* src, Image3F* img, + const Rect& rect) { + using brunsli::BrunsliStatus; + using brunsli::internal::dec::ProcessJpeg; + using brunsli::internal::dec::Stage; + using brunsli::internal::dec::State; + + size_t pos = src->TotalBitsConsumed(); + if ((pos & 0x7u) != 0) return false; + pos >>= 3u; + + const uint8_t* data = src->FirstByte() + pos; + const uint8_t* data_end = src->FirstByte() + src->TotalBytes(); + const uint8_t* chunk_end = data; + if (!SkipSection(&chunk_end, data_end - chunk_end)) return false; + const size_t part_length = chunk_end - data; + + State& ac_state = ac_states_[idx]; + ac_state.data = data; + ac_state.len = part_length; + + BrunsliStatus status = ProcessJpeg(&ac_state, &jpg_); + if (status != brunsli::BRUNSLI_OK) return false; + + src->SkipBits(part_length << 3u); + + return true; + } + + bool FinalizeDecoding(Image3F* opsin) { + BrunsliDecoderOptions options{}; + BrunsliExtensions extensions{}; + bool is_gray; + bool ok = + JpegDataToPixels(jpg_, options, extensions, opsin, &is_gray, pool_); + if (!ok) return false; + return true; + } + + private: + ThreadPool* pool_; + + brunsli::JPEGData jpg_; + std::vector states_; + brunsli::internal::dec::State* dc_states_; + brunsli::internal::dec::State* ac_states_; +}; + +BrunsliFrameDecoder::BrunsliFrameDecoder(ThreadPool* pool) + : impl_(make_unique(pool)) {} +BrunsliFrameDecoder::~BrunsliFrameDecoder() = default; +bool BrunsliFrameDecoder::ReadHeader(const FrameDimensions* frame_dim, + BitReader* src, + YCbCrChromaSubsampling subsampling) { + return impl_->ReadHeader(frame_dim, src, subsampling); +} +bool BrunsliFrameDecoder::DecodeDcGroup(int idx, jxl::BitReader* src) { + return impl_->DecodeDcGroup(idx, src); +} +bool BrunsliFrameDecoder::DecodeAcGroup(int idx, jxl::BitReader* src, + Image3F* img, const Rect& rect) { + return impl_->DecodeAcGroup(idx, src, img, rect); +} +bool BrunsliFrameDecoder::FinalizeDecoding(Image3F* opsin) { + return impl_->FinalizeDecoding(opsin); +} +} // namespace jxl diff --git a/jxl/brunsli.h b/jxl/brunsli.h new file mode 100644 index 000000000..5b88a34ba --- /dev/null +++ b/jxl/brunsli.h @@ -0,0 +1,100 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_BRUNSLI_H_ +#define JXL_BRUNSLI_H_ + +#include +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/span.h" +#include "jxl/base/status.h" +#include "jxl/codec_in_out.h" +#include "jxl/dec_params.h" + +// Utilities for rasterization of intermediate Brunsli representation. + +namespace jxl { + +enum class BrunsliFileSignature { + kBrunsli, + kNotEnoughData, + kInvalid, +}; + +BrunsliFileSignature IsBrunsliFile(jxl::Span compressed); + +struct BrunsliEncoderOptions { + float quant_scale = 1.0f; + std::string hdr_orig_colorspace; +}; + +struct BrunsliDecoderMeta { + std::string hdr_orig_colorspace; +}; + +Status BrunsliToPixels(jxl::Span compressed, + jxl::CodecInOut* JXL_RESTRICT io, + const BrunsliDecoderOptions& options, + BrunsliDecoderMeta* metadata, jxl::ThreadPool* pool); + +Status PixelsToBrunsli(const jxl::CodecInOut* JXL_RESTRICT io, + jxl::PaddedBytes* compressed, + const BrunsliEncoderOptions& options, + jxl::ThreadPool* pool); + +// Actual encoder has a lot of brunsli-specific state. Let's not expose it. +class BrunsliFrameEncoderInternal; + +class BrunsliFrameEncoder { + public: + BrunsliFrameEncoder(const FrameDimensions& frame_dim, ThreadPool* pool); + ~BrunsliFrameEncoder(); + bool ReadSourceImage(const ImageBundle* src, + const std::vector& quant_table, + YCbCrChromaSubsampling subsampling); + bool DoEncode(); + bool SerializeHeader(BitWriter* out); + bool SerializeDcGroup(size_t index, BitWriter* out, AuxOut* aux_out); + bool SerializeAcGroup(size_t index, BitWriter* out, AuxOut* aux_out); + + private: + std::unique_ptr impl_; +}; + +// Actual decoder has a lot of brunsli-specific state. Let's not expose it. +class BrunsliFrameDecoderInternal; + +class BrunsliFrameDecoder { + public: + explicit BrunsliFrameDecoder(ThreadPool* pool); + ~BrunsliFrameDecoder(); + + bool ReadHeader(const FrameDimensions* frame_dim, BitReader* src, + YCbCrChromaSubsampling subsampling); + bool DecodeDcGroup(int idx, BitReader* src); + bool DecodeAcGroup(int idx, BitReader* src, Image3F* img, const Rect& rect); + bool FinalizeDecoding(Image3F* opsin); + + private: + std::unique_ptr impl_; +}; + +} // namespace jxl + +#endif // JXL_BRUNSLI_H_ diff --git a/jxl/brunsli_test.cc b/jxl/brunsli_test.cc new file mode 100644 index 000000000..3b47855a2 --- /dev/null +++ b/jxl/brunsli_test.cc @@ -0,0 +1,164 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/brunsli.h" + +#include + +#include +#include + +#include "gtest/gtest.h" +#include "jxl/codec_in_out.h" +#include "jxl/color_encoding.h" +#include "jxl/color_management.h" +#include "jxl/enc_butteraugli_comparator.h" +#include "jxl/extras/codec.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/testdata_path.h" + +namespace jxl { +namespace { + +// Returns compressed size [bytes]. +size_t Roundtrip(CodecInOut* io, const BrunsliEncoderOptions& enc_options, + const BrunsliDecoderOptions& dec_options, ThreadPool* pool, + CodecInOut* JXL_RESTRICT io2, float max_butteraugli_score) { + PaddedBytes compressed; + + EXPECT_TRUE(PixelsToBrunsli(io, &compressed, enc_options, pool)); + + BrunsliDecoderMeta meta; + EXPECT_TRUE(BrunsliToPixels(jxl::Span(compressed), io2, + dec_options, &meta, pool)); + + EXPECT_LE(ButteraugliDistance(*io, *io2, /* hf_assymetry */ 1.0f, + /*distmap=*/nullptr, pool), + max_butteraugli_score); + + return compressed.size(); +} + +TEST(BrunsliTest, RoundtripSinglePixel) { + ThreadPool* pool = nullptr; + Image3F image(1, 1); + BrunsliEncoderOptions enc_options; + BrunsliDecoderOptions dec_options; + + image.PlaneRow(0, 0)[0] = 0.0f; + image.PlaneRow(1, 0)[0] = 0.0f; + image.PlaneRow(2, 0)[0] = 0.0f; + + CodecInOut io; + io.metadata.bits_per_sample = 8; + io.metadata.color_encoding = ColorManagement::SRGB(); + io.SetFromImage(std::move(image), io.metadata.color_encoding); + CodecInOut io2; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 0.001f); +} + +TEST(BrunsliTest, RoundtripTiny) { + ThreadPool* pool = nullptr; + const std::string pathname = + GetTestDataPath("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromFile(pathname, &io, pool)); + io.ShrinkTo(16, 16); + + BrunsliEncoderOptions enc_options; + enc_options.quant_scale = 0.0f; + BrunsliDecoderOptions dec_options; + + CodecInOut io2; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 0.3f); +} + +TEST(BrunsliTest, RoundtripSmallQ0) { + ThreadPool* pool = nullptr; + const std::string pathname = + GetTestDataPath("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromFile(pathname, &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + BrunsliEncoderOptions enc_options; + enc_options.quant_scale = 0.0f; + BrunsliDecoderOptions dec_options; + + CodecInOut io2; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 0.5f); +} + +TEST(BrunsliTest, RoundtripDecOptions) { + ThreadPool* pool = nullptr; + const std::string pathname = + GetTestDataPath("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromFile(pathname, &io, pool)); + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + + BrunsliEncoderOptions enc_options; + BrunsliDecoderOptions dec_options; + CodecInOut io2; + + // Also try different decoding options. + enc_options.quant_scale = 2.0f; + + dec_options.fix_dc_staircase = false; + dec_options.gaborish = false; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 5.4f); + + dec_options.fix_dc_staircase = true; + dec_options.gaborish = false; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 5.2f); + + dec_options.fix_dc_staircase = false; + dec_options.gaborish = true; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 4.777f); +} + +TEST(BrunsliTest, RoundtripUnalignedQ0_5) { + ThreadPool* pool = nullptr; + const std::string pathname = + GetTestDataPath("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ASSERT_TRUE(SetFromFile(pathname, &io, pool)); + io.ShrinkTo(io.xsize() / 12, io.ysize() / 7); + + BrunsliEncoderOptions enc_options; + enc_options.quant_scale = 0.5f; + BrunsliDecoderOptions dec_options; + + CodecInOut io2; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 1.3579f); +} + +TEST(BrunsliTest, RoundtripLarge) { + ThreadPool* pool = nullptr; + const std::string pathname = + GetTestDataPath("imagecompression.info/flower_foveon.png"); + CodecInOut io; + ASSERT_TRUE(SetFromFile(pathname, &io, pool)); + + BrunsliEncoderOptions enc_options; + enc_options.quant_scale = 2.0f; + BrunsliDecoderOptions dec_options; + + CodecInOut io2; + Roundtrip(&io, enc_options, dec_options, pool, &io2, 8.88888); +} + +} // namespace +} // namespace jxl diff --git a/jxl/butteraugli/butteraugli.cc b/jxl/butteraugli/butteraugli.cc new file mode 100644 index 000000000..2b90b79a7 --- /dev/null +++ b/jxl/butteraugli/butteraugli.cc @@ -0,0 +1,2029 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) +// +// The physical architecture of butteraugli is based on the following naming +// convention: +// * Opsin - dynamics of the photosensitive chemicals in the retina +// with their immediate electrical processing +// * Xyb - hybrid opponent/trichromatic color space +// x is roughly red-subtract-green. +// y is yellow. +// b is blue. +// Xyb values are computed from Opsin mixing, not directly from rgb. +// * Mask - for visual masking +// * Hf - color modeling for spatially high-frequency features +// * Lf - color modeling for spatially low-frequency features +// * Diffmap - to cluster and build an image of error between the images +// * Blur - to hold the smoothing code + +#include "jxl/butteraugli/butteraugli.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "jxl/base/profiler.h" +#include "jxl/base/status.h" +#include "jxl/image_ops.h" + +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif +#if PROFILER_ENABLED +#else +#define PROFILER_FUNC +#define PROFILER_ZONE(name) +#endif + +namespace jxl { +namespace butteraugli { + +static inline bool IsNan(const float x) { + uint32_t bits; + memcpy(&bits, &x, sizeof(bits)); + const uint32_t bitmask_exp = 0x7F800000; + return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); +} + +static inline bool IsNan(const double x) { + uint64_t bits; + memcpy(&bits, &x, sizeof(bits)); + return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || + (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); +} + +static inline void CheckImage(const ImageF& image, const char* name) { + PROFILER_FUNC; + for (size_t y = 0; y < image.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row = image.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + if (IsNan(row[x])) { + printf("NAN: Image %s @ %zu,%zu (of %zu,%zu)\n", name, x, y, + image.xsize(), image.ysize()); + exit(1); + } + } + } +} + +#if BUTTERAUGLI_ENABLE_CHECKS + +#define CHECK_NAN(x, str) \ + do { \ + if (IsNan(x)) { \ + printf("%d: %s\n", __LINE__, str); \ + abort(); \ + } \ + } while (0) + +#define CHECK_IMAGE(image, name) CheckImage(image, name) + +#else + +#define CHECK_NAN(x, str) +#define CHECK_IMAGE(image, name) + +#endif + +// Calculate a 2x2 subsampled image for purposes of recursive butteraugli at +// multiresolution. +static Image3F SubSample2x(const Image3F &in) { + size_t xs = (in.xsize() + 1) / 2; + size_t ys = (in.ysize() + 1) / 2; + Image3F retval(xs, ys); + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < ys; ++y) { + for (size_t x = 0; x < xs; ++x) { + retval.PlaneRow(c, y)[x] = 0; + } + } + } + for (size_t c = 0; c < 3; ++c) { + for (size_t y = 0; y < in.ysize(); ++y) { + for (size_t x = 0; x < in.xsize(); ++x) { + retval.PlaneRow(c, y / 2)[x / 2] += 0.25 * in.PlaneRow(c, y)[x]; + } + } + if ((in.xsize() & 1) != 0) { + for (size_t y = 0; y < retval.ysize(); ++y) { + size_t last_column = retval.xsize() - 1; + retval.PlaneRow(c, y)[last_column] *= 2.0; + } + } + if ((in.ysize() & 1) != 0) { + for (size_t x = 0; x < retval.xsize(); ++x) { + size_t last_row = retval.ysize() - 1; + retval.PlaneRow(c, last_row)[x] *= 2.0; + } + } + } + return retval; +} + +// Supersample src by 2x and add it to dest. +static void AddSupersampled2x(const ImageF &src, float w, ImageF &dest) { + for (size_t y = 0; y < dest.ysize(); ++y) { + for (size_t x = 0; x < dest.xsize(); ++x) { + // There will be less errors from the more averaged images. + // We take it into account to some extent using a scaler. + static const double kHeuristicMixingValue = 0.3; + dest.Row(y)[x] *= 1.0 - kHeuristicMixingValue * w; + dest.Row(y)[x] += w * src.Row(y / 2)[x / 2]; + } + } +} + + +// Purpose of kInternalGoodQualityThreshold: +// Normalize 'ok' image degradation to 1.0 across different versions of +// butteraugli. +static const double kInternalGoodQualityThreshold = 42.5; +static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; + +inline float MaskColor(const float color[3], const float mask[2]) { + return color[0] * mask[0] + color[1] * mask[1] + color[2] * mask[1]; +} + +std::vector ComputeKernel(float sigma) { + const float m = 2.25; // Accuracy increases when m is increased. + const float scaler = -1.0 / (2 * sigma * sigma); + const int diff = std::max(1, m * std::fabs(sigma)); + std::vector kernel(2 * diff + 1); + for (int i = -diff; i <= diff; ++i) { + kernel[i + diff] = std::exp(scaler * i * i); + } + return kernel; +} + +void ConvolveBorderColumn(const ImageF& in, const std::vector& kernel, + const float weight_no_border, + const float border_ratio, const size_t x, + float* BUTTERAUGLI_RESTRICT row_out) { + const size_t offset = kernel.size() / 2; + int minx = x < offset ? 0 : x - offset; + int maxx = std::min(in.xsize() - 1, x + offset); + float weight = 0.0f; + for (int j = minx; j <= maxx; ++j) { + weight += kernel[j - x + offset]; + } + // Interpolate linearly between the no-border scaling and border scaling. + weight = (1.0f - border_ratio) * weight + border_ratio * weight_no_border; + float scale = 1.0f / weight; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y); + float sum = 0.0f; + for (int j = minx; j <= maxx; ++j) { + sum += row_in[j] * kernel[j - x + offset]; + } + row_out[y] = sum * scale; + } +} + +// Computes a horizontal convolution and transposes the result. +void ConvolutionWithTranspose(const ImageF& in, + const std::vector& kernel, + const float border_ratio, + ImageF* BUTTERAUGLI_RESTRICT out) { + PROFILER_FUNC; + JXL_CHECK(out->xsize() == in.ysize()); + JXL_CHECK(out->ysize() == in.xsize()); + const size_t len = kernel.size(); + const size_t offset = len / 2; + float weight_no_border = 0.0f; + for (size_t j = 0; j < len; ++j) { + weight_no_border += kernel[j]; + } + const float scale_no_border = 1.0f / weight_no_border; + const size_t border1 = std::min(in.xsize(), offset); + const size_t border2 = in.xsize() > offset ? in.xsize() - offset : 0; + float* BUTTERAUGLI_RESTRICT scaled_kernel = + static_cast(malloc((len / 2 + 1) * sizeof(float))); + for (size_t i = 0; i <= len / 2; ++i) { + scaled_kernel[i] = kernel[i] * scale_no_border; + } + // left border + for (size_t x = 0; x < border1; ++x) { + ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, + out->Row(x)); + } + // middle + switch (len) { +#if 1 // speed-optimized version + case 5: { + const float sk0 = scaled_kernel[0]; + const float sk1 = scaled_kernel[1]; + const float sk2 = scaled_kernel[2]; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[4]) * sk0; + sum += (row_in[1] + row_in[3]) * sk1; + sum += (row_in[2]) * sk2; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + } break; + case 9: { + const float sk0 = scaled_kernel[0]; + const float sk1 = scaled_kernel[1]; + const float sk2 = scaled_kernel[2]; + const float sk3 = scaled_kernel[3]; + const float sk4 = scaled_kernel[4]; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[8]) * sk0; + sum += (row_in[1] + row_in[7]) * sk1; + sum += (row_in[2] + row_in[6]) * sk2; + sum += (row_in[3] + row_in[5]) * sk3; + sum += (row_in[4]) * sk4; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + } break; + case 17: + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[16]) * scaled_kernel[0]; + sum += (row_in[1] + row_in[15]) * scaled_kernel[1]; + sum += (row_in[2] + row_in[14]) * scaled_kernel[2]; + sum += (row_in[3] + row_in[13]) * scaled_kernel[3]; + sum += (row_in[4] + row_in[12]) * scaled_kernel[4]; + sum += (row_in[5] + row_in[11]) * scaled_kernel[5]; + sum += (row_in[6] + row_in[10]) * scaled_kernel[6]; + sum += (row_in[7] + row_in[9]) * scaled_kernel[7]; + sum += (row_in[8]) * scaled_kernel[8]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + break; + case 33: + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[32]) * scaled_kernel[0]; + sum += (row_in[1] + row_in[31]) * scaled_kernel[1]; + sum += (row_in[2] + row_in[30]) * scaled_kernel[2]; + sum += (row_in[3] + row_in[29]) * scaled_kernel[3]; + sum += (row_in[4] + row_in[28]) * scaled_kernel[4]; + sum += (row_in[5] + row_in[27]) * scaled_kernel[5]; + sum += (row_in[6] + row_in[26]) * scaled_kernel[6]; + sum += (row_in[7] + row_in[25]) * scaled_kernel[7]; + sum += (row_in[8] + row_in[24]) * scaled_kernel[8]; + sum += (row_in[9] + row_in[23]) * scaled_kernel[9]; + sum += (row_in[10] + row_in[22]) * scaled_kernel[10]; + sum += (row_in[11] + row_in[21]) * scaled_kernel[11]; + sum += (row_in[12] + row_in[20]) * scaled_kernel[12]; + sum += (row_in[13] + row_in[19]) * scaled_kernel[13]; + sum += (row_in[14] + row_in[18]) * scaled_kernel[14]; + sum += (row_in[15] + row_in[17]) * scaled_kernel[15]; + sum += (row_in[16]) * scaled_kernel[16]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + break; + case 11: + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[10]) * scaled_kernel[0]; + sum += (row_in[1] + row_in[9]) * scaled_kernel[1]; + sum += (row_in[2] + row_in[8]) * scaled_kernel[2]; + sum += (row_in[3] + row_in[7]) * scaled_kernel[3]; + sum += (row_in[4] + row_in[6]) * scaled_kernel[4]; + sum += (row_in[5]) * scaled_kernel[5]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + break; + case 41: + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[40]) * scaled_kernel[0]; + sum += (row_in[1] + row_in[39]) * scaled_kernel[1]; + sum += (row_in[2] + row_in[38]) * scaled_kernel[2]; + sum += (row_in[3] + row_in[37]) * scaled_kernel[3]; + sum += (row_in[4] + row_in[36]) * scaled_kernel[4]; + sum += (row_in[5] + row_in[35]) * scaled_kernel[5]; + sum += (row_in[6] + row_in[34]) * scaled_kernel[6]; + sum += (row_in[7] + row_in[33]) * scaled_kernel[7]; + sum += (row_in[8] + row_in[32]) * scaled_kernel[8]; + sum += (row_in[9] + row_in[31]) * scaled_kernel[9]; + sum += (row_in[10] + row_in[30]) * scaled_kernel[10]; + sum += (row_in[11] + row_in[29]) * scaled_kernel[11]; + sum += (row_in[12] + row_in[28]) * scaled_kernel[12]; + sum += (row_in[13] + row_in[27]) * scaled_kernel[13]; + sum += (row_in[14] + row_in[26]) * scaled_kernel[14]; + sum += (row_in[15] + row_in[25]) * scaled_kernel[15]; + sum += (row_in[16] + row_in[24]) * scaled_kernel[16]; + sum += (row_in[17] + row_in[23]) * scaled_kernel[17]; + sum += (row_in[18] + row_in[22]) * scaled_kernel[18]; + sum += (row_in[19] + row_in[21]) * scaled_kernel[19]; + sum += (row_in[20]) * scaled_kernel[20]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + break; + case 47: + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y) + border1 - offset; + for (size_t x = border1; x < border2; ++x, ++row_in) { + float sum = (row_in[0] + row_in[46]) * scaled_kernel[0]; + sum += (row_in[1] + row_in[45]) * scaled_kernel[1]; + sum += (row_in[2] + row_in[44]) * scaled_kernel[2]; + sum += (row_in[3] + row_in[43]) * scaled_kernel[3]; + sum += (row_in[4] + row_in[42]) * scaled_kernel[4]; + sum += (row_in[5] + row_in[41]) * scaled_kernel[5]; + sum += (row_in[6] + row_in[40]) * scaled_kernel[6]; + sum += (row_in[7] + row_in[39]) * scaled_kernel[7]; + sum += (row_in[8] + row_in[38]) * scaled_kernel[8]; + sum += (row_in[9] + row_in[37]) * scaled_kernel[9]; + sum += (row_in[10] + row_in[36]) * scaled_kernel[10]; + sum += (row_in[11] + row_in[35]) * scaled_kernel[11]; + sum += (row_in[12] + row_in[34]) * scaled_kernel[12]; + sum += (row_in[13] + row_in[33]) * scaled_kernel[13]; + sum += (row_in[14] + row_in[32]) * scaled_kernel[14]; + sum += (row_in[15] + row_in[31]) * scaled_kernel[15]; + sum += (row_in[16] + row_in[30]) * scaled_kernel[16]; + sum += (row_in[17] + row_in[29]) * scaled_kernel[17]; + sum += (row_in[18] + row_in[28]) * scaled_kernel[18]; + sum += (row_in[19] + row_in[27]) * scaled_kernel[19]; + sum += (row_in[20] + row_in[26]) * scaled_kernel[20]; + sum += (row_in[21] + row_in[25]) * scaled_kernel[21]; + sum += (row_in[22] + row_in[24]) * scaled_kernel[22]; + sum += (row_in[23]) * scaled_kernel[23]; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + row_out[y] = sum; + } + } + break; + default: + // printf("Warning: Unexpected kernel size! %d\n", len); +#else + default: +#endif + for (size_t y = 0; y < in.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = in.Row(y); + for (size_t j, x = border1; x < border2; ++x) { + const int d = x - offset; + float* BUTTERAUGLI_RESTRICT row_out = out->Row(x); + float sum = 0.0f; + for (j = 0; j <= len / 2; ++j) { + sum += row_in[d + j] * scaled_kernel[j]; + } + for (; j < len; ++j) { + sum += row_in[d + j] * scaled_kernel[len - 1 - j]; + } + row_out[y] = sum; + } + } + } + // right border + for (size_t x = border2; x < in.xsize(); ++x) { + ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, + out->Row(x)); + } + free(scaled_kernel); +} + +// A blur somewhat similar to a 2D Gaussian blur. +// See: https://en.wikipedia.org/wiki/Gaussian_blur +void Blur(const ImageF& in, float sigma, float border_ratio, + ImageF* BUTTERAUGLI_RESTRICT out) { + std::vector kernel = ComputeKernel(sigma); + ImageF tmp(in.ysize(), in.xsize()); + ConvolutionWithTranspose(in, kernel, border_ratio, &tmp); + ConvolutionWithTranspose(tmp, kernel, border_ratio, out); +} + +// Clamping linear interpolator. +inline double InterpolateClampNegative(const double* array, int size, + double ix) { + if (ix < 0) { + ix = 0; + } + int baseix = static_cast(ix); + double res; + if (baseix >= size - 1) { + res = array[size - 1]; + } else if (baseix < 0) { + res = array[0]; + } else { + double mix = ix - baseix; + int nextix = baseix + 1; + res = array[baseix] + mix * (array[nextix] - array[baseix]); + } + return res; +} + +double GammaMinArg() { + double out0, out1, out2; + OpsinAbsorbance(0.0, 0.0, 0.0, &out0, &out1, &out2); + return std::min(out0, std::min(out1, out2)); +} + +double GammaMaxArg() { + double out0, out1, out2; + OpsinAbsorbance(255.0, 255.0, 255.0, &out0, &out1, &out2); + return std::max(out0, std::max(out1, out2)); +} + +double SimpleGamma(double v) { + // A simple HDR compatible gamma function. + static const double kRetMul = 19.245013259874995; + static const double kRetAdd = -23.16046239805755; + static const double kVOffset = 9.9710635769299145; + if (v < 0) { + // This should happen rarely, but may lead to a NaN in log, which is + // undesirable. Since negative photons don't exist we solve the NaNs by + // clamping here. + v = 0; + } + return kRetMul * log(v + kVOffset) + kRetAdd; +} + +static inline double Gamma(double v) { + // SimpleGamma must be used when using an intensity_target with butteraugli + // to get values above 255.0. + // GammaPolynomial is faster but may only be used if the maximum input value + // is 255.0. + // TODO(lode): allow jxl to specify which to use depending on the intensity + // target. + return SimpleGamma(v); + // return GammaPolynomial(v); +} + +Image3F OpsinDynamicsImage(const Image3F& rgb) { + PROFILER_FUNC; + Image3F xyb(rgb.xsize(), rgb.ysize()); + const double kSigma = 1.2; + Image3F blurred(rgb.xsize(), rgb.ysize()); + Blur(rgb.Plane(0), kSigma, 0.0, const_cast(&blurred.Plane(0))); + Blur(rgb.Plane(1), kSigma, 0.0, const_cast(&blurred.Plane(1))); + Blur(rgb.Plane(2), kSigma, 0.0, const_cast(&blurred.Plane(2))); + for (size_t y = 0; y < rgb.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_r = rgb.ConstPlaneRow(0, y); + const float* BUTTERAUGLI_RESTRICT row_g = rgb.ConstPlaneRow(1, y); + const float* BUTTERAUGLI_RESTRICT row_b = rgb.ConstPlaneRow(2, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_r = blurred.ConstPlaneRow(0, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_g = blurred.ConstPlaneRow(1, y); + const float* BUTTERAUGLI_RESTRICT row_blurred_b = blurred.ConstPlaneRow(2, y); + float* BUTTERAUGLI_RESTRICT row_out_x = xyb.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_out_y = xyb.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_out_b = xyb.PlaneRow(2, y); + for (size_t x = 0; x < rgb.xsize(); ++x) { + float sensitivity[3]; + { + // Calculate sensitivity based on the smoothed image gamma derivative. + float pre_mixed0, pre_mixed1, pre_mixed2; + OpsinAbsorbance(row_blurred_r[x], row_blurred_g[x], row_blurred_b[x], + &pre_mixed0, &pre_mixed1, &pre_mixed2); + if (pre_mixed0 < 1e-4) { + pre_mixed0 = 1e-4; + } + if (pre_mixed1 < 1e-4) { + pre_mixed1 = 1e-4; + } + if (pre_mixed2 < 1e-4) { + pre_mixed2 = 1e-4; + } + sensitivity[0] = Gamma(pre_mixed0) / pre_mixed0; + sensitivity[1] = Gamma(pre_mixed1) / pre_mixed1; + sensitivity[2] = Gamma(pre_mixed2) / pre_mixed2; + if (sensitivity[0] < 1e-4) { + sensitivity[0] = 1e-4; + } + if (sensitivity[1] < 1e-4) { + sensitivity[1] = 1e-4; + } + if (sensitivity[2] < 1e-4) { + sensitivity[2] = 1e-4; + } + } + float cur_mixed0, cur_mixed1, cur_mixed2; + OpsinAbsorbance(row_r[x], row_g[x], row_b[x], &cur_mixed0, &cur_mixed1, + &cur_mixed2); + cur_mixed0 *= sensitivity[0]; + cur_mixed1 *= sensitivity[1]; + cur_mixed2 *= sensitivity[2]; + RgbToXyb(cur_mixed0, cur_mixed1, cur_mixed2, + &row_out_x[x], &row_out_y[x], &row_out_b[x]); + } + } + return xyb; +} + +// Make area around zero less important (remove it). +static BUTTERAUGLI_INLINE float RemoveRangeAroundZero(float w, float x) { + return x > w ? x - w : x < -w ? x + w : 0.0f; +} + +// Make area around zero more important (2x it until the limit). +static BUTTERAUGLI_INLINE float AmplifyRangeAroundZero(float w, float x) { + return x > w ? x + w : x < -w ? x - w : 2.0f * x; +} + +// XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. +// Vals space can be converted to L2-norm space (Euclidean and normalized) +// through visual masking. +template +BUTTERAUGLI_INLINE void XybLowFreqToVals(const V& x, const V& y, const V& b_arg, + V* BUTTERAUGLI_RESTRICT valx, + V* BUTTERAUGLI_RESTRICT valy, + V* BUTTERAUGLI_RESTRICT valb) { + static const double xmuli = 16.728334267161084; + static const double ymuli = 28.850249498135561; + static const double bmuli = 44.392179956162011; + static const double y_to_b_muli = -0.43716072660089195; + const V xmul(xmuli); + const V ymul(ymuli); + const V bmul(bmuli); + const V y_to_b_mul(y_to_b_muli); + const V b = b_arg + y_to_b_mul * y; + *valb = b * bmul; + *valx = x * xmul; + *valy = y * ymul; +} + +static ImageF SuppressInBrightAreas(size_t xsize, size_t ysize, double mul, + double mul2, double reg, const ImageF& hf, + const ImageF& brightness) { + PROFILER_FUNC; + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* rowhf = hf.Row(y); + const float* rowbr = brightness.Row(y); + float* rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + float v = rowhf[x]; + float scaler = mul * reg / (reg + rowbr[x]); + rownew[x] = scaler * v; + } + } + return inew; +} + +static float MaximumClamp(float v, float maxval) { + static const double kMul = 0.934914340314; + if (v >= maxval) { + v -= maxval; + v *= kMul; + v += maxval; + } else if (v < -maxval) { + v += maxval; + v *= kMul; + v -= maxval; + } + return v; +} + +static ImageF MaximumClamping(size_t xsize, size_t ysize, const ImageF& ix, + double yw) { + static const double kMul = 0.70036978414; + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* rowx = ix.Row(y); + float* rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + double v = rowx[x]; + if (v >= yw) { + v -= yw; + v *= kMul; + v += yw; + } else if (v < -yw) { + v += yw; + v *= kMul; + v -= yw; + } + rownew[x] = v; + } + } + return inew; +} + +static ImageF SuppressXByY(size_t xsize, size_t ysize, const ImageF& ix, + const ImageF& iy, const double yw) { + static const double s = 0.941388349694; + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* rowx = ix.Row(y); + const float* rowy = iy.Row(y); + float* rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + const double xval = rowx[x]; + const double yval = rowy[x]; + const double scaler = s + (yw * (1.0 - s)) / (yw + yval * yval); + rownew[x] = scaler * xval; + } + } + return inew; +} + +static void SeparateFrequencies(size_t xsize, size_t ysize, + const Image3F& xyb, + PsychoImage& ps) { + PROFILER_FUNC; + // Extract lf ... + static const double kSigmaLf = 7.15593339443; + static const double kSigmaHf = 3.22489901262; + static const double kSigmaUhf = 1.56416327805; + // Border handling is complicated. + static const double border_lf = 0.0; + static const double border_mf = 0.0; + static const double border_hf = 0.0; + ps.mf = Image3F(xsize, ysize); + ps.hf[0] = ImageF(xsize, ysize); + ps.hf[1] = ImageF(xsize, ysize); + ps.lf = Image3F(xyb.xsize(), xyb.ysize()); + ps.mf = Image3F(xyb.xsize(), xyb.ysize()); + for (int i = 0; i < 3; ++i) { + Blur(xyb.Plane(i), kSigmaLf, border_lf, + const_cast(&ps.lf.Plane(i))); + + // ... and keep everything else in mf. + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + ps.mf.PlaneRow(i, y)[x] = + xyb.PlaneRow(i, y)[x] - ps.lf.ConstPlaneRow(i, y)[x]; + } + } + if (i == 2) { + Blur(ps.mf.Plane(i), kSigmaHf, border_mf, + const_cast(&ps.mf.Plane(i))); + break; + } + // Divide mf into mf and hf. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(i, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] = row_mf[x]; + } + } + Blur(ps.mf.Plane(i), kSigmaHf, border_mf, + const_cast(&ps.mf.Plane(i))); + static const double kRemoveMfRange = 0.3; + static const double kAddMfRange = 0.1; + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] -= row_mf[x]; + row_mf[x] = RemoveRangeAroundZero(kRemoveMfRange, row_mf[x]); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_mf = ps.mf.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[1].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] -= row_mf[x]; + row_mf[x] = AmplifyRangeAroundZero(kAddMfRange, row_mf[x]); + } + } + } + } + // Suppress red-green by intensity change in the high freq channels. + static const double suppress = 286.09942757; + ps.hf[0] = SuppressXByY(xsize, ysize, ps.hf[0], ps.hf[1], suppress); + + ps.uhf[0] = ImageF(xsize, ysize); + ps.uhf[1] = ImageF(xsize, ysize); + for (int i = 0; i < 2; ++i) { + // Divide hf into hf and uhf. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[i].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] = row_hf[x]; + } + } + Blur(ps.hf[i], kSigmaUhf, border_hf, &ps.hf[i]); + static const double kRemoveHfRange = 0.12; + static const double kAddHfRange = 0.03; + static const double kRemoveUhfRange = 0.08; + static const double kAddUhfRange = 0.02; + static const double kMaxclampHf = 78.7416747972; + static const double kMaxclampUhf = 4.62878535439; + static double kMulYHf = 1.16155986803; + static double kMulYUhf = 2.32552960949; + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[0].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] -= row_hf[x]; + row_hf[x] = RemoveRangeAroundZero(kRemoveHfRange, row_hf[x]); + row_uhf[x] = RemoveRangeAroundZero(kRemoveUhfRange, row_uhf[x]); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_uhf = ps.uhf[1].Row(y); + float* BUTTERAUGLI_RESTRICT row_hf = ps.hf[1].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] -= row_hf[x]; + row_hf[x] = MaximumClamp(row_hf[x], kMaxclampHf); + row_uhf[x] = MaximumClamp(row_uhf[x], kMaxclampUhf); + row_uhf[x] *= kMulYUhf; + row_hf[x] *= kMulYHf; + row_hf[x] = AmplifyRangeAroundZero(kAddHfRange, row_hf[x]); + row_uhf[x] = AmplifyRangeAroundZero(kAddUhfRange, row_uhf[x]); + } + } + } + } + // Modify range around zero code only concerns the high frequency + // planes and only the X and Y channels. + // Convert low freq xyb to vals space so that we can do a simple squared sum + // diff on the low frequencies later. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT row_x = ps.lf.PlaneRow(0, y); + float* BUTTERAUGLI_RESTRICT row_y = ps.lf.PlaneRow(1, y); + float* BUTTERAUGLI_RESTRICT row_b = ps.lf.PlaneRow(2, y); + for (size_t x = 0; x < xsize; ++x) { + float valx, valy, valb; + XybLowFreqToVals(row_x[x], row_y[x], row_b[x], &valx, &valy, &valb); + row_x[x] = valx; + row_y[x] = valy; + row_b[x] = valb; + } + } +} + +static void L2Diff(const ImageF& i0, const ImageF& i1, const float w, + Image3F* BUTTERAUGLI_RESTRICT diffmap, size_t c) { + if (w == 0) { + return; + } + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = i0.ConstRow(y); + const float* BUTTERAUGLI_RESTRICT row1 = i1.ConstRow(y); + float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); + for (size_t x = 0; x < i0.xsize(); ++x) { + const float diff = row0[x] - row1[x]; + row_diff[x] += w * diff * diff; + } + } +} + +// i0 is the original image. +// i1 is the deformed copy. +static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, double w_0gt1, + double w_0lt1, + Image3F* BUTTERAUGLI_RESTRICT diffmap, size_t c) { + if (w_0gt1 == 0 && w_0lt1 == 0) { + return; + } + w_0gt1 *= 0.8; + w_0lt1 *= 0.8; + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = i0.Row(y); + const float* BUTTERAUGLI_RESTRICT row1 = i1.Row(y); + float* BUTTERAUGLI_RESTRICT row_diff = diffmap->PlaneRow(c, y); + for (size_t x = 0; x < i0.xsize(); ++x) { + // Primary symmetric quadratic objective. + double diff = row0[x] - row1[x]; + row_diff[x] += w_0gt1 * diff * diff; + + // Secondary half-open quadratic objectives. + const double fabs0 = std::fabs(row0[x]); + const double too_small = 0.4 * fabs0; + const double too_big = 1.0 * fabs0; + + if (row0[x] < 0) { + if (row1[x] > -too_small) { + double v = row1[x] + too_small; + row_diff[x] += w_0lt1 * v * v; + } else if (row1[x] < -too_big) { + double v = -row1[x] - too_big; + row_diff[x] += w_0lt1 * v * v; + } + } else { + if (row1[x] < too_small) { + double v = too_small - row1[x]; + row_diff[x] += w_0lt1 * v * v; + } else if (row1[x] > too_big) { + double v = row1[x] - too_big; + row_diff[x] += w_0lt1 * v * v; + } + } + } + } +} + +ImageF CalculateDiffmap(const ImageF& diffmap_in) { + PROFILER_FUNC; + // Take square root. + ImageF diffmap(diffmap_in.xsize(), diffmap_in.ysize()); + static const float kInitialSlope = 100.0f; + for (size_t y = 0; y < diffmap.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_in = diffmap_in.Row(y); + float* BUTTERAUGLI_RESTRICT row_out = diffmap.Row(y); + for (size_t x = 0; x < diffmap.xsize(); ++x) { + const float orig_val = row_in[x]; + // TODO(b/29974893): Until that is fixed do not call sqrt on very small + // numbers. + row_out[x] = (orig_val < (1.0f / (kInitialSlope * kInitialSlope)) + ? kInitialSlope * orig_val + : std::sqrt(orig_val)); + } + } + return diffmap; +} + +void MaskPsychoImage(const PsychoImage& pi0, const PsychoImage& pi1, + const size_t xsize, const size_t ysize, + MaskImage* BUTTERAUGLI_RESTRICT mask, + MaskImage* BUTTERAUGLI_RESTRICT mask_dc, + ImageF* BUTTERAUGLI_RESTRICT diff_ac) { + Image3F mask_xyb0(xsize, ysize); + Image3F mask_xyb1(xsize, ysize); + static const double muls[4] = { + 0.0, + 0.0632641915861, + 0.308212951541, + 1.16513324377, + }; + for (int i = 0; i < 2; ++i) { + double a = muls[2 * i]; + double b = muls[2 * i + 1]; + for (size_t y = 0; y < ysize; ++y) { + const float* BUTTERAUGLI_RESTRICT row_hf0 = pi0.hf[i].Row(y); + const float* BUTTERAUGLI_RESTRICT row_hf1 = pi1.hf[i].Row(y); + const float* BUTTERAUGLI_RESTRICT row_uhf0 = pi0.uhf[i].Row(y); + const float* BUTTERAUGLI_RESTRICT row_uhf1 = pi1.uhf[i].Row(y); + float* BUTTERAUGLI_RESTRICT row0 = mask_xyb0.PlaneRow(i, y); + float* BUTTERAUGLI_RESTRICT row1 = mask_xyb1.PlaneRow(i, y); + for (size_t x = 0; x < xsize; ++x) { + row0[x] = a * row_uhf0[x] + b * row_hf0[x]; + row1[x] = a * row_uhf1[x] + b * row_hf1[x]; + } + } + } + Mask(mask_xyb0, mask_xyb1, mask, mask_dc, diff_ac); +} + +ButteraugliComparator::ButteraugliComparator(const Image3F& rgb0, + double hf_asymmetry) + : xsize_(rgb0.xsize()), + ysize_(rgb0.ysize()), + hf_asymmetry_(hf_asymmetry), + sub_(nullptr) { + if (xsize_ < 8 || ysize_ < 8) { + return; + } + Image3F xyb0 = OpsinDynamicsImage(rgb0); + SeparateFrequencies(xsize_, ysize_, xyb0, pi0_); + + // Awful recursive construction of samples of different resolution. + // This is an after-thought and possibly somewhat parallel in + // functionality with the PsychoImage multi-resolution approach. + sub_ = new ButteraugliComparator(SubSample2x(rgb0), hf_asymmetry); +} + +ButteraugliComparator::~ButteraugliComparator() { + delete sub_; +} + + +void ButteraugliComparator::Mask( + MaskImage* BUTTERAUGLI_RESTRICT mask, + MaskImage* BUTTERAUGLI_RESTRICT mask_dc) const { + MaskPsychoImage(pi0_, pi0_, xsize_, ysize_, mask, mask_dc, nullptr); +} + +void ButteraugliComparator::Diffmap(const Image3F& rgb1, ImageF& result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&result); + return; + } + DiffmapOpsinDynamicsImage(OpsinDynamicsImage(rgb1), result); + if (sub_) { + if (sub_->xsize_ < 8 || sub_->ysize_ < 8) { + return; + } + ImageF subresult; + sub_->DiffmapOpsinDynamicsImage( + OpsinDynamicsImage(SubSample2x(rgb1)), subresult); + AddSupersampled2x(subresult, 0.5, result); + } +} + +void ButteraugliComparator::DiffmapOpsinDynamicsImage(const Image3F& xyb1, + ImageF& result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&result); + return; + } + PsychoImage pi1; + SeparateFrequencies(xsize_, ysize_, xyb1, pi1); + result = ImageF(xsize_, ysize_); + DiffmapPsychoImage(pi1, result); +} + +void ButteraugliComparator::DiffmapPsychoImage(const PsychoImage& pi1, + ImageF& result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) { + ZeroFillImage(&result); + return; + } + Image3F block_diff_dc(xsize_, ysize_); + ZeroFillImage(&block_diff_dc); + Image3F block_diff_ac(xsize_, ysize_); + ZeroFillImage(&block_diff_ac); + static const double wUhfMalta = 8.3230030810258917; + static const double norm1Uhf = 81.97966510743784; + MaltaDiffMap(pi0_.uhf[1], pi1.uhf[1], wUhfMalta * hf_asymmetry_, + wUhfMalta / hf_asymmetry_, norm1Uhf, &block_diff_ac, 1); + + static const double wUhfMaltaX = 51.738409933971198; + static const double norm1UhfX = 19.390881354476463; + MaltaDiffMap(pi0_.uhf[0], pi1.uhf[0], wUhfMaltaX * hf_asymmetry_, + wUhfMaltaX / hf_asymmetry_, norm1UhfX, &block_diff_ac, 0); + + static const double wHfMalta = 240.0908262374156; + static const double norm1Hf = 213.151889155; + MaltaDiffMapLF(pi0_.hf[1], pi1.hf[1], wHfMalta * std::sqrt(hf_asymmetry_), + wHfMalta / std::sqrt(hf_asymmetry_), norm1Hf, &block_diff_ac, + 1); + + static const double wHfMaltaX = 160.0; + static const double norm1HfX = 80.0; + MaltaDiffMapLF(pi0_.hf[0], pi1.hf[0], wHfMaltaX * std::sqrt(hf_asymmetry_), + wHfMaltaX / std::sqrt(hf_asymmetry_), norm1HfX, &block_diff_ac, + 0); + + static const double wMfMalta = 163.56963612738494; + static const double norm1Mf = 0.1635008533899469; + MaltaDiffMapLF(pi0_.mf.Plane(1), pi1.mf.Plane(1), wMfMalta, wMfMalta, norm1Mf, + &block_diff_ac, 1); + + static const double wMfMaltaX = 6164.558625327204; + static const double norm1MfX = 1002.5; + MaltaDiffMapLF(pi0_.mf.Plane(0), pi1.mf.Plane(0), wMfMaltaX, wMfMaltaX, + norm1MfX, &block_diff_ac, 0); + + static const double wmul[9] = { + 32, 5.0, 0, 32, 5, 237.33703833286302, 0.8170086922843028, + 1.0323708525451885, 5.5346699491372346, + }; + for (size_t c = 0; c < 3; ++c) { + if (c < 2) { // No blue channel error accumulated at HF. + L2DiffAsymmetric(pi0_.hf[c], pi1.hf[c], wmul[c] * hf_asymmetry_, + wmul[c] / hf_asymmetry_, &block_diff_ac, c); + } + L2Diff(pi0_.mf.Plane(c), pi1.mf.Plane(c), wmul[3 + c], &block_diff_ac, c); + L2Diff(pi0_.lf.Plane(c), pi1.lf.Plane(c), wmul[6 + c], &block_diff_dc, c); + } + + MaskImage mask_xyb_ac; + MaskImage mask_xyb_dc; + MaskPsychoImage(pi0_, pi1, xsize_, ysize_, &mask_xyb_ac, &mask_xyb_dc, + const_cast(&block_diff_ac.Plane(1))); + + result = CalculateDiffmap( + CombineChannels(mask_xyb_ac, mask_xyb_dc, block_diff_dc, block_diff_ac)); +} + +// Allows PaddedMaltaUnit to call either function via overloading. +struct MaltaTagLF {}; +struct MaltaTag {}; + +static float MaltaUnit(MaltaTagLF /*tag*/, const float* BUTTERAUGLI_RESTRICT d, + const int xs) { + const int xs3 = 3 * xs; + float retval = 0; + { + // x grows, y constant + float sum = d[-4] + d[-2] + d[0] + d[2] + d[4]; + retval += sum * sum; + } + { + // y grows, x constant + float sum = d[-xs3 - xs] + d[-xs - xs] + d[0] + d[xs + xs] + d[xs3 + xs]; + retval += sum * sum; + } + { + // both grow + float sum = + d[-xs3 - 3] + d[-xs - xs - 2] + d[0] + d[xs + xs + 2] + d[xs3 + 3]; + retval += sum * sum; + } + { + // y grows, x shrinks + float sum = + d[-xs3 + 3] + d[-xs - xs + 2] + d[0] + d[xs + xs - 2] + d[xs3 - 3]; + retval += sum * sum; + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + float sum = d[-xs3 - xs + 1] + d[-xs - xs + 1] + d[0] + d[xs + xs - 1] + + d[xs3 + xs - 1]; + retval += sum * sum; + } + { + // y grows -4 to 4, x grows -1 -> 1 + float sum = d[-xs3 - xs - 1] + d[-xs - xs - 1] + d[0] + d[xs + xs + 1] + + d[xs3 + xs + 1]; + retval += sum * sum; + } + { + // x grows -4 to 4, y grows -1 to 1 + float sum = d[-4 - xs] + d[-2 - xs] + d[0] + d[2 + xs] + d[4 + xs]; + retval += sum * sum; + } + { + // x grows -4 to 4, y shrinks 1 to -1 + float sum = d[-4 + xs] + d[-2 + xs] + d[0] + d[2 - xs] + d[4 - xs]; + retval += sum * sum; + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7______*__ + 8_________ */ + float sum = + d[-xs3 - 2] + d[-xs - xs - 1] + d[0] + d[xs + xs + 1] + d[xs3 + 2]; + retval += sum * sum; + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7__*______ + 8_________ */ + float sum = + d[-xs3 + 2] + d[-xs - xs + 1] + d[0] + d[xs + xs - 1] + d[xs3 - 2]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__*______ + 4____0____ + 5______*__ + 6_______*_ + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 3] + d[-xs - 2] + d[0] + d[xs + 2] + d[xs + xs + 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_______*_ + 3______*__ + 4____0____ + 5__*______ + 6_*_______ + 7_________ + 8_________ */ + float sum = + d[-xs - xs + 3] + d[-xs + 2] + d[0] + d[xs - 2] + d[xs + xs - 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2________* + 3______*__ + 4____0____ + 5__*______ + 6*________ + 7_________ + 8_________ */ + + float sum = + d[xs + xs - 4] + d[xs - 2] + d[0] + d[-xs + 2] + d[-xs - xs + 4]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2*________ + 3__*______ + 4____0____ + 5______*__ + 6________* + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 4] + d[-xs - 2] + d[0] + d[xs + 2] + d[xs + xs + 4]; + retval += sum * sum; + } + { + /* 0__*______ + 1_________ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7_________ + 8______*__ */ + float sum = d[-xs3 - xs - 2] + d[-xs - xs - 1] + d[0] + d[xs + xs + 1] + + d[xs3 + xs + 2]; + retval += sum * sum; + } + { + /* 0______*__ + 1_________ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7_________ + 8__*______ */ + float sum = d[-xs3 - xs + 2] + d[-xs - xs + 1] + d[0] + d[xs + xs - 1] + + d[xs3 + xs - 2]; + retval += sum * sum; + } + return retval; +} + +static float MaltaUnit(MaltaTag /*tag*/, const float* BUTTERAUGLI_RESTRICT d, + const int xs) { + const int xs3 = 3 * xs; + float retval = 0; + { + // x grows, y constant + float sum = + d[-4] + d[-3] + d[-2] + d[-1] + d[0] + d[1] + d[2] + d[3] + d[4]; + retval += sum * sum; + } + { + // y grows, x constant + float sum = d[-xs3 - xs] + d[-xs3] + d[-xs - xs] + d[-xs] + d[0] + d[xs] + + d[xs + xs] + d[xs3] + d[xs3 + xs]; + retval += sum * sum; + } + { + // both grow + float sum = d[-xs3 - 3] + d[-xs - xs - 2] + d[-xs - 1] + d[0] + d[xs + 1] + + d[xs + xs + 2] + d[xs3 + 3]; + retval += sum * sum; + } + { + // y grows, x shrinks + float sum = d[-xs3 + 3] + d[-xs - xs + 2] + d[-xs + 1] + d[0] + d[xs - 1] + + d[xs + xs - 2] + d[xs3 - 3]; + retval += sum * sum; + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + float sum = d[-xs3 - xs + 1] + d[-xs3 + 1] + d[-xs - xs + 1] + d[-xs] + + d[0] + d[xs] + d[xs + xs - 1] + d[xs3 - 1] + d[xs3 + xs - 1]; + retval += sum * sum; + } + { + // y grows -4 to 4, x grows -1 -> 1 + float sum = d[-xs3 - xs - 1] + d[-xs3 - 1] + d[-xs - xs - 1] + d[-xs] + + d[0] + d[xs] + d[xs + xs + 1] + d[xs3 + 1] + d[xs3 + xs + 1]; + retval += sum * sum; + } + { + // x grows -4 to 4, y grows -1 to 1 + float sum = d[-4 - xs] + d[-3 - xs] + d[-2 - xs] + d[-1] + d[0] + d[1] + + d[2 + xs] + d[3 + xs] + d[4 + xs]; + retval += sum * sum; + } + { + // x grows -4 to 4, y shrinks 1 to -1 + float sum = d[-4 + xs] + d[-3 + xs] + d[-2 + xs] + d[-1] + d[0] + d[1] + + d[2 - xs] + d[3 - xs] + d[4 - xs]; + retval += sum * sum; + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3___*_____ + 4____0____ + 5_____*___ + 6_____*___ + 7______*__ + 8_________ */ + float sum = d[-xs3 - 2] + d[-xs - xs - 1] + d[-xs - 1] + d[0] + d[xs + 1] + + d[xs + xs + 1] + d[xs3 + 2]; + retval += sum * sum; + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_____*___ + 4____0____ + 5___*_____ + 6___*_____ + 7__*______ + 8_________ */ + float sum = d[-xs3 + 2] + d[-xs - xs + 1] + d[-xs + 1] + d[0] + d[xs - 1] + + d[xs + xs - 1] + d[xs3 - 2]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__**_____ + 4____0____ + 5_____**__ + 6_______*_ + 7_________ + 8_________ */ + float sum = d[-xs - xs - 3] + d[-xs - 2] + d[-xs - 1] + d[0] + d[xs + 1] + + d[xs + 2] + d[xs + xs + 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_______*_ + 3_____**__ + 4____0____ + 5__**_____ + 6_*_______ + 7_________ + 8_________ */ + float sum = d[-xs - xs + 3] + d[-xs + 2] + d[-xs + 1] + d[0] + d[xs - 1] + + d[xs - 2] + d[xs + xs - 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_________ + 3______*** + 4___*0*___ + 5***______ + 6_________ + 7_________ + 8_________ */ + + float sum = d[xs - 4] + d[xs - 3] + d[xs - 2] + d[-1] + + d[0] + d[1] + d[-xs + 2] + d[-xs + 3] + d[-xs + 4]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_________ + 3***______ + 4___*0*___ + 5______*** + 6_________ + 7_________ + 8_________ */ + float sum = d[-xs - 4] + d[-xs - 3] + d[-xs - 2] + d[-1] + + d[0] + d[1] + d[xs + 2] + d[xs + 3] + d[xs + 4]; + retval += sum * sum; + } + { + /* 0___*_____ + 1___*_____ + 2___*_____ + 3____*____ + 4____0____ + 5____*____ + 6_____*___ + 7_____*___ + 8_____*___ */ + float sum = d[-xs3 - xs - 1] + d[-xs3 - 1] + d[-xs - xs - 1] + d[-xs] + + d[0] + d[xs] + d[xs + xs + 1] + d[xs3 + 1] + d[xs3 + xs + 1]; + retval += sum * sum; + } + { + /* 0_____*___ + 1_____*___ + 2____ *___ + 3____*____ + 4____0____ + 5____*____ + 6___*_____ + 7___*_____ + 8___*_____ */ + float sum = d[-xs3 - xs + 1] + d[-xs3 + 1] + d[-xs - xs + 1] + d[-xs] + + d[0] + d[xs] + d[xs + xs - 1] + d[xs3 - 1] + d[xs3 + xs - 1]; + retval += sum * sum; + } + return retval; +} + +// Returns MaltaUnit. "fastMode" avoids bounds-checks when x0 and y0 are known +// to be far enough from the image borders. "diffs" is a packed image. +template +static BUTTERAUGLI_INLINE float PaddedMaltaUnit( + const float* BUTTERAUGLI_RESTRICT diffs, const size_t x0, const size_t y0, + const size_t xsize_, const size_t ysize_) { + int ix0 = y0 * xsize_ + x0; + const float* BUTTERAUGLI_RESTRICT d = &diffs[ix0]; + if (fastMode || + (x0 >= 4 && y0 >= 4 && x0 < (xsize_ - 4) && y0 < (ysize_ - 4))) { + return MaltaUnit(Tag(), d, xsize_); + } + + float borderimage[9 * 9]; + for (int dy = 0; dy < 9; ++dy) { + int y = y0 + dy - 4; + if (y < 0 || static_cast(y) >= ysize_) { + for (int dx = 0; dx < 9; ++dx) { + borderimage[dy * 9 + dx] = 0.0f; + } + } else { + for (int dx = 0; dx < 9; ++dx) { + int x = x0 + dx - 4; + if (x < 0 || static_cast(x) >= xsize_) { + borderimage[dy * 9 + dx] = 0.0f; + } else { + borderimage[dy * 9 + dx] = diffs[y * xsize_ + x]; + } + } + } + } + return MaltaUnit(Tag(), &borderimage[4 * 9 + 4], 9); +} + +template +static void MaltaDiffMapImpl(const ImageF& lum0, const ImageF& lum1, + const size_t xsize_, const size_t ysize_, + const double w_0gt1, const double w_0lt1, + const double norm1, const double len, + const double mulli, + Image3F* BUTTERAUGLI_RESTRICT block_diff_ac, + size_t c) { + const float kWeight0 = 0.5; + const float kWeight1 = 0.33; + + const double w_pre0gt1 = mulli * std::sqrt(kWeight0 * w_0gt1) / (len * 2 + 1); + const double w_pre0lt1 = mulli * std::sqrt(kWeight1 * w_0lt1) / (len * 2 + 1); + const float norm2_0gt1 = w_pre0gt1 * norm1; + const float norm2_0lt1 = w_pre0lt1 * norm1; + + std::vector diffs(ysize_ * xsize_); + for (size_t y = 0, ix = 0; y < ysize_; ++y) { + const float* BUTTERAUGLI_RESTRICT row0 = lum0.Row(y); + const float* BUTTERAUGLI_RESTRICT row1 = lum1.Row(y); + for (size_t x = 0; x < xsize_; ++x, ++ix) { + const float absval = 0.5f * (std::abs(row0[x]) + std::abs(row1[x])); + const float diff = row0[x] - row1[x]; + const float scaler = norm2_0gt1 / (static_cast(norm1) + absval); + + // Primary symmetric quadratic objective. + diffs[ix] = scaler * diff; + + const float scaler2 = norm2_0lt1 / (static_cast(norm1) + absval); + const double fabs0 = std::fabs(row0[x]); + + // Secondary half-open quadratic objectives. + const double too_small = 0.55 * fabs0; + const double too_big = 1.05 * fabs0; + + if (row0[x] < 0) { + if (row1[x] > -too_small) { + double impact = scaler2 * (row1[x] + too_small); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } else if (row1[x] < -too_big) { + double impact = scaler2 * (-row1[x] - too_big); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } + } else { + if (row1[x] < too_small) { + double impact = scaler2 * (too_small - row1[x]); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } else if (row1[x] > too_big) { + double impact = scaler2 * (row1[x] - too_big); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } + } + } + } + + size_t y0 = 0; + // Top + for (; y0 < 4; ++y0) { + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } + + // Middle + for (; y0 < ysize_ - 4; ++y0) { + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + size_t x0 = 0; + for (; x0 < 4; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + for (; x0 < xsize_ - 4; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + + for (; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } + + // Bottom + for (; y0 < ysize_; ++y0) { + float* BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->PlaneRow(c, y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } +} + +void ButteraugliComparator::MaltaDiffMap( + const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, + Image3F* BUTTERAUGLI_RESTRICT block_diff_ac, size_t c) const { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.359826387683; + MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, w_0gt1, w_0lt1, norm1, + len, mulli, block_diff_ac, c); +} + +void ButteraugliComparator::MaltaDiffMapLF( + const ImageF& lum0, const ImageF& lum1, const double w_0gt1, + const double w_0lt1, const double norm1, + Image3F* BUTTERAUGLI_RESTRICT block_diff_ac, size_t c) const { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.737143715861; + MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, w_0gt1, w_0lt1, + norm1, len, mulli, block_diff_ac, c); +} + +ImageF ButteraugliComparator::CombineChannels( + const MaskImage& mask_xyb, const MaskImage& mask_xyb_dc, + const Image3F& block_diff_dc, const Image3F& block_diff_ac) const { + PROFILER_FUNC; + ImageF result(xsize_, ysize_); + for (size_t y = 0; y < ysize_; ++y) { + float* BUTTERAUGLI_RESTRICT row_out = result.Row(y); + for (size_t x = 0; x < xsize_; ++x) { + float mask[2] = { + mask_xyb.mask_x.Row(y)[x], + mask_xyb.mask_yb.Row(y)[x], + }; + float dc_mask[2] = { + mask_xyb_dc.mask_x.Row(y)[x], + mask_xyb_dc.mask_yb.Row(y)[x], + }; + float diff_dc[3]; + float diff_ac[3]; + for (int i = 0; i < 3; ++i) { + diff_dc[i] = block_diff_dc.PlaneRow(i, y)[x]; + diff_ac[i] = block_diff_ac.PlaneRow(i, y)[x]; + } + row_out[x] = MaskColor(diff_dc, dc_mask) + MaskColor(diff_ac, mask); + } + } + return result; +} + +double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { + PROFILER_FUNC; + float retval = 0.0f; + for (size_t y = 0; y < diffmap.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row = diffmap.ConstRow(y); + for (size_t x = 0; x < diffmap.xsize(); ++x) { + retval = std::max(retval, row[x]); + } + } + return retval; +} + +// ===== Functions used by Mask only ===== +static std::array MakeMask(double extmul, double extoff, + double mul, double offset, + double scaler) { + std::array lut; + for (size_t i = 0; i < lut.size(); ++i) { + const double c = mul / ((0.01 * scaler * i) + offset); + lut[i] = kGlobalScale * (1.0 + extmul * (c + extoff)); + if (lut[i] < 1e-5) { + lut[i] = 1e-5; + } + JXL_DASSERT(lut[i] >= 0.0); + lut[i] *= lut[i]; + } + return lut; +} + +double MaskX(double delta) { + static const double extmul = 1.9741728897212369; + static const double extoff = 2.0124797155052101; + static const double offset = 0.26118960206891428; + static const double scaler = 386.58249095811573; + static const double mul = 4.0096602151572194; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskY(double delta) { + static const double extmul = 3.2760071347758579; + static const double extoff = -3.3377693215128232; + static const double offset = 1.9592295279427681; + static const double scaler = 1.4422616544932898; + static const double mul = 9.6463717073130066; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcX(double delta) { + static const double extmul = 7.9213692914149458; + static const double extoff = 2.5312740226978478; + static const double offset = 0.1060403567938254; + static const double scaler = 43.546844862805052; + static const double mul = 0.32110246816137022; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcY(double delta) { + static const double extmul = 0.019875068816088557; + static const double extoff = 0.1; + static const double offset = 0.1; + static const double scaler = 7.0; + static const double mul = 9.8744858184185791; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +ImageF DiffPrecomputeX(const ImageF& xyb0, const ImageF& xyb1, + float mul, float cutoff) { + PROFILER_FUNC; + const size_t xsize = xyb0.xsize(); + const size_t ysize = xyb0.ysize(); + ImageF result(xsize, ysize); + size_t x1, y1; + size_t x2, y2; + for (size_t y = 0; y < ysize; ++y) { + if (y + 1 < ysize) { + y2 = y + 1; + } else if (y > 0) { + y2 = y - 1; + } else { + y2 = y; + } + if (y == 0 && ysize >= 2) { + y1 = y + 1; + } else if (y > 0) { + y1 = y - 1; + } else { + y1 = y; + } + const float* BUTTERAUGLI_RESTRICT row0_in = xyb0.Row(y); + const float* BUTTERAUGLI_RESTRICT row1_in = xyb1.Row(y); + const float* BUTTERAUGLI_RESTRICT row0_in1 = xyb0.Row(y1); + const float* BUTTERAUGLI_RESTRICT row1_in1 = xyb1.Row(y1); + const float* BUTTERAUGLI_RESTRICT row0_in2 = xyb0.Row(y2); + const float* BUTTERAUGLI_RESTRICT row1_in2 = xyb1.Row(y2); + float* BUTTERAUGLI_RESTRICT row_out = result.Row(y); + for (size_t x = 0; x < xsize; ++x) { + if (x + 1 < xsize) { + x2 = x + 1; + } else if (x > 0) { + x2 = x - 1; + } else { + x2 = x; + } + if (x == 0 && xsize >= 2) { + x1 = x + 1; + } else if (x > 0) { + x1 = x - 1; + } else { + x1 = x; + } + double sup0 = (std::fabs(row0_in[x] - row0_in[x2]) + + std::fabs(row0_in[x] - row0_in2[x]) + + std::fabs(row0_in[x] - row0_in[x1]) + + std::fabs(row0_in[x] - row0_in1[x]) + + 3 * (std::fabs(row0_in2[x] - row0_in1[x]) + + std::fabs(row0_in[x1] - row0_in[x2]))); + double sup1 = (std::fabs(row1_in[x] - row1_in[x2]) + + std::fabs(row1_in[x] - row1_in2[x]) + + std::fabs(row1_in[x] - row1_in[x1]) + + std::fabs(row1_in[x] - row1_in1[x]) + + 3 * (std::fabs(row1_in2[x] - row1_in1[x]) + + std::fabs(row1_in[x1] - row1_in[x2]))); + + row_out[x] = mul * std::min(sup0, sup1); + if (row_out[x] >= cutoff) { + row_out[x] = cutoff; + } + { + static const double limit = 0.5 * cutoff; + if (row_out[x] >= limit) { + row_out[x] += limit; + row_out[x] *= 0.5; + } + } + { + static const double limit = 0.25 * cutoff; + if (row_out[x] >= limit) { + row_out[x] += limit; + row_out[x] *= 0.5; + } + } + } + } + return result; +} + +// Precalculates masking for y channel, giving masks for +// both images back so that they can be used for similarity comparisons +// too. +void DiffPrecomputeY(const ImageF& xyb0, const ImageF& xyb1, + float mul, float mul2, + ImageF *out0, ImageF *out1) { + PROFILER_FUNC; + const size_t xsize = xyb0.xsize(); + const size_t ysize = xyb0.ysize(); + size_t x1, y1; + size_t x2, y2; + for (size_t y = 0; y < ysize; ++y) { + if (y + 1 < ysize) { + y2 = y + 1; + } else if (y > 0) { + y2 = y - 1; + } else { + y2 = y; + } + if (y == 0 && ysize >= 2) { + y1 = y + 1; + } else if (y > 0) { + y1 = y - 1; + } else { + y1 = y; + } + const float* BUTTERAUGLI_RESTRICT row0_in = xyb0.Row(y); + const float* BUTTERAUGLI_RESTRICT row1_in = xyb1.Row(y); + const float* BUTTERAUGLI_RESTRICT row0_in1 = xyb0.Row(y1); + const float* BUTTERAUGLI_RESTRICT row1_in1 = xyb1.Row(y1); + const float* BUTTERAUGLI_RESTRICT row0_in2 = xyb0.Row(y2); + const float* BUTTERAUGLI_RESTRICT row1_in2 = xyb1.Row(y2); + float* BUTTERAUGLI_RESTRICT row_out0 = out0->Row(y); + float* BUTTERAUGLI_RESTRICT row_out1 = out1->Row(y); + for (size_t x = 0; x < xsize; ++x) { + if (x + 1 < xsize) { + x2 = x + 1; + } else if (x > 0) { + x2 = x - 1; + } else { + x2 = x; + } + if (x == 0 && xsize >= 2) { + x1 = x + 1; + } else if (x > 0) { + x1 = x - 1; + } else { + x1 = x; + } + double sup0 = (std::fabs(row0_in[x] - row0_in[x2]) + + std::fabs(row0_in[x] - row0_in2[x]) + + std::fabs(row0_in[x] - row0_in[x1]) + + std::fabs(row0_in[x] - row0_in1[x]) + + 3 * (std::fabs(row0_in2[x] - row0_in1[x]) + + std::fabs(row0_in[x1] - row0_in[x2]))); + double sup1 = (std::fabs(row1_in[x] - row1_in[x2]) + + std::fabs(row1_in[x] - row1_in2[x]) + + std::fabs(row1_in[x] - row1_in[x1]) + + std::fabs(row1_in[x] - row1_in1[x]) + + 3 * (std::fabs(row1_in2[x] - row1_in1[x]) + + std::fabs(row1_in[x1] - row1_in[x2]))); + // kBias makes log behave more linearly. + static const double kBias = 7; + row_out0[x] = mul * (log(sup0 * sup0 * mul2 + kBias) - log(kBias)); + row_out1[x] = mul * (log(sup1 * sup1 * mul2 + kBias) - log(kBias)); + } + } +} + +void Mask(const Image3F& xyb0, const Image3F& xyb1, + MaskImage* BUTTERAUGLI_RESTRICT mask, + MaskImage* BUTTERAUGLI_RESTRICT mask_dc, + ImageF* BUTTERAUGLI_RESTRICT diff_ac) { + PROFILER_FUNC; + const size_t xsize = xyb0.xsize(); + const size_t ysize = xyb0.ysize(); + *mask = MaskImage(xsize, ysize); + *mask_dc = MaskImage(xsize, ysize); + static const double muls[2] = { + 0.580411307192999992, + 0.236069675367, + }; + double normalizer = { + 1.0 / (muls[0] + muls[1]), + }; + static const double r0 = 1.63479141169; + static const double r1 = 5.5; + static const double r2 = 8.0; + static const double border_ratio = 0; + + { + // X component + static const double mul = 0.533043878407; + static const double cutoff = 0.5; + ImageF diff = DiffPrecomputeX(xyb0.Plane(0), xyb1.Plane(0), mul, cutoff); + ImageF blurred(diff.xsize(), diff.ysize()); + Blur(diff, r2, border_ratio, &blurred); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + mask->mask_x.Row(y)[x] = blurred.Row(y)[x]; + } + } + } + { + // Y component + static const double mul = 0.559; + static const double mul2 = 1.0; + ImageF diff0(xsize, ysize); + ImageF diff1(xsize, ysize); + ImageF blurred0_a(xsize, ysize); + ImageF blurred0_b(xsize, ysize); + ImageF blurred1_a(xsize, ysize); + ImageF blurred1_b(xsize, ysize); + DiffPrecomputeY(xyb0.Plane(1), xyb1.Plane(1), mul, mul2, &diff0, &diff1); + Blur(diff0, r0, border_ratio, &blurred0_a); + Blur(diff0, r1, border_ratio, &blurred0_b); + Blur(diff1, r0, border_ratio, &blurred1_a); + Blur(diff1, r1, border_ratio, &blurred1_b); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + const double val = normalizer * ( + muls[0] * blurred1_a.Row(y)[x] + + muls[1] * blurred1_b.Row(y)[x]); + mask->mask_yb.Row(y)[x] = val; + if (diff_ac != nullptr) { + static const double kMaskToErrorMul = 0.2; + double va = blurred0_a.Row(y)[x] - blurred1_a.Row(y)[x]; + double wa = kMaskToErrorMul * normalizer * muls[0] * va; + double vb = blurred0_b.Row(y)[x] - blurred1_b.Row(y)[x]; + double wb = kMaskToErrorMul * normalizer * muls[1] * vb; + diff_ac->Row(y)[x] += wa * wa + wb * wb; + } + } + } + } + // B component + static const double w00 = 564.23707674490015; + static const double w11 = 11.480266246995546; + static const double p1_to_p0 = 0.022922908053854666; + + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + const double s0 = mask->mask_x.Row(y)[x]; + const double s1 = mask->mask_yb.Row(y)[x]; + const double p1 = w11 * s1; + const double p0 = w00 * s0 + p1_to_p0 * p1; + + mask->mask_x.Row(y)[x] = MaskX(p0); + mask->mask_yb.Row(y)[x] = MaskY(p1); + mask_dc->mask_x.Row(y)[x] = MaskDcX(p0); + mask_dc->mask_yb.Row(y)[x] = MaskDcY(p1); + } + } +} + +bool ButteraugliDiffmap(const Image3F& rgb0, const Image3F& rgb1, + double hf_asymmetry, ImageF& result_image) { + PROFILER_FUNC; + const size_t xsize = rgb0.xsize(); + const size_t ysize = rgb0.ysize(); + if (xsize < 1 || ysize < 1) { + return JXL_FAILURE("Zero-sized image"); + } + if (!SameSize(rgb0, rgb1)) { + return JXL_FAILURE("Size mismatch"); + } + static const int kMax = 8; + if (xsize < kMax || ysize < kMax) { + // Butteraugli values for small (where xsize or ysize is smaller + // than 8 pixels) images are non-sensical, but most likely it is + // less disruptive to try to compute something than just give up. + // Temporarily extend the borders of the image to fit 8 x 8 size. + size_t xborder = xsize < kMax ? (kMax - xsize) / 2 : 0; + size_t yborder = ysize < kMax ? (kMax - ysize) / 2 : 0; + size_t xscaled = std::max(kMax, xsize); + size_t yscaled = std::max(kMax, ysize); + Image3F scaled0(xscaled, yscaled); + Image3F scaled1(xscaled, yscaled); + for (int i = 0; i < 3; ++i) { + for (size_t y = 0; y < yscaled; ++y) { + for (size_t x = 0; x < xscaled; ++x) { + size_t x2 = + std::min(xsize - 1, std::max(0, x - xborder)); + size_t y2 = + std::min(ysize - 1, std::max(0, y - yborder)); + scaled0.PlaneRow(i, y)[x] = rgb0.PlaneRow(i, y2)[x2]; + scaled1.PlaneRow(i, y)[x] = rgb1.PlaneRow(i, y2)[x2]; + } + } + } + ImageF diffmap_scaled; + const bool ok = + ButteraugliDiffmap(scaled0, scaled1, hf_asymmetry, diffmap_scaled); + result_image = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + result_image.Row(y)[x] = diffmap_scaled.Row(y + yborder)[x + xborder]; + } + } + return ok; + } + ButteraugliComparator butteraugli(rgb0, hf_asymmetry); + butteraugli.Diffmap(rgb1, result_image); + return true; +} + +bool ButteraugliInterface(const Image3F& rgb0, const Image3F& rgb1, + float hf_asymmetry, ImageF& diffmap, + double& diffvalue) { + if (!ButteraugliDiffmap(rgb0, rgb1, hf_asymmetry, diffmap)) { + return false; + } + diffvalue = ButteraugliScoreFromDiffmap(diffmap); + return true; +} + +double ButteraugliFuzzyClass(double score) { + static const double fuzzy_width_up = 4.8; + static const double fuzzy_width_down = 4.8; + static const double m0 = 2.0; + static const double scaler = 0.7777; + double val; + if (score < 1.0) { + // val in [scaler .. 2.0] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_down)); + val -= 1.0; // from [1 .. 2] to [0 .. 1] + val *= 2.0 - scaler; // from [0 .. 1] to [0 .. 2.0 - scaler] + val += scaler; // from [0 .. 2.0 - scaler] to [scaler .. 2.0] + } else { + // val in [0 .. scaler] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_up)); + val *= scaler; + } + return val; +} + +// #define PRINT_OUT_NORMALIZATION + +double ButteraugliFuzzyInverse(double seek) { + double pos = 0; + // NOLINTNEXTLINE(clang-analyzer-security.FloatLoopCounter) + for (double range = 1.0; range >= 1e-10; range *= 0.5) { + double cur = ButteraugliFuzzyClass(pos); + if (cur < seek) { + pos -= range; + } else { + pos += range; + } + } +#ifdef PRINT_OUT_NORMALIZATION + if (seek == 1.0) { + fprintf(stderr, "Fuzzy inverse %g\n", pos); + } +#endif + return pos; +} + +#ifdef PRINT_OUT_NORMALIZATION +static double print_out_normalization = ButteraugliFuzzyInverse(1.0); +#endif + + + +namespace { + +void ScoreToRgb(double score, double good_threshold, double bad_threshold, + uint8_t rgb[3]) { + double heatmap[12][3] = { + {0, 0, 0}, + {0, 0, 1}, + {0, 1, 1}, + {0, 1, 0}, // Good level + {1, 1, 0}, + {1, 0, 0}, // Bad level + {1, 0, 1}, + {0.5, 0.5, 1.0}, + {1.0, 0.5, 0.5}, // Pastel colors for the very bad quality range. + {1.0, 1.0, 0.5}, + {1, 1, 1}, + {1, 1, 1}, // Last color repeated to have a solid range of white. + }; + if (score < good_threshold) { + score = (score / good_threshold) * 0.3; + } else if (score < bad_threshold) { + score = 0.3 + + (score - good_threshold) / (bad_threshold - good_threshold) * 0.15; + } else { + score = 0.45 + (score - bad_threshold) / (bad_threshold * 12) * 0.5; + } + static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); + score = std::min(std::max(score * (kTableSize - 1), 0.0), + kTableSize - 2); + int ix = static_cast(score); + ix = std::min(std::max(0, ix), kTableSize - 2); // Handle NaN + double mix = score - ix; + for (int i = 0; i < 3; ++i) { + double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; + rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); + } +} + +} // namespace + +Image3B CreateHeatMapImage(const ImageF& distmap, double good_threshold, + double bad_threshold) { + Image3B heatmap(distmap.xsize(), distmap.ysize()); + for (size_t y = 0; y < distmap.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT row_distmap = distmap.ConstRow(y); + uint8_t* BUTTERAUGLI_RESTRICT row_h0 = heatmap.PlaneRow(0, y); + uint8_t* BUTTERAUGLI_RESTRICT row_h1 = heatmap.PlaneRow(1, y); + uint8_t* BUTTERAUGLI_RESTRICT row_h2 = heatmap.PlaneRow(2, y); + for (size_t x = 0; x < distmap.xsize(); ++x) { + const float d = row_distmap[x]; + uint8_t rgb[3]; + ScoreToRgb(d, good_threshold, bad_threshold, rgb); + row_h0[x] = rgb[0]; + row_h1[x] = rgb[1]; + row_h2[x] = rgb[2]; + } + } + return heatmap; +} + +} // namespace butteraugli +} // namespace jxl diff --git a/jxl/butteraugli/butteraugli.h b/jxl/butteraugli/butteraugli.h new file mode 100644 index 000000000..78b86173d --- /dev/null +++ b/jxl/butteraugli/butteraugli.h @@ -0,0 +1,330 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) + +#ifndef JXL_BUTTERAUGLI_BUTTERAUGLI_H_ +#define JXL_BUTTERAUGLI_BUTTERAUGLI_H_ + +#include +#include +#include +#include + +#include +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/image.h" +#include "jxl/image_ops.h" + +#define BUTTERAUGLI_ENABLE_CHECKS 0 +#define BUTTERAUGLI_RESTRICT JXL_RESTRICT + +// This is the main interface to butteraugli image similarity +// analysis function. + +namespace jxl { +namespace butteraugli { + +// ButteraugliInterface defines the public interface for butteraugli. +// +// It calculates the difference between rgb0 and rgb1. +// +// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains +// the red image for c == 0, green for c == 1, blue for c == 2. Location index +// px is calculated as y * xsize + x. +// +// Value of pixels of images rgb0 and rgb1 need to be represented as raw +// intensity. Most image formats store gamma corrected intensity in pixel +// values. This gamma correction has to be removed, by applying the following +// function: +// butteraugli_val = 255.0 * pow(png_val / 255.0, gamma); +// A typical value of gamma is 2.2. It is usually stored in the image header. +// Take care not to confuse that value with its inverse. The gamma value should +// be always greater than one. +// Butteraugli does not work as intended if the caller does not perform +// gamma correction. +// +// hf_asymmetry is a multiplier for penalizing new HF artifacts more than +// blurring away features (1.0 -> neutral). +// +// diffmap will contain an image of the size xsize * ysize, containing +// localized differences for values px (indexed with the px the same as rgb0 +// and rgb1). diffvalue will give a global score of similarity. +// +// A diffvalue smaller than kButteraugliGood indicates that images can be +// observed as the same image. +// diffvalue larger than kButteraugliBad indicates that a difference between +// the images can be observed. +// A diffvalue between kButteraugliGood and kButteraugliBad indicates that +// a subtle difference can be observed between the images. +// +// Returns true on success. + +bool ButteraugliInterface(const Image3F &rgb0, + const Image3F &rgb1, + float hf_asymmetry, + ImageF &diffmap, + double &diffvalue); + +// Converts the butteraugli score into fuzzy class values that are continuous +// at the class boundary. The class boundary location is based on human +// raters, but the slope is arbitrary. Particularly, it does not reflect +// the expectation value of probabilities of the human raters. It is just +// expected that a smoother class boundary will allow for higher-level +// optimization algorithms to work faster. +// +// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the +// scoring is fuzzy, a butteraugli score of 0.96 would return a class of +// around 1.9. +double ButteraugliFuzzyClass(double score); + +// Input values should be in range 0 (bad) to 2 (good). Use +// kButteraugliNormalization as normalization. +double ButteraugliFuzzyInverse(double seek); + +// Implementation details, don't use anything below or your code will +// break in the future. + +#ifdef _MSC_VER +#define BUTTERAUGLI_INLINE __forceinline +#else +#define BUTTERAUGLI_INLINE inline +#endif + +#ifdef __clang__ +// Early versions of Clang did not support __builtin_assume_aligned. +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) +#elif defined(__GNUC__) +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1 +#else +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0 +#endif + +// Returns a void* pointer which the compiler then assumes is N-byte aligned. +// Example: float* JXL_RESTRICT aligned = (float*)JXL_ASSUME_ALIGNED(in, 32); +// +// The assignment semantics are required by GCC/Clang. ICC provides an in-place +// __assume_aligned, whereas MSVC's __assume appears unsuitable. +#if BUTTERAUGLI_HAS_ASSUME_ALIGNED +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) +#else +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr) +#endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED + +struct MaskImage { + MaskImage() {} + MaskImage(int xs, int ys) : + mask_x(xs, ys), + mask_yb(xs, ys) {} + ImageF mask_x; + ImageF mask_yb; +}; + +struct PsychoImage { + ImageF uhf[2]; // XY + ImageF hf[2]; // XY + Image3F mf; // XYB + Image3F lf; // XYB +}; + +class ButteraugliComparator { + public: + ButteraugliComparator(const Image3F &rgb0, double hf_asymmetry); + virtual ~ButteraugliComparator(); + + // Computes the butteraugli map between the original image given in the + // constructor and the distorted image give here. + void Diffmap(const Image3F &rgb1, ImageF &result) const; + + // Same as above, but OpsinDynamicsImage() was already applied. + void DiffmapOpsinDynamicsImage(const Image3F &xyb1, ImageF &result) const; + + // Same as above, but the frequency decomposition was already applied. + void DiffmapPsychoImage(const PsychoImage& ps1, ImageF &result) const; + +void Mask(MaskImage *BUTTERAUGLI_RESTRICT mask, MaskImage *BUTTERAUGLI_RESTRICT mask_dc) const; + + private: + void MaltaDiffMapLF(const ImageF &y0, const ImageF &y1, double w_0gt1, + double w_0lt1, double normalization, + Image3F *BUTTERAUGLI_RESTRICT block_diff_ac, + size_t c) const; + + void MaltaDiffMap(const ImageF &y0, const ImageF &y1, double w_0gt1, + double w_0lt1, double normalization, + Image3F *BUTTERAUGLI_RESTRICT block_diff_ac, + size_t c) const; + + ImageF CombineChannels(const MaskImage &mask_ac, + const MaskImage &mask_dc, + const Image3F &block_diff_dc, + const Image3F &block_diff_ac) const; + + const size_t xsize_; + const size_t ysize_; + float hf_asymmetry_; + PsychoImage pi0_; + ButteraugliComparator *sub_; +}; + +bool ButteraugliDiffmap(const Image3F &rgb0, const Image3F &rgb1, + double hf_asymmetry, ImageF &diffmap); + +double ButteraugliScoreFromDiffmap(const ImageF& distmap); + +// Generate rgb-representation of the distance between two images. +Image3B CreateHeatMapImage(const ImageF &distmap, double good_threshold, + double bad_threshold); + +// Compute values of local frequency and dc masking based on the activity +// in the two images. +void Mask(const Image3F &xyb0, const Image3F &xyb1, + MaskImage *BUTTERAUGLI_RESTRICT mask, + MaskImage *BUTTERAUGLI_RESTRICT mask_dc, ImageF *diff_ac = nullptr); + +template +BUTTERAUGLI_INLINE void RgbToXyb(const V &r, const V &g, const V &b, + V *JXL_RESTRICT valx, V *JXL_RESTRICT valy, + V *JXL_RESTRICT valb) { + *valx = r - g; + *valy = r + g; + *valb = b; +} + +template +BUTTERAUGLI_INLINE void OpsinAbsorbance(const V &in0, const V &in1, + const V &in2, V *JXL_RESTRICT out0, + V *JXL_RESTRICT out1, + V *JXL_RESTRICT out2) { + // https://en.wikipedia.org/wiki/Photopsin absorbance modeling. + static const double mixi0 = 0.29956550340058319; + static const double mixi1 = 0.63373087833825936; + static const double mixi2 = 0.077705617820981968; + static const double mixi3 = 1.7557483643287353; + static const double mixi4 = 0.22158691104574774; + static const double mixi5 = 0.69391388044116142; + static const double mixi6 = 0.0987313588422; + static const double mixi7 = 1.7557483643287353; + static const double mixi8 = 0.02; + static const double mixi9 = 0.02; + static const double mixi10 = 0.20480129041026129; + static const double mixi11 = 12.226454707163354; + + const V mix0(mixi0); + const V mix1(mixi1); + const V mix2(mixi2); + const V mix3(mixi3); + const V mix4(mixi4); + const V mix5(mixi5); + const V mix6(mixi6); + const V mix7(mixi7); + const V mix8(mixi8); + const V mix9(mixi9); + const V mix10(mixi10); + const V mix11(mixi11); + + *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; + *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; + *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; +} + +Image3F OpsinDynamicsImage(const Image3F &rgb); + +ImageF Blur(const ImageF& in, float sigma, float border_ratio); + +double SimpleGamma(double v); + +double GammaMinArg(); +double GammaMaxArg(); + +// Polynomial evaluation via Clenshaw's scheme (similar to Horner's). +// Template enables compile-time unrolling of the recursion, but must reside +// outside of a class due to the specialization. +template +static inline void ClenshawRecursion(const double x, const double *coefficients, + double *b1, double *b2) { + const double x_b1 = x * (*b1); + const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX]; + *b2 = *b1; + *b1 = t; + + ClenshawRecursion(x, coefficients, b1, b2); +} + +// Base case +template <> +inline void ClenshawRecursion<0>( + const double x, const double *coefficients, double *b1, + double *b2) { // NOLINT(readability-non-const-parameter) + const double x_b1 = x * (*b1); + // The final iteration differs - no 2 * x_b1 here. + *b1 = x_b1 - (*b2) + coefficients[0]; +} + +// Rational polynomial := dividing two polynomial evaluations. These are easier +// to find than minimax polynomials. +struct RationalPolynomial { + template + static double EvaluatePolynomial(const double x, + const double (&coefficients)[N]) { + double b1 = 0.0; + double b2 = 0.0; + ClenshawRecursion(x, coefficients, &b1, &b2); + return b1; + } + + // Evaluates the polynomial at x (in [min_value, max_value]). + inline double operator()(const double x) const { + // First normalize to [0, 1]. + const double x01 = (x - min_value) / (max_value - min_value); + // And then to [-1, 1] domain of Chebyshev polynomials. + const double xc = 2.0 * x01 - 1.0; + + const double yp = EvaluatePolynomial(xc, p); + const double yq = EvaluatePolynomial(xc, q); + if (yq == 0.0) return 0.0; + return static_cast(yp / yq); + } + + // Domain of the polynomials; they are undefined elsewhere. + double min_value; + double max_value; + + // Coefficients of T_n (Chebyshev polynomials of the first kind). + // Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability. + double p[5 + 1]; + double q[5 + 1]; +}; + +static inline double GammaPolynomial(double value) { + static const RationalPolynomial r = { + 0.971783, 590.188894, + { + 98.7821300963361, 164.273222212631, 92.948112871376, + 33.8165311212688, 6.91626704983562, 0.556380877028234 + }, + { + 1, 1.64339473427892, 0.89392405219969, 0.298947051776379, + 0.0507146002577288, 0.00226495093949756 + }}; + return r(value); +} + +} // namespace butteraugli +} // namespace jxl + +#endif // JXL_BUTTERAUGLI_BUTTERAUGLI_H_ diff --git a/jxl/byte_order_test.cc b/jxl/byte_order_test.cc new file mode 100644 index 000000000..84a8805af --- /dev/null +++ b/jxl/byte_order_test.cc @@ -0,0 +1,62 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/base/byte_order.h" + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(ByteOrderTest, TestRoundTripBE16) { + const uint32_t in = 0x1234; + uint8_t buf[2]; + StoreBE16(in, buf); + EXPECT_EQ(in, LoadBE16(buf)); + EXPECT_NE(in, LoadLE16(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE16) { + const uint32_t in = 0x1234; + uint8_t buf[2]; + StoreLE16(in, buf); + EXPECT_EQ(in, LoadLE16(buf)); + EXPECT_NE(in, LoadBE16(buf)); +} + +TEST(ByteOrderTest, TestRoundTripBE32) { + const uint32_t in = 0xFEDCBA98u; + uint8_t buf[4]; + StoreBE32(in, buf); + EXPECT_EQ(in, LoadBE32(buf)); + EXPECT_NE(in, LoadLE32(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE32) { + const uint32_t in = 0xFEDCBA98u; + uint8_t buf[4]; + StoreLE32(in, buf); + EXPECT_EQ(in, LoadLE32(buf)); + EXPECT_NE(in, LoadBE32(buf)); +} + +TEST(ByteOrderTest, TestRoundTripLE64) { + const uint64_t in = 0xFEDCBA9876543210ull; + uint8_t buf[8]; + StoreLE64(in, buf); + EXPECT_EQ(in, LoadLE64(buf)); +} + +} // namespace +} // namespace jxl diff --git a/jxl/chroma_from_luma.cc b/jxl/chroma_from_luma.cc new file mode 100644 index 000000000..04217e2fd --- /dev/null +++ b/jxl/chroma_from_luma.cc @@ -0,0 +1,350 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/chroma_from_luma.h" + +#include + +#include +#include +#include + +#include "jxl/aux_out.h" +#include "jxl/base/bits.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/profiler.h" +#include "jxl/base/span.h" +#include "jxl/base/status.h" +#include "jxl/common.h" +#include "jxl/dct_util.h" +#include "jxl/entropy_coder.h" +#include "jxl/image_ops.h" +#include "jxl/modular/encoding/encoding.h" +#include "jxl/predictor.h" +#include "jxl/quantizer.h" + +namespace jxl { + +namespace { + +template +void FindBestCorrelation(const Image3F& dct, ImageB* JXL_RESTRICT map, + int* JXL_RESTRICT dc, float base, + const DequantMatrices& dequant, ThreadPool* pool) { + constexpr float kScale = SCALE; + constexpr float kZeroThresh = + kScale * kZeroBiasDefault[SIDE_CHANNEL] * + 0.9999f; // just epsilon less for better rounding + const float* const JXL_RESTRICT qm = + dequant.InvMatrix(AcStrategy::Type::DCT, SIDE_CHANNEL); + std::vector d_num_zeros_thread; + auto process_row_init = [&d_num_zeros_thread](size_t num_threads) { + d_num_zeros_thread.resize(256 * num_threads); + return true; + }; + auto process_row = [&](int ty, int thread) { + uint8_t* JXL_RESTRICT row_out = map->Row(ty); + for (size_t tx = 0; tx < map->xsize(); ++tx) { + const size_t y0 = ty * kColorTileDimInBlocks; + const size_t x0 = tx * kColorTileDimInBlocks * kDCTBlockSize; + const size_t y1 = + std::min(y0 + kColorTileDimInBlocks, dct.ysize()); + const size_t x1 = std::min( + x0 + kColorTileDimInBlocks * kDCTBlockSize, dct.xsize()); + int32_t d_num_zeros[257] = {0}; + for (size_t y = y0; y < y1; ++y) { + const float* JXL_RESTRICT row_m = dct.ConstPlaneRow(MAIN_CHANNEL, y); + const float* JXL_RESTRICT row_s = dct.ConstPlaneRow(SIDE_CHANNEL, y); + for (size_t x = x0; x < x1; ++x) { + const float scaled_m = row_m[x] * qm[x % kDCTBlockSize]; + const float scaled_s = kScale * row_s[x] * qm[x % kDCTBlockSize] + + (OFFSET - base * kScale) * scaled_m; + // Increment num_zeros[idx] if + // std::abs(scaled_s - (idx - OFFSET) * + // scaled_m) < kZeroThresh + if (std::abs(scaled_m) > 1e-8f) { + float from; + float to; + if (scaled_m > 0) { + from = (scaled_s - kZeroThresh) / scaled_m; + to = (scaled_s + kZeroThresh) / scaled_m; + } else { + from = (scaled_s + kZeroThresh) / scaled_m; + to = (scaled_s - kZeroThresh) / scaled_m; + } + if (from < 0.0f) { + from = 0.0f; + } + if (to > 255.0f) { + to = 255.0f; + } + // Instead of clamping the both values + // we just check that range is sane. + if (from <= to) { + d_num_zeros_thread[256 * thread + + static_cast(std::ceil(from))]++; + if (to < 255.f) + d_num_zeros_thread[256 * thread + + static_cast(std::floor(to + 1))]--; + if (x % kDCTBlockSize == 0) continue; + d_num_zeros[static_cast(std::ceil(from))]++; + d_num_zeros[static_cast(std::floor(to + 1))]--; + } + } + } + } + int best = 0; + int32_t best_sum = 0; + FindIndexOfSumMaximum(d_num_zeros, 256, &best, &best_sum); + row_out[tx] = best; + } + }; + + RunOnPool(pool, 0, map->ysize(), process_row_init, process_row, + "FindCorrelation"); + + size_t num_threads = d_num_zeros_thread.size() / 256; + int32_t d_num_zeros_global[256] = {0}; + for (size_t t = 0; t < num_threads; t++) { + for (size_t i = 0; i < 256; i++) { + d_num_zeros_global[i] += d_num_zeros_thread[t * 256 + i]; + } + } + int global_best = 0; + int32_t global_sum = 0; + FindIndexOfSumMaximum(d_num_zeros_global, 256, &global_best, &global_sum); + *dc = global_best; +} + +} // namespace + +ColorCorrelationMap::ColorCorrelationMap(size_t xsize, size_t ysize, bool XYB) + : ytox_map(DivCeil(xsize, kColorTileDim), DivCeil(ysize, kColorTileDim)), + ytob_map(DivCeil(xsize, kColorTileDim), DivCeil(ysize, kColorTileDim)) { + FillImage(kColorOffset, &ytox_map); + FillImage(kColorOffset, &ytob_map); + if (!XYB) { + base_correlation_b_ = 0; + } + RecomputeDCFactors(); +} + +void FindBestColorCorrelationMap(const Image3F& opsin, + const DequantMatrices& dequant, + ThreadPool* pool, ColorCorrelationMap* cmap) { + PROFILER_ZONE("enc YTo* correlation"); + + const size_t xsize_blocks = opsin.xsize() / kBlockDim; + const size_t ysize_blocks = opsin.ysize() / kBlockDim; + Image3F dct(xsize_blocks * kDCTBlockSize, ysize_blocks); + TransposedScaledDCT(opsin, &dct); + + int32_t ytob_dc = kColorOffset; + int32_t ytox_dc = kColorOffset; + + FindBestCorrelation(dct, &cmap->ytob_map, &ytob_dc, + cmap->YtoBRatio(kColorOffset), dequant, + pool); + FindBestCorrelation(dct, &cmap->ytox_map, &ytox_dc, + cmap->YtoXRatio(kColorOffset), dequant, + pool); + cmap->SetYToBDC(ytob_dc); + cmap->SetYToXDC(ytox_dc); +} + +class ColorCorrelationMapCoder { + public: + // TODO(veluca): change predictors? + template + using Predictor = predictor::ComputeResiduals< + T, predictor::PackSignedRange<0, 255>, + predictor::Predictors2>; + + enum { + kNumResidualContexts = 8, + kContextsPerChannel = kNumResidualContexts + 4, + kNumContexts = 2 * kContextsPerChannel, + }; + + static int Context(size_t c, size_t correct, size_t badness) { + if (correct == 0) { + JXL_ASSERT(badness != 0); + badness = (badness + 1) >> 1; + size_t badness_offset = + std::min(badness, kNumResidualContexts) - 1; + return kContextsPerChannel * c + badness_offset; + } + return kContextsPerChannel * c + kNumResidualContexts + + CeilLog2Nonzero(9 - correct); + } + static_assert(kNumContexts == kCmapContexts, + "Invalid number of cmap contexts"); + + struct Decoder : public Predictor { + BitReader* JXL_RESTRICT br; + ANSSymbolReader* decoder; + const std::vector* context_map; + uint8_t* JXL_RESTRICT rows[2]; + size_t stride; + size_t base_context; + AuxOut* JXL_RESTRICT aux_out; + + Decoder(BitReader* JXL_RESTRICT br, ANSSymbolReader* decoder, + const std::vector* context_map, + uint8_t* JXL_RESTRICT rows[2], size_t stride, size_t base_context, + AuxOut* JXL_RESTRICT aux_out) + : br(br), + decoder(decoder), + context_map(context_map), + rows{rows[0], rows[1]}, + stride(stride), + base_context(base_context), + aux_out(aux_out) {} + + void Decode(size_t xsize, size_t ysize) { + Predictor::Run(xsize, ysize, aux_out); + } + + HWY_ATTR JXL_INLINE void Prediction( + size_t x, size_t y, const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT num_correct, + const uint32_t* JXL_RESTRICT min_error, int32_t* JXL_RESTRICT decoded) { + for (size_t c = 0; c < 2; c++) { + size_t ctx = base_context + Context(c, num_correct[c], min_error[c]); + + uint32_t residual = ReadHybridUint(ctx, br, decoder, *context_map); + + if (x == 0 && y == 0) { + decoded[c] = kColorOffset + UnpackSigned(residual); + } else { + decoded[c] = predictions[c] + UnpackSigned(residual); + } + + rows[c][y * stride + x] = decoded[c]; + } + } + }; + + struct Encoder : public Predictor { + std::vector* JXL_RESTRICT tokens; + const uint8_t* JXL_RESTRICT rows[2]; + size_t stride; + size_t base_context; + AuxOut* JXL_RESTRICT aux_out; + + Encoder(std::vector* JXL_RESTRICT tokens, + const uint8_t* JXL_RESTRICT rows[2], size_t stride, + size_t base_context, AuxOut* JXL_RESTRICT aux_out) + : tokens(tokens), + rows{rows[0], rows[1]}, + stride(stride), + base_context(base_context), + aux_out(aux_out) {} + + void Encode(size_t xsize, size_t ysize) { + Predictor::Run(xsize, ysize, aux_out); + } + + HWY_ATTR JXL_INLINE void Prediction( + size_t x, size_t y, const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT num_correct, + const uint32_t* JXL_RESTRICT min_error, int32_t* JXL_RESTRICT decoded) { + for (size_t c = 0; c < 2; c++) { + size_t ctx = base_context + Context(c, num_correct[c], min_error[c]); + + decoded[c] = rows[c][y * stride + x]; + + uint32_t residual; + if (x == 0 && y == 0) { + residual = PackSigned(decoded[c] - kColorOffset); + } else { + residual = PackSigned(decoded[c] - predictions[c]); + } + + TokenizeHybridUint(ctx, residual, tokens); + } + } + }; +}; + +Status DecodeColorMap(BitReader* JXL_RESTRICT br, ANSSymbolReader* decoder, + const std::vector& context_map, + ColorCorrelationMap* cmap, const Rect& rect, + size_t base_context, AuxOut* JXL_RESTRICT aux_out) { + uint8_t* JXL_RESTRICT rows[2] = { + rect.Row(&cmap->ytox_map, 0), + rect.Row(&cmap->ytob_map, 0), + }; + const size_t stride = cmap->ytob_map.PixelsPerRow(); + + ColorCorrelationMapCoder::Decoder(br, decoder, &context_map, rows, stride, + base_context, aux_out) + .Decode(rect.xsize(), rect.ysize()); + return true; +} + +bool DecodeFullColorMap(BitReader* JXL_RESTRICT br, ColorCorrelationMap* cmap, + bool use_new_cmap) { + if (use_new_cmap) { + if (!br->JumpToByteBoundary()) return false; + const Span span = br->GetSpan(); + Rect rect0(cmap->ytob_map); + size_t pos = 0; + if (!modular_rect_decompress_2(span, &pos, &cmap->ytox_map, &cmap->ytob_map, + rect0, kColorOffset, kColorOffset)) + return JXL_FAILURE("Failed to decode color map"); + br->SkipBits(8 * pos); + } else { + // no longer exists + } + return true; +} + +void EncodeColorMap(const ColorCorrelationMap& cmap, const Rect& rect, + std::vector* tokens, size_t base_context, + AuxOut* JXL_RESTRICT aux_out) { + const uint8_t* JXL_RESTRICT rows[2] = { + rect.ConstRow(cmap.ytox_map, 0), + rect.ConstRow(cmap.ytob_map, 0), + }; + const size_t stride = cmap.ytob_map.PixelsPerRow(); + ColorCorrelationMapCoder::Encoder(tokens, rows, stride, base_context, aux_out) + .Encode(rect.xsize(), rect.ysize()); +} + +void EncodeFullColorMap(const ColorCorrelationMap& cmap, const Rect& rect, + BitWriter* writer, size_t layer, AuxOut* aux_out, + bool use_new_cmap) { + if (use_new_cmap) { + PaddedBytes enc_co; + modular_options cfopts; + set_default_modular_options(cfopts); + cfopts.nb_repeats = 1; + modular_rect_compress_2(cmap.ytox_map, cmap.ytob_map, rect, &enc_co, + &cfopts, kColorOffset, kColorOffset); + + if (aux_out != nullptr) + aux_out->layers[layer].total_bits += enc_co.size() * 8; + + writer->ZeroPadToByte(); + *writer += enc_co; + + } else { + // no longer exists + } +} + +} // namespace jxl diff --git a/jxl/chroma_from_luma.h b/jxl/chroma_from_luma.h new file mode 100644 index 000000000..50f4f8b0c --- /dev/null +++ b/jxl/chroma_from_luma.h @@ -0,0 +1,189 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_CHROMA_FROM_LUMA_H_ +#define JXL_CHROMA_FROM_LUMA_H_ + +// Chroma-from-luma, computed using heuristics to determine the best linear +// model for the X and B channels from the Y channel. + +#include +#include + +#include +#include + +#include "jxl/aux_out.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/status.h" +#include "jxl/common.h" +#include "jxl/dec_ans.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/enc_ans.h" +#include "jxl/enc_bit_writer.h" +#include "jxl/entropy_coder.h" +#include "jxl/field_encodings.h" +#include "jxl/fields.h" +#include "jxl/image.h" +#include "jxl/opsin_params.h" +#include "jxl/quant_weights.h" + +namespace jxl { + +// Tile is the rectangular grid of blocks that share color correlation +// parameters ("factor_x/b" such that residual_b = blue - Y * factor_b). +static constexpr size_t kColorTileDim = 64; + +static_assert(kColorTileDim % kBlockDim == 0, + "Color tile dim should be divisible by block dim"); +static constexpr size_t kColorTileDimInBlocks = kColorTileDim / kBlockDim; + +static_assert(kGroupDimInBlocks % kColorTileDimInBlocks == 0, + "Group dim should be divisible by color tile dim"); + +static constexpr const uint8_t kColorOffset = 127; +static constexpr const uint8_t kDefaultColorFactor = 84; + +static constexpr U32Enc kColorFactorDist(Val(kDefaultColorFactor), Val(256), + BitsOffset(2, 8), BitsOffset(258, 12)); + +struct ColorCorrelationMap { + ColorCorrelationMap() = default; + // xsize/ysize are in pixels + // set XYB=false to do something close to no-op cmap (needed for now since + // cmap is mandatory) + ColorCorrelationMap(size_t xsize, size_t ysize, bool XYB = true); + + float YtoXRatio(int32_t x_factor) const { + return base_correlation_x_ + (x_factor - kColorOffset) * color_scale_; + } + + float YtoBRatio(int32_t b_factor) const { + return base_correlation_b_ + (b_factor - kColorOffset) * color_scale_; + } + + void EncodeDC(BitWriter* writer, size_t layer, AuxOut* aux_out) const { + BitWriter::Allotment allotment(writer, 1 + 2 * kBitsPerByte + 12 + 32); + if (ytox_dc_ == kColorOffset && ytob_dc_ == kColorOffset && + color_factor_ == kDefaultColorFactor && base_correlation_x_ == 0.0f && + base_correlation_b_ == kYToBRatio) { + writer->Write(1, 1); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + return; + } + writer->Write(1, 0); + JXL_CHECK(U32Coder::Write(kColorFactorDist, color_factor_, writer)); + JXL_CHECK(F16Coder::Write(base_correlation_x_, writer)); + JXL_CHECK(F16Coder::Write(base_correlation_b_, writer)); + writer->Write(kBitsPerByte, ytox_dc_); + writer->Write(kBitsPerByte, ytob_dc_); + ReclaimAndCharge(writer, &allotment, layer, aux_out); + } + + HWY_ATTR Status DecodeDC(BitReader* br) { + if (br->ReadFixedBits<1>() == 1) { + // All default. + return true; + } + SetColorFactor(U32Coder::Read(kColorFactorDist, br)); + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &base_correlation_x_)); + JXL_RETURN_IF_ERROR(F16Coder::Read(br, &base_correlation_b_)); + ytox_dc_ = br->ReadFixedBits(); + ytob_dc_ = br->ReadFixedBits(); + RecomputeDCFactors(); + return true; + } + + void SetColorFactor(uint32_t factor) { + color_factor_ = factor; + color_scale_ = 1.0f / color_factor_; + RecomputeDCFactors(); + } + + void SetYToBDC(int32_t ytob_dc) { + ytob_dc_ = ytob_dc; + RecomputeDCFactors(); + } + void SetYToXDC(int32_t ytox_dc) { + ytox_dc_ = ytox_dc; + RecomputeDCFactors(); + } + + const float* DCFactors() const { return dc_factors_; } + + void RecomputeDCFactors() { + dc_factors_[0] = YtoXRatio(ytox_dc_); + dc_factors_[2] = YtoBRatio(ytob_dc_); + } + + ImageB ytox_map; + ImageB ytob_map; + + private: + // range of factor: -1.51 to +1.52 + uint32_t color_factor_ = kDefaultColorFactor; + float color_scale_ = 1.0f / color_factor_; + float base_correlation_x_ = 0.0f; + float base_correlation_b_ = kYToBRatio; + int32_t ytox_dc_ = kColorOffset; + int32_t ytob_dc_ = kColorOffset; + HWY_ALIGN float dc_factors_[4] = {}; +}; + +void FindBestColorCorrelationMap(const Image3F& opsin, + const DequantMatrices& dequant, + ThreadPool* pool, ColorCorrelationMap* cmap); + +void EncodeColorMap(const ColorCorrelationMap& cmap, const Rect& rect, + std::vector* tokens, size_t base_context, + AuxOut* JXL_RESTRICT aux_out); + +Status DecodeColorMap(BitReader* JXL_RESTRICT br, ANSSymbolReader* decoder, + const std::vector& context_map, + ColorCorrelationMap* cmap, const Rect& rect, + size_t base_context, AuxOut* JXL_RESTRICT aux_out); + +// Declared here to avoid including predictor.h. +static constexpr size_t kCmapContexts = 24; + +void EncodeFullColorMap(const ColorCorrelationMap& cmap, const Rect& rect, + BitWriter* writer, size_t layer, AuxOut* aux_out, + bool use_new_cmap); + +bool DecodeFullColorMap(BitReader* JXL_RESTRICT br, ColorCorrelationMap* cmap, + bool use_new_cmap); + +template +inline void FindIndexOfSumMaximum(const V* array, const size_t len, R* idx, + V* sum) { + JXL_ASSERT(len > 0); + V maxval = 0; + V val = 0; + R maxidx = 0; + for (size_t i = 0; i < len; ++i) { + val += array[i]; + if (val > maxval) { + maxval = val; + maxidx = i; + } + } + *idx = maxidx; + *sum = maxval; +} + +} // namespace jxl + +#endif // JXL_CHROMA_FROM_LUMA_H_ diff --git a/jxl/codec_in_out.h b/jxl/codec_in_out.h new file mode 100644 index 000000000..87433f16f --- /dev/null +++ b/jxl/codec_in_out.h @@ -0,0 +1,212 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_CODEC_IN_OUT_H_ +#define JXL_CODEC_IN_OUT_H_ + +// Holds inputs/outputs for decoding/encoding images. + +#include + +#include +#include + +#include "jxl/base/data_parallel.h" +#include "jxl/common.h" +#include "jxl/frame_header.h" +#include "jxl/headers.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" + +namespace jxl { + +// Per-channel interval, used to convert between (full-range) external and +// (bounded or unbounded) temp values. See external_image.cc for the definitions +// of temp/external. +struct CodecInterval { + CodecInterval() = default; + constexpr CodecInterval(float min, float max) : min(min), width(max - min) {} + // Defaults for temp. + float min = 0.0f; + float width = 1.0f; +}; + +using CodecIntervals = std::array; // RGB[A] or Y[A] + +// Allows passing arbitrary metadata to decoders (required for PNM). +class DecoderHints { + public: + // key=color_space, value=Description(c/pp): specify the ColorEncoding of + // the pixels for decoding. Otherwise, if the codec did not obtain an ICC + // profile from the image, assume sRGB. + // + // Strings are taken from the command line, so avoid spaces for convenience. + void Add(const std::string& key, const std::string& value) { + kv_.emplace_back(key, value); + } + + // Calls func(key, value) in order of Add. + template + void Foreach(const Func& func) const { + for (const KeyValue& kv : kv_) { + func(kv.key, kv.value); + } + } + + private: + // Splitting into key/value avoids parsing in each codec. + struct KeyValue { + KeyValue(std::string key, std::string value) + : key(std::move(key)), value(std::move(value)) {} + + std::string key; + std::string value; + }; + + std::vector kv_; +}; + +// Optional text/EXIF metadata. +struct Blobs { + PaddedBytes exif; + PaddedBytes iptc; + PaddedBytes jumbf; + PaddedBytes xmp; +}; + +// Holds a preview, a main image or one or more frames, plus the inputs/outputs +// to/from decoding/encoding. +class CodecInOut { + public: + CodecInOut() : preview_frame(&metadata) { + frames.reserve(1); + frames.emplace_back(&metadata); + } + + // Move-only. + CodecInOut(CodecInOut&&) = default; + CodecInOut& operator=(CodecInOut&&) = default; + + ImageBundle& Main() { + JXL_DASSERT(frames.size() == 1); + return frames[0]; + } + const ImageBundle& Main() const { + JXL_DASSERT(frames.size() == 1); + return frames[0]; + } + + // If c_current.IsGray(), all planes must be identical. + void SetFromImage(Image3F&& color, const ColorEncoding& c_current) { + Main().SetFromImage(std::move(color), c_current); + } + + void CheckMetadata() const { + JXL_CHECK(metadata.bits_per_sample != 0); + JXL_CHECK(!metadata.color_encoding.icc.empty()); + + if (preview_frame.xsize() != 0) preview_frame.VerifyMetadata(); + JXL_CHECK(preview_frame.metadata() == &metadata); + + for (const ImageBundle& ib : frames) { + ib.VerifyMetadata(); + JXL_CHECK(ib.metadata() == &metadata); + } + } + + size_t xsize() const { return Main().xsize(); } + size_t ysize() const { return Main().ysize(); } + void ShrinkTo(size_t xsize, size_t ysize) { + // preview is unaffected. + for (ImageBundle& ib : frames) { + ib.ShrinkTo(xsize, ysize); + } + } + + template + Status VerifyDimensions(T xs, T ys) const { + if (xs == 0 || ys == 0) return JXL_FAILURE("Empty image."); + if (xs > dec_max_xsize) return JXL_FAILURE("Image too wide."); + if (ys > dec_max_ysize) return JXL_FAILURE("Image too tall."); + + const uint64_t num_pixels = uint64_t(xs) * ys; + if (num_pixels > dec_max_pixels) return JXL_FAILURE("Image too big."); + + return true; + } + + // Calls TransformTo for each ImageBundle (preview/frames). + Status TransformTo(const ColorEncoding& c_desired, + ThreadPool* pool = nullptr) { + if (metadata.m2.have_preview) { + JXL_RETURN_IF_ERROR(preview_frame.TransformTo(c_desired, pool)); + } + for (ImageBundle& ib : frames) { + JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, pool)); + } + return true; + } + + // -- ENCODER OUTPUT: + + // Size [bytes] of encoded bitstream after encoding / before decoding. + mutable size_t enc_size; + + // Encoder-specific function of its bits_per_sample argument. Used to compute + // error tolerance in round trips. + mutable size_t enc_bits_per_sample; + + // -- DECODER INPUT: + + // Upper limit on pixel dimensions/area, enforced by DimensionsWithinLimits + // (called from decoders). Fuzzers set smaller values to limit memory use. + uint32_t dec_max_xsize = 0xFFFFFFFFu; + uint32_t dec_max_ysize = 0xFFFFFFFFu; + uint64_t dec_max_pixels = ~0ull; + + // Used to set c_current for codecs that lack color space metadata. + DecoderHints dec_hints; + + // -- DECODER OUTPUT + + // Total number of pixels decoded (may differ from #frames * xsize * ysize + // if frames are cropped) + uint64_t dec_pixels = 0; + + // -- DECODER OUTPUT, ENCODER INPUT: + + // Metadata stored into / retrieved from bitstreams. + + Blobs blobs; + ImageMetadata metadata; // applies to preview and all frames + + // If metadata.have_preview: + PreviewHeader preview; + ImageBundle preview_frame; + + // If metadata.have_animation: + AnimationHeader animation; + std::vector animation_frames; + + std::vector frames; // size=1 if !metadata.have_animation + + bool use_sjpeg = false; + // If the image should be written to a JPEG, use this quality for encoding. + // If the image was read from a JPEG, this represents the + size_t jpeg_quality; +}; + +} // namespace jxl + +#endif // JXL_CODEC_IN_OUT_H_ diff --git a/jxl/coeff_order.cc b/jxl/coeff_order.cc new file mode 100644 index 000000000..4ee86359d --- /dev/null +++ b/jxl/coeff_order.cc @@ -0,0 +1,347 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/coeff_order.h" + +#include + +#include +#include + +#include "jxl/ans_params.h" +#include "jxl/aux_out.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/profiler.h" +#include "jxl/base/span.h" +#include "jxl/coeff_order_fwd.h" +#include "jxl/dec_ans.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/enc_ans.h" +#include "jxl/enc_bit_writer.h" +#include "jxl/entropy_coder.h" +#include "jxl/lehmer_code.h" +#include "jxl/modular/encoding/encoding.h" +#include "jxl/modular/image/image.h" + +namespace jxl { +namespace { +void SetDefaultOrder(AcStrategy acs, coeff_order_t* JXL_RESTRICT order) { + PROFILER_FUNC; + const size_t size = + kDCTBlockSize * acs.covered_blocks_x() * acs.covered_blocks_y(); + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + for (size_t k = 0; k < size; ++k) { + order[k] = natural_coeff_order[k]; + } +} +} // namespace + +uint32_t ComputeUsedOrders(const SpeedTier speed, + const AcStrategyImage& ac_strategy, + const Rect& rect) { + // Use default orders for small images. + if (ac_strategy.xsize() < 5 && ac_strategy.ysize() < 5) return 0; + + // Only uses DCT8 = 0, so bitfield = 1. + if (speed == SpeedTier::kFalcon) return 1; + + uint32_t ret = 0; + size_t xsize_blocks = rect.xsize(); + size_t ysize_blocks = rect.ysize(); + // TODO(janwas): parallelize + for (size_t by = 0; by < ysize_blocks; ++by) { + AcStrategyRow acs_row = ac_strategy.ConstRow(rect, by); + for (size_t bx = 0; bx < xsize_blocks; ++bx) { + int ord = kStrategyOrder[acs_row[bx].RawStrategy()]; + ret |= 1u << ord; + } + } + return ret; +} + +void ComputeCoeffOrder(const ACImage3& acs, const AcStrategyImage& ac_strategy, + const FrameDimensions& frame_dim, uint32_t used_orders, + coeff_order_t* JXL_RESTRICT order) { + int32_t num_zeros[3 * AcStrategy::kNumValidStrategies * + AcStrategy::kMaxCoeffArea] = {}; + // No need to compute number of zero coefficients if all orders are the + // default. + if (used_orders != 0) { + // Count number of zero coefficients, separately for each DCT band. + for (size_t group_index = 0; group_index < frame_dim.num_groups; + group_index++) { + const size_t gx = group_index % frame_dim.xsize_groups; + const size_t gy = group_index / frame_dim.xsize_groups; + const Rect rect(gx * kGroupDimInBlocks, gy * kGroupDimInBlocks, + kGroupDimInBlocks, kGroupDimInBlocks, + frame_dim.xsize_blocks, frame_dim.ysize_blocks); + const ac_qcoeff_t* JXL_RESTRICT rows[3] = { + acs.ConstPlaneRow(0, group_index), + acs.ConstPlaneRow(1, group_index), + acs.ConstPlaneRow(2, group_index), + }; + size_t ac_offset = 0; + + for (size_t by = 0; by < rect.ysize(); ++by) { + AcStrategyRow acs_row = ac_strategy.ConstRow(rect, by); + for (size_t bx = 0; bx < rect.xsize(); ++bx) { + AcStrategy acs = acs_row[bx]; + if (!acs.IsFirstBlock()) continue; + size_t size = kDCTBlockSize << acs.log2_covered_blocks(); + for (size_t c = 0; c < 3; ++c) { + const size_t order_offset = + (kStrategyOrder[acs.RawStrategy()] * 3 + c) * + AcStrategy::kMaxCoeffArea; + for (size_t k = 0; k < size; k++) { + if (rows[c][ac_offset + k] == 0) { + num_zeros[order_offset + k]++; + } + } + // Ensure LLFs are first in the order. + size_t cx = acs.covered_blocks_x(); + size_t cy = acs.covered_blocks_y(); + CoefficientLayout(&cy, &cx); + for (size_t iy = 0; iy < cy; iy++) { + for (size_t ix = 0; ix < cx; ix++) { + num_zeros[order_offset + iy * kBlockDim * cx + ix] = -1; + } + } + } + ac_offset += size; + } + } + } + } + + uint16_t computed = 0; + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + // Ensure natural coefficient order is not permuted if the order is + // not transmitted. + if ((1 << ord) & ~used_orders) { + for (size_t c = 0; c < 3; c++) { + SetDefaultOrder(AcStrategy::FromRawStrategy(o), + &order[(3 * ord + c) * AcStrategy::kMaxCoeffArea]); + } + continue; + } + AcStrategy acs = AcStrategy::FromRawStrategy(o); + size_t sz = kDCTBlockSize * acs.covered_blocks_x() * acs.covered_blocks_y(); + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + + for (uint8_t c = 0; c < 3; c++) { + struct PosAndCount { + uint32_t pos; + uint32_t count; + }; + + // Apply zig-zag order. + PosAndCount pos_and_val[AcStrategy::kMaxCoeffArea]; + size_t offset = (ord * 3 + c) * AcStrategy::kMaxCoeffArea; + for (size_t i = 0; i < sz; ++i) { + size_t pos = natural_coeff_order[i]; + pos_and_val[i].pos = pos; + // We don't care for the exact number -> quantize number of zeros, + // to get less permuted order. + pos_and_val[i].count = num_zeros[offset + pos] / std::sqrt(sz) + 0.1; + } + + // Stable-sort -> elements with same number of zeros will preserve their + // order. + auto comparator = [](const PosAndCount& a, const PosAndCount& b) -> bool { + return a.count < b.count; + }; + std::stable_sort(pos_and_val, pos_and_val + sz, comparator); + + // Grab indices. + for (size_t i = 0; i < sz; ++i) { + order[(ord * 3 + c) * AcStrategy::kMaxCoeffArea + i] = + pos_and_val[i].pos; + } + } + } +} +namespace { +constexpr uint32_t kPermutationContexts = 8; +uint32_t Context(uint32_t val) { + uint32_t nbits, bits; + EncodeVarLenUint(val, &nbits, &bits); + return std::min(nbits, kPermutationContexts - 1); +} + +void TokenizePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, std::vector* tokens) { + std::vector lehmer(size); + std::vector temp(size + 1); + ComputeLehmerCode(order, temp.data(), size, lehmer.data()); + size_t end = size; + while (end > skip && lehmer[end - 1] == 0) { + --end; + } + uint32_t nbits, bits; + EncodeVarLenUint(end - skip, &nbits, &bits); + tokens->emplace_back(Context(size), nbits, nbits, bits); + uint32_t last = 0; + for (size_t i = skip; i < end; ++i) { + EncodeVarLenUint(lehmer[i], &nbits, &bits); + tokens->emplace_back(Context(last), nbits, nbits, bits); + last = lehmer[i]; + } +} + +HWY_ATTR void ReadPermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br, ANSSymbolReader* reader, + const std::vector& context_map) { + std::vector lehmer(size); + std::vector temp(size); + uint32_t nbits = reader->ReadSymbol(context_map[Context(size)], br); + uint32_t bits = br->ReadBits(nbits); + uint32_t end = DecodeVarLenUint(nbits, bits) + skip; + uint32_t last = 0; + for (size_t i = skip; i < end; ++i) { + br->Refill(); // covers ReadSymbolWithoutRefill + ReadBits + uint32_t nbits = + reader->ReadSymbolWithoutRefill(context_map[Context(last)], br); + uint32_t bits = br->ReadBits(nbits); + lehmer[i] = DecodeVarLenUint(nbits, bits); + last = lehmer[i]; + } + DecodeLehmerCode(lehmer.data(), temp.data(), size, order); +} + +} // namespace + +Status DecodePermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br) { + std::vector context_map; + ANSCode code; + JXL_RETURN_IF_ERROR(DecodeHistograms( + br, kPermutationContexts, ANS_MAX_ALPHA_SIZE, &code, &context_map)); + ANSSymbolReader reader(&code, br); + ReadPermutation(skip, size, order, br, &reader, context_map); + if (!reader.CheckANSFinalState()) { + return JXL_FAILURE("Invalid ANS stream"); + } + return true; +} + +void EncodePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, BitWriter* writer, int layer, + AuxOut* aux_out) { + std::vector tokens; + TokenizePermutation(order, skip, size, &tokens); + std::vector context_map; + EntropyEncodingData codes; + BuildAndEncodeHistograms(HistogramParams(), kPermutationContexts, {tokens}, + &codes, &context_map, writer, layer, aux_out); + WriteTokens(tokens, codes, context_map, writer, layer, aux_out); +} + +namespace { +void EncodeCoeffOrder(const coeff_order_t* JXL_RESTRICT order, AcStrategy acs, + std::vector* tokens) { + const size_t llf = acs.covered_blocks_x() * acs.covered_blocks_y(); + const size_t size = kDCTBlockSize * llf; + coeff_order_t order_zigzag[AcStrategy::kMaxCoeffArea]; + const coeff_order_t* natural_coeff_order_lut = acs.NaturalCoeffOrderLut(); + for (size_t i = 0; i < size; ++i) { + order_zigzag[i] = natural_coeff_order_lut[order[i]]; + } + TokenizePermutation(order_zigzag, llf, size, tokens); +} +} // namespace + +void EncodeCoeffOrders(uint16_t used_orders, const coeff_order_t* order, + BitWriter* writer, size_t layer, AuxOut* aux_out) { + uint16_t computed = 0; + std::vector tokens; + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + if ((used_orders & (1 << ord)) == 0) continue; + AcStrategy acs = AcStrategy::FromRawStrategy(o); + for (size_t c = 0; c < 3; c++) { + EncodeCoeffOrder(&order[(3 * ord + c) * AcStrategy::kMaxCoeffArea], acs, + &tokens); + } + } + // Do not write anything if no order is used. + if (used_orders != 0) { + std::vector context_map; + EntropyEncodingData codes; + BuildAndEncodeHistograms(HistogramParams(), kPermutationContexts, {tokens}, + &codes, &context_map, writer, layer, aux_out); + WriteTokens(tokens, codes, context_map, writer, layer, aux_out); + } +} + +namespace { + +Status DecodeCoeffOrder(AcStrategy acs, coeff_order_t* order, BitReader* br, + ANSSymbolReader* reader, + const std::vector& context_map) { + PROFILER_FUNC; + const size_t llf = acs.covered_blocks_x() * acs.covered_blocks_y(); + const size_t size = kDCTBlockSize * llf; + + ReadPermutation(llf, size, order, br, reader, context_map); + const coeff_order_t* natural_coeff_order = acs.NaturalCoeffOrder(); + for (size_t k = 0; k < size; ++k) { + order[k] = natural_coeff_order[order[k]]; + } + return true; +} + +} // namespace + +Status DecodeCoeffOrders(uint16_t used_orders, coeff_order_t* order, + BitReader* br) { + uint16_t computed = 0; + std::vector context_map; + ANSCode code; + std::unique_ptr reader; + // Bitstream does not have histograms if no coefficient order is used. + if (used_orders != 0) { + JXL_RETURN_IF_ERROR(DecodeHistograms( + br, kPermutationContexts, ANS_MAX_ALPHA_SIZE, &code, &context_map)); + reader = make_unique(&code, br); + } + for (uint8_t o = 0; o < AcStrategy::kNumValidStrategies; ++o) { + uint8_t ord = kStrategyOrder[o]; + if (computed & (1 << ord)) continue; + computed |= 1 << ord; + AcStrategy acs = AcStrategy::FromRawStrategy(o); + if ((used_orders & (1 << ord)) == 0) { + for (size_t c = 0; c < 3; c++) { + SetDefaultOrder(acs, &order[(3 * ord + c) * AcStrategy::kMaxCoeffArea]); + } + } else { + for (size_t c = 0; c < 3; c++) { + JXL_RETURN_IF_ERROR(DecodeCoeffOrder( + acs, &order[(3 * ord + c) * AcStrategy::kMaxCoeffArea], br, + reader.get(), context_map)); + } + } + } + if (used_orders && !reader->CheckANSFinalState()) { + return JXL_FAILURE("Invalid ANS stream"); + } + return true; +} + +} // namespace jxl diff --git a/jxl/coeff_order.h b/jxl/coeff_order.h new file mode 100644 index 000000000..5fa16d872 --- /dev/null +++ b/jxl/coeff_order.h @@ -0,0 +1,76 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COEFF_ORDER_H_ +#define JXL_COEFF_ORDER_H_ + +#include +#include + +#include "jxl/ac_strategy.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/status.h" +#include "jxl/coeff_order_fwd.h" +#include "jxl/common.h" +#include "jxl/dct_util.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/enc_bit_writer.h" +#include "jxl/enc_params.h" + +namespace jxl { + +static constexpr size_t kCoeffOrderSize = + 3 * kNumOrders * AcStrategy::kMaxCoeffArea; + +// Mapping from AC strategy to order bucket. Strategies with different natural +// orders must have different buckets. +constexpr uint8_t kStrategyOrder[AcStrategy::kNumValidStrategies] = { + 0, 1, 1, 1, 2, 3, 4, 4, 5, 5, 6, 6, 1, 1, 1, 1, 1, 1}; + +static_assert(AcStrategy::kNumValidStrategies == 18, + "Update this array when adding or removing AC strategies."); + +// Orders that are actually used in part of image. `rect` is in block units. +uint32_t ComputeUsedOrders(SpeedTier speed, const AcStrategyImage& ac_strategy, + const Rect& rect); + +// Modify zig-zag order, so that DCT bands with more zeros go later. +// Order of DCT bands with same number of zeros is untouched, so +// permutation will be cheaper to encode. +void ComputeCoeffOrder(const ACImage3& acs, const AcStrategyImage& ac_strategy, + const FrameDimensions& frame_dim, uint32_t used_orders, + coeff_order_t* JXL_RESTRICT order); + +void EncodeCoeffOrders(uint16_t used_orders, + const coeff_order_t* JXL_RESTRICT order, + BitWriter* writer, size_t layer, + AuxOut* JXL_RESTRICT aux_out); + +Status DecodeCoeffOrders(uint16_t used_orders, coeff_order_t* order, + BitReader* br); + +// Encoding/decoding of a single permutation. `size`: number of elements in the +// permutation. `skip`: number of elements to skip from the *beginning* of the +// permutation. +void EncodePermutation(const coeff_order_t* JXL_RESTRICT order, size_t skip, + size_t size, BitWriter* writer, int layer, + AuxOut* aux_out); + +Status DecodePermutation(size_t skip, size_t size, coeff_order_t* order, + BitReader* br); + +} // namespace jxl + +#endif // JXL_COEFF_ORDER_H_ diff --git a/jxl/coeff_order_fwd.h b/jxl/coeff_order_fwd.h new file mode 100644 index 000000000..176bcad27 --- /dev/null +++ b/jxl/coeff_order_fwd.h @@ -0,0 +1,56 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COEFF_ORDER_FWD_H_ +#define JXL_COEFF_ORDER_FWD_H_ + +// Breaks circular dependency between ac_strategy and coeff_order. + +#include +#include + +#include "base/compiler_specific.h" + +namespace jxl { + +// Needs at least 10 bits. A 32-bit type speeds up DecodeAC by 2% at the cost of +// more memory. +using coeff_order_t = uint32_t; + +// Maximum number of orders to be used. Note that this needs to be multiplied by +// the number of channels. One per "size class" (plus one extra for DCT8), +// shared between transforms of size XxY and of size YxX. +constexpr uint8_t kNumOrders = 7; + +// DCT coefficients are laid out in such a way that the number of rows of +// coefficients is always the smaller coordinate. +JXL_INLINE constexpr size_t CoefficientRows(size_t rows, size_t columns) { + return rows < columns ? rows : columns; +} + +JXL_INLINE constexpr size_t CoefficientColumns(size_t rows, size_t columns) { + return rows < columns ? columns : rows; +} + +JXL_INLINE void CoefficientLayout(size_t* JXL_RESTRICT rows, + size_t* JXL_RESTRICT columns) { + size_t r = *rows; + size_t c = *columns; + *rows = CoefficientRows(r, c); + *columns = CoefficientColumns(r, c); +} + +} // namespace jxl + +#endif // JXL_COEFF_ORDER_FWD_H_ diff --git a/jxl/coeff_order_test.cc b/jxl/coeff_order_test.cc new file mode 100644 index 000000000..8eff10d03 --- /dev/null +++ b/jxl/coeff_order_test.cc @@ -0,0 +1,110 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/coeff_order.h" + +#include + +#include +#include // iota +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/base/span.h" +#include "jxl/coeff_order_fwd.h" +#include "jxl/dec_bit_reader.h" + +namespace jxl { +namespace { + +size_t RoundtripPermutation(coeff_order_t* perm, coeff_order_t* out, + size_t len) { + BitWriter writer; + EncodePermutation(perm, 0, len, &writer, 0, nullptr); + writer.ZeroPadToByte(); + BitReader reader(writer.GetSpan()); + EXPECT_TRUE(DecodePermutation(0, len, out, &reader)); + EXPECT_TRUE(reader.Close()); + return writer.GetSpan().size(); +} + +enum Permutation { kIdentity, kFewSwaps, kFewSlides, kRandom }; + +#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ + defined(THREAD_SANITIZER) +constexpr size_t kNumReps = 128; +#else +constexpr size_t kNumReps = 1024; +#endif +constexpr size_t kSwaps = 32; + +void TestPermutation(Permutation kind, size_t len) { + std::vector perm(len); + std::iota(perm.begin(), perm.end(), 0); + std::mt19937 rng; + if (kind == kFewSwaps) { + std::uniform_int_distribution dist(0, len - 1); + for (size_t i = 0; i < kSwaps; i++) { + size_t a = dist(rng); + size_t b = dist(rng); + std::swap(perm[a], perm[b]); + } + } + if (kind == kFewSlides) { + std::uniform_int_distribution dist(0, len - 1); + for (size_t i = 0; i < kSwaps; i++) { + size_t a = dist(rng); + size_t b = dist(rng); + size_t from = std::min(a, b); + size_t to = std::max(a, b); + size_t start = perm[from]; + for (size_t j = from; j < to; j++) { + perm[j] = perm[j + 1]; + } + perm[to] = start; + } + } + if (kind == kRandom) { + std::shuffle(perm.begin(), perm.end(), rng); + } + std::vector out(len); + size_t size = 0; + for (size_t i = 0; i < kNumReps; i++) { + size = RoundtripPermutation(perm.data(), out.data(), len); + for (size_t idx = 0; idx < len; idx++) { + EXPECT_EQ(perm[idx], out[idx]); + } + } + printf("Encoded size: %zu\n", size); +} + +TEST(CoeffOrderTest, IdentitySmall) { TestPermutation(kIdentity, 256); } +TEST(CoeffOrderTest, kFewSlidesSmall) { TestPermutation(kFewSlides, 256); } +TEST(CoeffOrderTest, FewSwapsSmall) { TestPermutation(kFewSwaps, 256); } +TEST(CoeffOrderTest, RandomSmall) { TestPermutation(kRandom, 256); } + +TEST(CoeffOrderTest, IdentityMedium) { TestPermutation(kIdentity, 1 << 12); } +TEST(CoeffOrderTest, FewSlidesMedium) { TestPermutation(kFewSlides, 1 << 12); } +TEST(CoeffOrderTest, FewSwapsMedium) { TestPermutation(kFewSwaps, 1 << 12); } +TEST(CoeffOrderTest, RandomMedium) { TestPermutation(kRandom, 1 << 12); } + +TEST(CoeffOrderTest, IdentityBig) { TestPermutation(kIdentity, 1 << 16); } +TEST(CoeffOrderTest, FewSlidesBig) { TestPermutation(kFewSlides, 1 << 16); } +TEST(CoeffOrderTest, FewSwapsBig) { TestPermutation(kFewSwaps, 1 << 16); } +TEST(CoeffOrderTest, RandomBig) { TestPermutation(kRandom, 1 << 16); } + +} // namespace +} // namespace jxl diff --git a/jxl/color_encoding.cc b/jxl/color_encoding.cc new file mode 100644 index 000000000..5dfa59452 --- /dev/null +++ b/jxl/color_encoding.cc @@ -0,0 +1,508 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/color_encoding.h" + +#include + +#include "jxl/fields.h" + +namespace jxl { +namespace { + +// These strings are baked into Description - do not change. + +std::string ToString(ColorSpace color_space) { + switch (color_space) { + case ColorSpace::kRGB: + return "RGB"; + case ColorSpace::kGray: + return "Gra"; + case ColorSpace::kXYB: + return "XYB"; + case ColorSpace::kUnknown: + return "CS?"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid ColorSpace %u", static_cast(color_space)); +} + +std::string ToString(WhitePoint white_point) { + switch (white_point) { + case WhitePoint::kD65: + return "D65"; + case WhitePoint::kCustom: + return "Cst"; + case WhitePoint::kE: + return "EER"; + case WhitePoint::kDCI: + return "DCI"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid WhitePoint %u", static_cast(white_point)); +} + +std::string ToString(Primaries primaries) { + switch (primaries) { + case Primaries::kSRGB: + return "SRG"; + case Primaries::k2100: + return "202"; + case Primaries::kP3: + return "DCI"; + case Primaries::kCustom: + return "Cst"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid Primaries %u", static_cast(primaries)); +} + +std::string ToString(TransferFunction transfer_function) { + switch (transfer_function) { + case TransferFunction::kSRGB: + return "SRG"; + case TransferFunction::kLinear: + return "Lin"; + case TransferFunction::k709: + return "709"; + case TransferFunction::kPQ: + return "PeQ"; + case TransferFunction::kHLG: + return "HLG"; + case TransferFunction::kDCI: + return "DCI"; + case TransferFunction::kUnknown: + return "TF?"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid TransferFunction %u", + static_cast(transfer_function)); +} + +std::string ToString(RenderingIntent rendering_intent) { + switch (rendering_intent) { + case RenderingIntent::kPerceptual: + return "Per"; + case RenderingIntent::kRelative: + return "Rel"; + case RenderingIntent::kSaturation: + return "Sat"; + case RenderingIntent::kAbsolute: + return "Abs"; + } + // Should not happen - visitor fails if enum is invalid. + JXL_ABORT("Invalid RenderingIntent %u", + static_cast(rendering_intent)); +} + +template +Status ParseEnum(const std::string& token, Enum* value) { + std::string str; + for (Enum e : Values()) { + if (ToString(e) == token) { + *value = e; + return true; + } + } + return false; +} + +class Tokenizer { + public: + Tokenizer(const std::string* input, char separator) + : input_(input), separator_(separator) {} + + Status Next(std::string* JXL_RESTRICT next) { + const size_t end = input_->find(separator_, start_); + if (end == std::string::npos) { + *next = input_->substr(start_); // rest of string + } else { + *next = input_->substr(start_, end - start_); + } + if (next->empty()) return JXL_FAILURE("Missing token"); + start_ = end + 1; + return true; + } + + private: + const std::string* const input_; // not owned + const char separator_; + size_t start_ = 0; // of next token +}; + +Status ParseDouble(Tokenizer* tokenizer, double* JXL_RESTRICT d) { + std::string num; + JXL_RETURN_IF_ERROR(tokenizer->Next(&num)); + *d = std::stod(num); + return true; +} + +Status ParseColorSpace(Tokenizer* JXL_RESTRICT tokenizer, + ColorEncoding* JXL_RESTRICT c) { + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + ColorSpace cs; + if (ParseEnum(str, &cs)) { + c->SetColorSpace(cs); + return true; + } + + return JXL_FAILURE("Unknown ColorSpace %s", str.c_str()); +} + +Status ParseWhitePoint(Tokenizer* JXL_RESTRICT tokenizer, + ColorEncoding* JXL_RESTRICT c) { + if (c->ImplicitWhitePoint()) return true; + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (ParseEnum(str, &c->white_point)) return true; + + CIExy xy; + Tokenizer xy_tokenizer(&str, ';'); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.x)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.y)); + if (c->SetWhitePoint(xy)) return true; + + return JXL_FAILURE("Invalid white point %s", str.c_str()); +} + +Status ParsePrimaries(Tokenizer* JXL_RESTRICT tokenizer, + ColorEncoding* JXL_RESTRICT c) { + if (!c->HasPrimaries()) return true; + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (ParseEnum(str, &c->primaries)) return true; + + PrimariesCIExy xy; + Tokenizer xy_tokenizer(&str, ';'); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.r.x)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.r.y)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.g.x)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.g.y)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.b.x)); + JXL_RETURN_IF_ERROR(ParseDouble(&xy_tokenizer, &xy.b.y)); + if (c->SetPrimaries(xy)) return true; + + return JXL_FAILURE("Invalid primaries %s", str.c_str()); +} + +Status ParseRenderingIntent(Tokenizer* JXL_RESTRICT tokenizer, + ColorEncoding* JXL_RESTRICT c) { + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + if (ParseEnum(str, &c->rendering_intent)) return true; + + return JXL_FAILURE("Invalid RenderingIntent %s\n", str.c_str()); +} + +Status ParseTransferFunction(Tokenizer* JXL_RESTRICT tokenizer, + ColorEncoding* JXL_RESTRICT c) { + if (c->tf.SetImplicit()) return true; + + std::string str; + JXL_RETURN_IF_ERROR(tokenizer->Next(&str)); + TransferFunction transfer_function; + if (ParseEnum(str, &transfer_function)) { + c->tf.SetTransferFunction(transfer_function); + return true; + } + + if (str[0] == 'g') { + const double gamma = std::stod(str.substr(1)); + if (c->tf.SetGamma(gamma)) return true; + } + + return JXL_FAILURE("Invalid gamma %s", str.c_str()); +} + +static double F64FromI32(const int32_t i) { return i * 1E-6; } +static int32_t I32FromF64(const double f) { + return static_cast(std::round(f * 1E6)); +} + +} // namespace + +CIExy Customxy::Get() const { + CIExy xy; + xy.x = F64FromI32(x); + xy.y = F64FromI32(y); + return xy; +} + +Status Customxy::Set(const CIExy& xy) { + x = I32FromF64(xy.x); + y = I32FromF64(xy.y); + size_t extension_bits, total_bits; + if (!Bundle::CanEncode(*this, &extension_bits, &total_bits)) { + return JXL_FAILURE("Unable to encode XY %f %f", xy.x, xy.y); + } + return true; +} + +bool CustomTransferFunction::SetImplicit() { + if (nonserialized_color_space == ColorSpace::kXYB) { + if (!SetGamma(1.0 / 3)) JXL_ASSERT(false); + return true; + } + return false; +} + +Status CustomTransferFunction::SetGamma(double gamma) { + if (gamma <= 0.0 || gamma > 1.0) { + return JXL_FAILURE("Invalid gamma %f", gamma); + } + + have_gamma_ = false; + if (ApproxEq(gamma, 1.0)) { + transfer_function_ = TransferFunction::kLinear; + return true; + } + if (ApproxEq(gamma, 1.0 / 2.6)) { + transfer_function_ = TransferFunction::kDCI; + return true; + } + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + + have_gamma_ = true; + gamma_ = std::round(gamma * kGammaMul); + transfer_function_ = TransferFunction::kUnknown; + return true; +} + +CIExy ColorEncoding::GetWhitePoint() const { + CIExy xy; + switch (white_point) { + case WhitePoint::kCustom: + return white_.Get(); + + case WhitePoint::kD65: + xy.x = 0.3127; + xy.y = 0.3290; + return xy; + + case WhitePoint::kDCI: + // From https://ieeexplore.ieee.org/document/7290729 C.2 page 11 + xy.x = 0.314; + xy.y = 0.351; + return xy; + + case WhitePoint::kE: + xy.x = xy.y = 1.0 / 3; + return xy; + } + JXL_ABORT("Invalid WhitePoint %u", static_cast(white_point)); +} + +Status ColorEncoding::SetWhitePoint(const CIExy& xy) { + if (xy.x == 0.0 || xy.y == 0.0) { + return JXL_FAILURE("Invalid white point %f %f", xy.x, xy.y); + } + if (ApproxEq(xy.x, 0.3127) && ApproxEq(xy.y, 0.3290)) { + white_point = WhitePoint::kD65; + return true; + } + if (ApproxEq(xy.x, 1.0 / 3) && ApproxEq(xy.y, 1.0 / 3)) { + white_point = WhitePoint::kE; + return true; + } + if (ApproxEq(xy.x, 0.314) && ApproxEq(xy.y, 0.351)) { + white_point = WhitePoint::kDCI; + return true; + } + white_point = WhitePoint::kCustom; + return white_.Set(xy); +} + +PrimariesCIExy ColorEncoding::GetPrimaries() const { + JXL_ASSERT(HasPrimaries()); + PrimariesCIExy xy; + switch (primaries) { + case Primaries::kCustom: + xy.r = red_.Get(); + xy.g = green_.Get(); + xy.b = blue_.Get(); + return xy; + + case Primaries::kSRGB: + xy.r.x = 0.639998686; + xy.r.y = 0.330010138; + xy.g.x = 0.300003784; + xy.g.y = 0.600003357; + xy.b.x = 0.150002046; + xy.b.y = 0.059997204; + return xy; + + case Primaries::k2100: + xy.r.x = 0.708; + xy.r.y = 0.292; + xy.g.x = 0.170; + xy.g.y = 0.797; + xy.b.x = 0.131; + xy.b.y = 0.046; + return xy; + + case Primaries::kP3: + xy.r.x = 0.680; + xy.r.y = 0.320; + xy.g.x = 0.265; + xy.g.y = 0.690; + xy.b.x = 0.150; + xy.b.y = 0.060; + return xy; + } + JXL_ABORT("Invalid Primaries %u", static_cast(primaries)); +} + +Status ColorEncoding::SetPrimaries(const PrimariesCIExy& xy) { + JXL_ASSERT(HasPrimaries()); + if (xy.r.x == 0.0 || xy.r.y == 0.0 || xy.g.x == 0.0 || xy.g.y == 0.0 || + xy.b.x == 0.0 || xy.b.y == 0.0) { + return JXL_FAILURE("Invalid primaries %f %f %f %f %f %f", xy.r.x, xy.r.y, + xy.g.x, xy.g.y, xy.b.x, xy.b.y); + } + + if (ApproxEq(xy.r.x, 0.64) && ApproxEq(xy.r.y, 0.33) && + ApproxEq(xy.g.x, 0.30) && ApproxEq(xy.g.y, 0.60) && + ApproxEq(xy.b.x, 0.15) && ApproxEq(xy.b.y, 0.06)) { + primaries = Primaries::kSRGB; + return true; + } + + if (ApproxEq(xy.r.x, 0.708) && ApproxEq(xy.r.y, 0.292) && + ApproxEq(xy.g.x, 0.170) && ApproxEq(xy.g.y, 0.797) && + ApproxEq(xy.b.x, 0.131) && ApproxEq(xy.b.y, 0.046)) { + primaries = Primaries::k2100; + return true; + } + if (ApproxEq(xy.r.x, 0.680) && ApproxEq(xy.r.y, 0.320) && + ApproxEq(xy.g.x, 0.265) && ApproxEq(xy.g.y, 0.690) && + ApproxEq(xy.b.x, 0.150) && ApproxEq(xy.b.y, 0.060)) { + primaries = Primaries::kP3; + return true; + } + + primaries = Primaries::kCustom; + JXL_RETURN_IF_ERROR(red_.Set(xy.r)); + JXL_RETURN_IF_ERROR(green_.Set(xy.g)); + JXL_RETURN_IF_ERROR(blue_.Set(xy.b)); + return true; +} + +std::string Description(const ColorEncoding& c_in) { + // Copy required for Implicit* + ColorEncoding c = c_in; + + std::string d = ToString(c.GetColorSpace()); + + if (!c.ImplicitWhitePoint()) { + d += '_'; + if (c.white_point == WhitePoint::kCustom) { + const CIExy wp = c.GetWhitePoint(); + d += std::to_string(wp.x) + ';'; + d += std::to_string(wp.y); + } else { + d += ToString(c.white_point); + } + } + + if (c.HasPrimaries()) { + d += '_'; + if (c.primaries == Primaries::kCustom) { + const PrimariesCIExy pr = c.GetPrimaries(); + d += std::to_string(pr.r.x) + ';'; + d += std::to_string(pr.r.y) + ';'; + d += std::to_string(pr.g.x) + ';'; + d += std::to_string(pr.g.y) + ';'; + d += std::to_string(pr.b.x) + ';'; + d += std::to_string(pr.b.y); + } else { + d += ToString(c.primaries); + } + } + + d += '_'; + d += ToString(c.rendering_intent); + + if (!c.tf.SetImplicit()) { + d += '_'; + if (c.tf.IsGamma()) { + d += 'g'; + d += std::to_string(c.tf.GetGamma()); + } else { + d += ToString(c.tf.GetTransferFunction()); + } + } + + return d; +} + +Status ParseDescription(const std::string& description, + ColorEncoding* JXL_RESTRICT c) { + Tokenizer tokenizer(&description, '_'); + JXL_RETURN_IF_ERROR(ParseColorSpace(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseWhitePoint(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParsePrimaries(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseRenderingIntent(&tokenizer, c)); + JXL_RETURN_IF_ERROR(ParseTransferFunction(&tokenizer, c)); + return true; +} + +std::vector AllEncodings() { + std::vector all_encodings; + all_encodings.reserve(300); + ColorEncoding c; + + for (ColorSpace cs : Values()) { + if (cs == ColorSpace::kUnknown || cs == ColorSpace::kXYB) continue; + c.SetColorSpace(cs); + + for (WhitePoint wp : Values()) { + if (wp == WhitePoint::kCustom) continue; + if (c.ImplicitWhitePoint() && c.white_point != wp) continue; + c.white_point = wp; + + for (Primaries primaries : Values()) { + if (primaries == Primaries::kCustom) continue; + if (!c.HasPrimaries()) continue; + c.primaries = primaries; + + for (TransferFunction tf : Values()) { + if (tf == TransferFunction::kUnknown) continue; + if (c.tf.SetImplicit() && + (c.tf.IsGamma() || c.tf.GetTransferFunction() != tf)) { + continue; + } + c.tf.SetTransferFunction(tf); + + for (RenderingIntent ri : Values()) { + c.rendering_intent = ri; + + all_encodings.push_back(c); + } + } + } + } + } + + return all_encodings; +} + +Customxy::Customxy() { Bundle::Init(this); } +CustomTransferFunction::CustomTransferFunction() { Bundle::Init(this); } +ColorEncoding::ColorEncoding() { Bundle::Init(this); } + +} // namespace jxl diff --git a/jxl/color_encoding.h b/jxl/color_encoding.h new file mode 100644 index 000000000..23d42a208 --- /dev/null +++ b/jxl/color_encoding.h @@ -0,0 +1,445 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COLOR_ENCODING_H_ +#define JXL_COLOR_ENCODING_H_ + +// Metadata for color space conversions. + +#include +#include +#include + +#include // std::abs +#include +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/status.h" +#include "jxl/field_encodings.h" + +namespace jxl { + +// (All CIE units are for the standard 1931 2 degree observer) + +enum class ColorSpace : uint32_t { kRGB, kGray, kXYB, kUnknown }; + +static inline const char* EnumName(ColorSpace /*unused*/) { + return "ColorSpace"; +} +static inline constexpr uint64_t EnumBits(ColorSpace /*unused*/) { + using CS = ColorSpace; + return MakeBit(CS::kRGB) | MakeBit(CS::kGray) | MakeBit(CS::kXYB) | + MakeBit(CS::kUnknown); +} + +// Values from CICP ColourPrimaries. +enum class WhitePoint : uint32_t { + kD65 = 1, // sRGB/BT.709/Display P3/BT.2020 + kCustom = 2, // Actual values encoded in separate fields + kE = 10, // XYZ + kDCI = 11, // DCI-P3 +}; + +static inline const char* EnumName(WhitePoint /*unused*/) { + return "WhitePoint"; +} +static inline constexpr uint64_t EnumBits(WhitePoint /*unused*/) { + return MakeBit(WhitePoint::kD65) | MakeBit(WhitePoint::kCustom) | + MakeBit(WhitePoint::kE) | MakeBit(WhitePoint::kDCI); +} + +// Values from CICP ColourPrimaries +enum class Primaries : uint32_t { + kSRGB = 1, // Same as BT.709 + kCustom = 2, // Actual values encoded in separate fields + k2100 = 9, // Same as BT.2020 + kP3 = 11, +}; + +static inline const char* EnumName(Primaries /*unused*/) { return "Primaries"; } +static inline constexpr uint64_t EnumBits(Primaries /*unused*/) { + using Pr = Primaries; + return MakeBit(Pr::kSRGB) | MakeBit(Pr::kCustom) | MakeBit(Pr::k2100) | + MakeBit(Pr::kP3); +} + +// Values from CICP TransferCharacteristics +enum TransferFunction : uint32_t { + k709 = 1, + kUnknown = 2, + kLinear = 8, + kSRGB = 13, + kPQ = 16, // from BT.2100 + kDCI = 17, // from SMPTE RP 431-2 reference projector + kHLG = 18, // from BT.2100 +}; + +static inline const char* EnumName(TransferFunction /*unused*/) { + return "TransferFunction"; +} +static inline constexpr uint64_t EnumBits(TransferFunction /*unused*/) { + using TF = TransferFunction; + return MakeBit(TF::k709) | MakeBit(TF::kLinear) | MakeBit(TF::kSRGB) | + MakeBit(TF::kPQ) | MakeBit(TF::kDCI) | MakeBit(TF::kHLG) | + MakeBit(TF::kUnknown); +} + +enum class RenderingIntent : uint32_t { + // Values match ICC sRGB encodings. + kPerceptual = 0, // good for photos, requires a profile with LUT. + kRelative, // good for logos. + kSaturation, // perhaps useful for CG with fully saturated colors. + kAbsolute, // leaves white point unchanged; good for proofing. +}; + +static inline const char* EnumName(RenderingIntent /*unused*/) { + return "RenderingIntent"; +} +static inline constexpr uint64_t EnumBits(RenderingIntent /*unused*/) { + using RI = RenderingIntent; + return MakeBit(RI::kPerceptual) | MakeBit(RI::kRelative) | + MakeBit(RI::kSaturation) | MakeBit(RI::kAbsolute); +} + +// Chromaticity (Y is omitted because it is 1 for primaries/white points) +struct CIExy { + double x = 0.0; + double y = 0.0; +}; + +struct PrimariesCIExy { + CIExy r; + CIExy g; + CIExy b; +}; + +// Serializable form of CIExy. +struct Customxy { + Customxy(); + static const char* Name() { return "Customxy"; } + + template + Status VisitFields(Visitor* JXL_RESTRICT visitor) { + visitor->S32(Bits(19), BitsOffset(19, 524288), BitsOffset(20, 1048576), + BitsOffset(21, 2097152), 0, &x); + visitor->S32(Bits(19), BitsOffset(19, 524288), BitsOffset(20, 1048576), + BitsOffset(21, 2097152), 0, &y); + return true; + } + + CIExy Get() const; + // Returns false if x or y do not fit in the encoding. + Status Set(const CIExy& xy); + + int32_t x; + int32_t y; +}; + +struct CustomTransferFunction { + CustomTransferFunction(); + static const char* Name() { return "CustomTransferFunction"; } + + // Sets fields and returns true if nonserialized_color_space has an implicit + // transfer function, otherwise leaves fields unchanged and returns false. + bool SetImplicit(); + + // Gamma: only used for PNG inputs + bool IsGamma() const { return have_gamma_; } + double GetGamma() const { + JXL_ASSERT(IsGamma()); + return gamma_ * 1E-7; // (0, 1) + } + Status SetGamma(double gamma); + + TransferFunction GetTransferFunction() const { + JXL_ASSERT(!IsGamma()); + return transfer_function_; + } + void SetTransferFunction(const TransferFunction tf) { + have_gamma_ = false; + transfer_function_ = tf; + } + + bool IsUnknown() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kUnknown); + } + bool IsSRGB() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kSRGB); + } + bool IsLinear() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kLinear); + } + bool IsPQ() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kPQ); + } + bool IsHLG() const { + return !have_gamma_ && (transfer_function_ == TransferFunction::kHLG); + } + bool IsSame(const CustomTransferFunction& other) const { + if (have_gamma_ != other.have_gamma_) return false; + if (have_gamma_) { + if (gamma_ != other.gamma_) return false; + } else { + if (transfer_function_ != other.transfer_function_) return false; + } + return true; + } + + template + bool VisitFields(Visitor* JXL_RESTRICT visitor) { + if (visitor->Conditional(!SetImplicit())) { + visitor->Bool(false, &have_gamma_); + + if (visitor->Conditional(have_gamma_)) { + visitor->Bits(24, kGammaMul, &gamma_); + if (gamma_ > kGammaMul) { + return JXL_FAILURE("Invalid gamma %u", gamma_); + } + } + + if (visitor->Conditional(!have_gamma_)) { + JXL_RETURN_IF_ERROR( + visitor->Enum(TransferFunction::kSRGB, &transfer_function_)); + } + } + + return true; + } + + // Must be set before calling VisitFields! + ColorSpace nonserialized_color_space = ColorSpace::kRGB; + + private: + static constexpr uint32_t kGammaMul = 10000000; + + bool have_gamma_; + + // OETF exponent to go from linear to gamma-compressed. + uint32_t gamma_; // Only used if have_gamma_. + + // Can be kUnknown. + TransferFunction transfer_function_; // Only used if !have_gamma_. +}; + +// Compact encoding of data required to interpret and translate pixels to a +// known color space. Stored in Metadata. +struct ColorEncoding { + ColorEncoding(); + static const char* Name() { return "ColorEncoding"; } + + bool IsGray() const { return color_space_ == ColorSpace::kGray; } + size_t Channels() const { return IsGray() ? 1 : 3; } + + // Returns false if the field is invalid and unusable. + bool HasPrimaries() const { + return !IsGray() && color_space_ != ColorSpace::kXYB; + } + + // Returns true after setting the field to a value defined by color_space, + // otherwise false and leaves the field unchanged. + bool ImplicitWhitePoint() { + if (color_space_ == ColorSpace::kXYB) { + white_point = WhitePoint::kD65; + return true; + } + return false; + } + + bool IsSRGB() const { + if (!IsGray() && color_space_ != ColorSpace::kRGB) return false; + if (white_point != WhitePoint::kD65) return false; + if (primaries != Primaries::kSRGB) return false; + if (!tf.IsSRGB()) return false; + return true; + } + + void SetSRGB(const ColorSpace cs) { + icc.clear(); + received_icc = opaque_icc = false; + JXL_ASSERT(cs == ColorSpace::kGray || cs == ColorSpace::kRGB); + color_space_ = cs; + white_point = WhitePoint::kD65; + primaries = Primaries::kSRGB; + tf.SetTransferFunction(TransferFunction::kSRGB); + rendering_intent = RenderingIntent::kRelative; + } + + template + bool VisitFields(Visitor* JXL_RESTRICT visitor) { + // NOTE: `icc` is not serialized here to avoid dependency on icc_codec.h. + + if (visitor->AllDefault(*this, &all_default)) return true; + + visitor->Bool(false, &received_icc); + + if (visitor->Conditional(received_icc)) { + visitor->Bool(false, &opaque_icc); + } + + // NOTE: use most likely values as default so ImageMetadata.all_default is + // true. We set ColorSpace/TransferFunction to kUnknown below if opaque_icc. + + if (visitor->Conditional(!opaque_icc)) { + JXL_RETURN_IF_ERROR(visitor->Enum(ColorSpace::kRGB, &color_space_)); + + if (visitor->Conditional(!ImplicitWhitePoint())) { + JXL_RETURN_IF_ERROR(visitor->Enum(WhitePoint::kD65, &white_point)); + if (visitor->Conditional(white_point == WhitePoint::kCustom)) { + JXL_RETURN_IF_ERROR(visitor->VisitNested(&white_)); + } + } + + if (visitor->Conditional(HasPrimaries())) { + JXL_RETURN_IF_ERROR(visitor->Enum(Primaries::kSRGB, &primaries)); + if (visitor->Conditional(primaries == Primaries::kCustom)) { + JXL_RETURN_IF_ERROR(visitor->VisitNested(&red_)); + JXL_RETURN_IF_ERROR(visitor->VisitNested(&green_)); + JXL_RETURN_IF_ERROR(visitor->VisitNested(&blue_)); + } + } + + JXL_RETURN_IF_ERROR(visitor->VisitNested(&tf)); + + JXL_RETURN_IF_ERROR( + visitor->Enum(RenderingIntent::kRelative, &rendering_intent)); + } + + if (opaque_icc) { + // Set correct 'defaults' (see above) but only when reading. + if (visitor->IsReading()) { + color_space_ = ColorSpace::kUnknown; + tf.SetTransferFunction(TransferFunction::kUnknown); + } + } else { + // Not opaque implies all fields should be known. + if (color_space_ == ColorSpace::kUnknown || tf.IsUnknown()) { + return JXL_FAILURE("Opaque %d but cs %u and tf %u%s", opaque_icc, + color_space_, + tf.IsGamma() ? 0 : tf.GetTransferFunction(), + tf.IsGamma() ? "(gamma)" : ""); + } + } + + return true; + } + + // Accessors ensure tf.nonserialized_color_space is updated at the same time. + ColorSpace GetColorSpace() const { return color_space_; } + void SetColorSpace(const ColorSpace cs) { + color_space_ = cs; + tf.nonserialized_color_space = cs; + } + + CIExy GetWhitePoint() const; + Status SetWhitePoint(const CIExy& xy); + + PrimariesCIExy GetPrimaries() const; + Status SetPrimaries(const PrimariesCIExy& xy); + + // Checks if the color spaces (including white point / primaries) are the + // same, but ignores the transfer function, rendering intent and ICC bytes. + bool SameColorSpace(const ColorEncoding& other) const { + if (color_space_ != other.color_space_) return false; + + if (white_point != other.white_point) return false; + if (white_point == WhitePoint::kCustom) { + if (white_.x != other.white_.x || white_.y != other.white_.y) + return false; + } + + if (HasPrimaries() != other.HasPrimaries()) return false; + if (HasPrimaries()) { + if (primaries != other.primaries) return false; + if (primaries == Primaries::kCustom) { + if (red_.x != other.red_.x || red_.y != other.red_.y) return false; + if (green_.x != other.green_.x || green_.y != other.green_.y) + return false; + if (blue_.x != other.blue_.x || blue_.y != other.blue_.y) return false; + } + } + return true; + } + + // Checks if the color space and transfer function are the same, ignoring + // rendering intent and ICC bytes + bool SameColorEncoding(const ColorEncoding& other) const { + return SameColorSpace(other) && tf.IsSame(other.tf); + } + + mutable bool all_default; + + // Either empty, or a valid ICC profile. + PaddedBytes icc; + + // icc is non-empty AND it comes from an external source. + bool received_icc; + + // received_icc AND the ICC cannot be described by ColorSpace and/or + // TransferFunction. If true, subsequent fields are default-initialized! + bool opaque_icc; + + // Not serialized if opaque_icc. Fully describes the ICC if !opaque_icc. + // SetProfile will either fail or initialize all fields. + + WhitePoint white_point; + Primaries primaries; // Only valid if HasPrimaries() + CustomTransferFunction tf; + RenderingIntent rendering_intent; + + private: + ColorSpace color_space_; // Can be kUnknown + + // Only used if white_point == kCustom. + Customxy white_; + + // Only used if primaries == kCustom. + Customxy red_; + Customxy green_; + Customxy blue_; +}; + +// Returns whether the two inputs are approximately equal. +static inline bool ApproxEq(const double a, const double b, +#if JPEGXL_ENABLE_SKCMS + double max_l1 = 1E-3) { +#else + double max_l1 = 8E-5) { +#endif + // Threshold should be sufficient for ICC's 15-bit fixed-point numbers. + // We have seen differences of 7.1E-5 with lcms2 and 1E-3 with skcms. + return std::abs(a - b) <= max_l1; +} + +// Returns a representation of the ColorEncoding fields (not icc). +// Example description: "RGB_D65_SRG_Rel_Lin" +std::string Description(const ColorEncoding& c); +Status ParseDescription(const std::string& description, + ColorEncoding* JXL_RESTRICT c); + +// Returns ColorEncoding with empty ICC profile. Caller must use +// ColorEncoding::CreateProfile() to generate a profile. +std::vector AllEncodings(); + +// Define the operator<< for tests. +static inline ::std::ostream& operator<<(::std::ostream& os, + const ColorEncoding& c) { + return os << "ColorEncoding/" << Description(c); +} + +} // namespace jxl + +#endif // JXL_COLOR_ENCODING_H_ diff --git a/jxl/color_encoding_test.cc b/jxl/color_encoding_test.cc new file mode 100644 index 000000000..f2670c13b --- /dev/null +++ b/jxl/color_encoding_test.cc @@ -0,0 +1,110 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/color_encoding.h" + +#include + +#include "gtest/gtest.h" + +namespace jxl { +namespace { + +TEST(ColorEncodingTest, RoundTripAll) { + for (const ColorEncoding& c_original : AllEncodings()) { + // Verify Set(Get) yields the same white point/primaries/gamma. + { + ColorEncoding c; + EXPECT_TRUE(c.SetWhitePoint(c_original.GetWhitePoint())); + EXPECT_EQ(c_original.white_point, c.white_point); + } + { + ColorEncoding c; + EXPECT_TRUE(c.SetPrimaries(c_original.GetPrimaries())); + EXPECT_EQ(c_original.primaries, c.primaries); + } + if (c_original.tf.IsGamma()) { + ColorEncoding c; + EXPECT_TRUE(c.tf.SetGamma(c_original.tf.GetGamma())); + EXPECT_TRUE(c_original.tf.IsSame(c.tf)); + } + + // Verify ParseDescription(Description) yields the same ColorEncoding + { + const std::string description = Description(c_original); + printf("%s\n", description.c_str()); + ColorEncoding c; + EXPECT_TRUE(ParseDescription(description, &c)); + EXPECT_TRUE(c_original.SameColorEncoding(c)); + } + } +} + +// Verify Set(Get) for specific custom values + +TEST(ColorEncodingTest, CustomWhitePoint) { + ColorEncoding c; + // Nonsensical values + CIExy xy_in; + xy_in.x = 0.8; + xy_in.y = 0.01; + EXPECT_TRUE(c.SetWhitePoint(xy_in)); + const CIExy xy = c.GetWhitePoint(); + + ColorEncoding c2; + EXPECT_TRUE(c2.SetWhitePoint(xy)); + EXPECT_TRUE(c.SameColorSpace(c2)); +} + +TEST(ColorEncodingTest, CustomPrimaries) { + ColorEncoding c; + PrimariesCIExy xy_in; + // Nonsensical values + xy_in.r.x = -0.01; + xy_in.r.y = 0.2; + xy_in.g.x = 0.4; + xy_in.g.y = 0.401; + xy_in.b.x = 1.1; + xy_in.b.y = -1.2; + EXPECT_TRUE(c.SetPrimaries(xy_in)); + const PrimariesCIExy xy = c.GetPrimaries(); + + ColorEncoding c2; + EXPECT_TRUE(c2.SetPrimaries(xy)); + EXPECT_TRUE(c.SameColorSpace(c2)); +} + +TEST(ColorEncodingTest, CustomGamma) { + ColorEncoding c; +#ifndef JXL_CRASH_ON_ERROR + EXPECT_FALSE(c.tf.SetGamma(0.0)); + EXPECT_FALSE(c.tf.SetGamma(-1E-6)); + EXPECT_FALSE(c.tf.SetGamma(1.001)); +#endif + EXPECT_TRUE(c.tf.SetGamma(1.0)); + EXPECT_FALSE(c.tf.IsGamma()); + EXPECT_TRUE(c.tf.IsLinear()); + + EXPECT_TRUE(c.tf.SetGamma(0.123)); + EXPECT_TRUE(c.tf.IsGamma()); + const double gamma = c.tf.GetGamma(); + + ColorEncoding c2; + EXPECT_TRUE(c2.tf.SetGamma(gamma)); + EXPECT_TRUE(c.SameColorEncoding(c2)); + EXPECT_TRUE(c2.tf.IsGamma()); +} + +} // namespace +} // namespace jxl diff --git a/jxl/color_management.cc b/jxl/color_management.cc new file mode 100644 index 000000000..47a2980b7 --- /dev/null +++ b/jxl/color_management.cc @@ -0,0 +1,1684 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/color_management.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/field_encodings.h" +#include "jxl/linalg.h" +#include "jxl/rational_polynomial.h" +#if JPEGXL_ENABLE_SKCMS +#include "skcms.h" +#else // JPEGXL_ENABLE_SKCMS +#include "lcms2.h" +#include "lcms2_plugin.h" +#endif // JPEGXL_ENABLE_SKCMS + +namespace jxl { +namespace { + +#define JXL_CMS_VERBOSE 0 + +// Define to 1 on OS X as a workaround for older LCMS lacking MD5. +#define JXL_CMS_OLD_VERSION 0 + +// cms functions (even *THR) are not thread-safe, except cmsDoTransform. +// To ensure all functions are covered without frequent lock-taking nor risk of +// recursive lock, we lock in the top-level APIs. +std::mutex lcms_mutex; + +#if JPEGXL_ENABLE_SKCMS +JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const float XYZ[3]) { + const float factor = 1.f / (XYZ[0] + XYZ[1] + XYZ[2]); + CIExy xy; + xy.x = XYZ[0] * factor; + xy.y = XYZ[1] * factor; + return xy; +} + +void CIEXYZFromWhiteCIExy(const CIExy& xy, float XYZ[3]) { + // Target Y = 1. + const float factor = 1 / xy.y; + XYZ[0] = xy.x * factor; + XYZ[1] = 1; + XYZ[2] = (1 - xy.x - xy.y) * factor; +} +#else // JPEGXL_ENABLE_SKCMS +// (LCMS interface requires xyY but we omit the Y for white points/primaries.) + +JXL_MUST_USE_RESULT CIExy CIExyFromxyY(const cmsCIExyY& xyY) { + CIExy xy; + xy.x = xyY.x; + xy.y = xyY.y; + return xy; +} + +JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const cmsCIEXYZ& XYZ) { + cmsCIExyY xyY; + cmsXYZ2xyY(/*Dest=*/&xyY, /*Source=*/&XYZ); + return CIExyFromxyY(xyY); +} + +JXL_MUST_USE_RESULT cmsCIEXYZ D50_XYZ() { + // Quantized D50 as stored in ICC profiles. + return {0.96420288, 1.0, 0.82490540}; +} + +JXL_MUST_USE_RESULT cmsCIExyY xyYFromCIExy(const CIExy& xy) { + const cmsCIExyY xyY = {xy.x, xy.y, 1.0}; + return xyY; +} + +// RAII + +struct ProfileDeleter { + void operator()(void* p) { cmsCloseProfile(p); } +}; +using Profile = std::unique_ptr; + +struct TransformDeleter { + void operator()(void* p) { cmsDeleteTransform(p); } +}; +using Transform = std::unique_ptr; + +struct CurveDeleter { + void operator()(cmsToneCurve* p) { cmsFreeToneCurve(p); } +}; +using Curve = std::unique_ptr; + +Status CreateProfileXYZ(const cmsContext context, + Profile* JXL_RESTRICT profile) { + profile->reset(cmsCreateXYZProfileTHR(context)); + if (profile->get() == nullptr) return JXL_FAILURE("Failed to create XYZ"); + return true; +} + +// Multi-Localized Unicode string +class MLU { + public: + MLU(const cmsContext context, const char* ascii) + : mlu_(cmsMLUalloc(context, 0)) { + if (!cmsMLUsetASCII(mlu_, "en", "US", ascii)) { + JXL_NOTIFY_ERROR("Failed to set ASCII"); + } + } + ~MLU() { cmsMLUfree(mlu_); } + + MLU(const MLU&) = delete; + MLU& operator=(const MLU&) = delete; + MLU(MLU&&) = delete; + MLU& operator=(MLU&&) = delete; + + cmsMLU* get() const { return mlu_; } + + private: + cmsMLU* mlu_; +}; + +// Sets header and required tags; called by EncodeProfile. +Status SetTags(const cmsContext context, const Profile& profile, + const std::string& profile_description) { + cmsHPROFILE p = profile.get(); + + // Header + cmsSetHeaderFlags(p, 1); // embedded + + const MLU copyright( + context, + "Copyright 2018 Google LLC, CC-BY-SA 3.0 Unported license" + "(https://creativecommons.org/licenses/by-sa/3.0/legalcode)"); + const MLU manufacturer(context, "Google"); + const MLU model(context, "Image codec"); + const MLU description(context, profile_description.c_str()); + + // Required tags + bool all_ok = true; + all_ok &= cmsWriteTag(p, cmsSigCopyrightTag, copyright.get()); + all_ok &= cmsWriteTag(p, cmsSigDeviceMfgDescTag, manufacturer.get()); + all_ok &= cmsWriteTag(p, cmsSigDeviceModelDescTag, model.get()); + all_ok &= cmsWriteTag(p, cmsSigProfileDescriptionTag, description.get()); + + if (!all_ok) return JXL_FAILURE("Failed to write header/tags"); + return true; +} + +// Replacement for cmsMD5computeID, which re-encodes the profile and in so +// doing changes the creator, resulting in different checksums than here. +// See https://github.com/mm2/Little-CMS/issues/181. This implementation uses +// newer LCMS features but the checksum matches displaycal's expectation. +void ComputeProfileID(const cmsContext context, PaddedBytes icc, + uint8_t id[16]) { +#if !JXL_CMS_OLD_VERSION + cmsProfileID profile_id; + // Per http://www.color.org/specification/ICC1v43_2010-12.pdf section 7.2.18 + JXL_ASSERT(icc.size() > 99); + memset(icc.data() + 44, 0, 4); // Profile flags. + memset(icc.data() + 64, 0, 4); // Rendering intent. + memset(icc.data() + 84, 0, 16); // Profile ID. + cmsHANDLE md5_handle = cmsMD5alloc(context); + cmsMD5add(md5_handle, icc.data(), icc.size()); + cmsMD5finish(&profile_id, md5_handle); + memcpy(id, profile_id.ID8, 16); +#endif // !JXL_CMS_OLD_VERSION +} + +Status EncodeProfile(const cmsContext context, const Profile& profile, + const std::string& description, PaddedBytes* icc) { + JXL_RETURN_IF_ERROR(SetTags(context, profile, description)); + + cmsUInt32Number size = 0; + if (!cmsSaveProfileToMem(profile.get(), nullptr, &size)) { + return JXL_FAILURE("Failed to get profile size"); + } + JXL_ASSERT(size != 0); + + icc->resize(size); + if (!cmsSaveProfileToMem(profile.get(), icc->data(), &size)) { + return JXL_FAILURE("Failed to encode profile"); + } + JXL_ASSERT(size == icc->size()); + ComputeProfileID(context, *icc, icc->data() + 84); + return true; +} +#endif // !JPEGXL_ENABLE_SKCMS + +#if JPEGXL_ENABLE_SKCMS +// IMPORTANT: icc must outlive profile. +Status DecodeProfile(const PaddedBytes& icc, skcms_ICCProfile* const profile) { + if (!skcms_Parse(icc.data(), icc.size(), profile)) { + return JXL_FAILURE("Failed to parse ICC profile with %zu bytes", + icc.size()); + } + return true; +} +#else // JPEGXL_ENABLE_SKCMS +Status DecodeProfile(const cmsContext context, const PaddedBytes& icc, + Profile* profile) { + profile->reset(cmsOpenProfileFromMemTHR(context, icc.data(), icc.size())); + if (profile->get() == nullptr) { + return JXL_FAILURE("Failed to decode profile"); + } + + // WARNING: due to the LCMS MD5 issue mentioned above, many existing + // profiles have incorrect MD5, so do not even bother checking them nor + // generating warning clutter. + + return true; +} +#endif // JPEGXL_ENABLE_SKCMS + +// Definitions for BT.2100-2 transfer functions: +// "display" is linear light (nits) normalized to [0, 1]. +// "encoded" is a nonlinear encoding (e.g. PQ) in [0, 1]. +// "scene" is a linear function of photon counts, normalized to [0, 1]. + +// Despite the stated ranges, we need unbounded transfer functions: see +// http://www.littlecms.com/CIC18_UnboundedCMM.pdf. Inputs can be negative or +// above 1 due to chromatic adaptation. To avoid severe round-trip errors caused +// by clamping, we mirror negative inputs via copysign (f(-x) = -f(x), see +// https://developer.apple.com/documentation/coregraphics/cgcolorspace/1644735-extendedsrgb) +// and extend the function domains above 1. + +// Hybrid Log-Gamma. +class TF_HLG { + public: + // EOTF. e = encoded. + JXL_INLINE double DisplayFromEncoded(const double e) const { + const double lifted = e * (1.0 - kBeta) + kBeta; + return OOTF(InvOETF(lifted)); + } + + // Inverse EOTF. d = display. + JXL_INLINE double EncodedFromDisplay(const double d) const { + const double lifted = OETF(InvOOTF(d)); + const double e = (lifted - kBeta) * (1.0 / (1.0 - kBeta)); + return e; + } + + private: + // OETF (defines the HLG approach). s = scene, returns encoded. + JXL_INLINE double OETF(double s) const { + if (s == 0.0) return 0.0; + const double original_sign = s; + s = std::abs(s); + + if (s <= kDiv12) return std::copysign(std::sqrt(3.0 * s), original_sign); + + const double e = kA * std::log(12 * s - kB) + kC; + JXL_ASSERT(e > 0.0); + return std::copysign(e, original_sign); + } + + // e = encoded, returns scene. + JXL_INLINE double InvOETF(double e) const { + if (e == 0.0) return 0.0; + const double original_sign = e; + e = std::abs(e); + + if (e <= 0.5) return std::copysign(e * e * (1.0 / 3), original_sign); + + const double s = (std::exp((e - kC) * kRA) + kB) * kDiv12; + JXL_ASSERT(s >= 0); + return std::copysign(s, original_sign); + } + + // s = scene, returns display. + JXL_INLINE double OOTF(const double s) const { + // The actual (red channel) OOTF is RD = alpha * YS^(gamma-1) * RS, where + // YS = 0.2627 * RS + 0.6780 * GS + 0.0593 * BS. Let alpha = 1 so we return + // "display" (normalized [0, 1]) instead of nits. Our transfer function + // interface does not allow a dependency on YS. Fortunately, the system + // gamma at 334 nits is 1.0, so this reduces to RD = RS. + return s; + } + + // d = display, returns scene. + JXL_INLINE double InvOOTF(const double d) const { + return d; // see OOTF(). + } + + // Assume 1000:1 contrast @ 200 nits => gamma 0.9 + static constexpr double kBeta = 0.04; // = sqrt(3 * contrast^(1/gamma)) + + static constexpr double kA = 0.17883277; + static constexpr double kRA = 1.0 / kA; + static constexpr double kB = 1 - 4 * kA; + static constexpr double kC = 0.5599107295; + static constexpr double kDiv12 = 1.0 / 12; +}; + +// Perceptual Quantization +class TF_PQ { + public: + // EOTF (defines the PQ approach). e = encoded. + JXL_INLINE double DisplayFromEncoded(double e) const { + if (e == 0.0) return 0.0; + const double original_sign = e; + e = std::abs(e); + + const double xp = std::pow(e, 1.0 / kM2); + const double num = std::max(xp - kC1, 0.0); + const double den = kC2 - kC3 * xp; + JXL_ASSERT(den != 0.0); + const double d = std::pow(num / den, 1.0 / kM1); + JXL_ASSERT(d >= 0.0); // Equal for e ~= 1E-9 + return std::copysign(d, original_sign); + } + + // Inverse EOTF. d = display. + JXL_INLINE double EncodedFromDisplay(double d) const { + if (d == 0.0) return 0.0; + const double original_sign = d; + d = std::abs(d); + + const double xp = std::pow(d, kM1); + const double num = kC1 + xp * kC2; + const double den = 1.0 + xp * kC3; + const double e = std::pow(num / den, kM2); + JXL_ASSERT(e > 0.0); + return std::copysign(e, original_sign); + } + + private: + static constexpr double kM1 = 2610.0 / 16384; + static constexpr double kM2 = (2523.0 / 4096) * 128; + static constexpr double kC1 = 3424.0 / 4096; + static constexpr double kC2 = (2413.0 / 4096) * 32; + static constexpr double kC3 = (2392.0 / 4096) * 32; +}; + +// sRGB +class TF_SRGB { + public: + template + HWY_ATTR JXL_INLINE V DisplayFromEncoded(V x) const { + const HWY_FULL(float) d; + const HWY_FULL(uint32_t) du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = x & kSign; + x = AndNot(kSign, x); // abs + + // Computed via af_cheb_rational (k=100); replicated 4x. + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + 2.200248328e-04, 2.200248328e-04, 2.200248328e-04, 2.200248328e-04, + 1.043637593e-02, 1.043637593e-02, 1.043637593e-02, 1.043637593e-02, + 1.624820318e-01, 1.624820318e-01, 1.624820318e-01, 1.624820318e-01, + 7.961564959e-01, 7.961564959e-01, 7.961564959e-01, 7.961564959e-01, + 8.210152774e-01, 8.210152774e-01, 8.210152774e-01, 8.210152774e-01, + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + 2.631846970e-01, 2.631846970e-01, 2.631846970e-01, 2.631846970e-01, + 1.076976492e+00, 1.076976492e+00, 1.076976492e+00, 1.076976492e+00, + 4.987528350e-01, 4.987528350e-01, 4.987528350e-01, 4.987528350e-01, + -5.512498495e-02, -5.512498495e-02, -5.512498495e-02, -5.512498495e-02, + 6.521209011e-03, 6.521209011e-03, 6.521209011e-03, 6.521209011e-03, + }; + const V linear = x * Set(d, kLowDivInv); + const V poly = EvalRationalPolynomial(x, p, q); + const V magnitude = + IfThenElse(x > Set(d, kThreshSRGBToLinear), poly, linear); + return AndNot(kSign, magnitude) | original_sign; + } + + template + HWY_ATTR JXL_INLINE V EncodedFromDisplay(V x) const { + const HWY_FULL(float) d; + const HWY_FULL(uint32_t) du; + const V kSign = BitCast(d, Set(du, 0x80000000u)); + const V original_sign = x & kSign; + x = AndNot(kSign, x); // abs + + // Computed via af_cheb_rational (k=100); replicated 4x. + HWY_ALIGN constexpr float p[(4 + 1) * 4] = { + -5.135152395e-04, -5.135152395e-04, -5.135152395e-04, -5.135152395e-04, + 5.287254571e-03, 5.287254571e-03, 5.287254571e-03, 5.287254571e-03, + 3.903842876e-01, 3.903842876e-01, 3.903842876e-01, 3.903842876e-01, + 1.474205315e+00, 1.474205315e+00, 1.474205315e+00, 1.474205315e+00, + 7.352629620e-01, 7.352629620e-01, 7.352629620e-01, 7.352629620e-01, + }; + HWY_ALIGN constexpr float q[(4 + 1) * 4] = { + 1.004519624e-02, 1.004519624e-02, 1.004519624e-02, 1.004519624e-02, + 3.036675394e-01, 3.036675394e-01, 3.036675394e-01, 3.036675394e-01, + 1.340816930e+00, 1.340816930e+00, 1.340816930e+00, 1.340816930e+00, + 9.258482155e-01, 9.258482155e-01, 9.258482155e-01, 9.258482155e-01, + 2.424867759e-02, 2.424867759e-02, 2.424867759e-02, 2.424867759e-02, + }; + const V linear = x * Set(d, kLowDiv); + const V poly = EvalRationalPolynomial(Sqrt(x), p, q); + const V magnitude = + IfThenElse(x > Set(d, kThreshLinearToSRGB), poly, linear); + return AndNot(kSign, magnitude) | original_sign; + } + + private: + static constexpr float kThreshSRGBToLinear = 0.04045f; + static constexpr float kThreshLinearToSRGB = 0.0031308f; + static constexpr float kLowDiv = 12.92f; + static constexpr float kLowDivInv = 1.0f / kLowDiv; +}; + +#if JPEGXL_ENABLE_SKCMS + +// NOTE: this is only used to provide a reasonable ICC profile that other +// software can read. Our own transforms use ExtraTF instead because that is +// more precise and supports unbounded mode. +template +std::vector CreateTableCurve(uint32_t N, const Func& func) { + JXL_ASSERT(N <= 4096); // ICC MFT2 only allows 4K entries + // No point using float - LCMS converts to 16-bit for A2B/MFT. + std::vector table(N); + for (uint32_t i = 0; i < N; ++i) { + const float x = static_cast(i) / (N - 1); // 1.0 at index N - 1. + // LCMS requires EOTF (e.g. 2.4 exponent). + double y = func.DisplayFromEncoded(static_cast(x)); + JXL_ASSERT(y >= 0.0); + // Clamp to table range - necessary for HLG. + if (y > 1.0) y = 1.0; + // 1.0 corresponds to table value 0xFFFF. + table[i] = static_cast(std::round(y * 65535.0)); + } + return table; +} + +void ICCComputeMD5(const PaddedBytes& data, uint8_t sum[16]) { + PaddedBytes data64 = data; + data64.push_back(128); + // Add bytes such that ((size + 8) & 63) == 0. + size_t extra = ((64 - ((data64.size() + 8) & 63)) & 63); + data64.resize(data64.size() + extra, 0); + for (uint64_t i = 0; i < 64; i += 8) { + data64.push_back((data.size() << 3u) >> i); + } + + static const uint32_t sineparts[64] = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, + 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, + 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, + 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, + 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, + 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + }; + static const uint32_t shift[64] = { + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + }; + + uint32_t a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476; + + for (size_t i = 0; i < data64.size(); i += 64) { + uint32_t a = a0, b = b0, c = c0, d = d0, f, g; + for (size_t j = 0; j < 64; j++) { + if (j < 16) { + f = (b & c) | ((~b) & d); + g = j; + } else if (j < 32) { + f = (d & b) | ((~d) & c); + g = (5 * j + 1) & 0xf; + } else if (j < 48) { + f = b ^ c ^ d; + g = (3 * j + 5) & 0xf; + } else { + f = c ^ (b | (~d)); + g = (7 * j) & 0xf; + } + uint32_t dg0 = data64[i + g * 4 + 0], dg1 = data64[i + g * 4 + 1], + dg2 = data64[i + g * 4 + 2], dg3 = data64[i + g * 4 + 3]; + uint32_t u = dg0 | (dg1 << 8u) | (dg2 << 16u) | (dg3 << 24u); + f += a + sineparts[j] + u; + a = d; + d = c; + c = b; + b += (f << shift[j]) | (f >> (32u - shift[j])); + } + a0 += a; + b0 += b; + c0 += c; + d0 += d; + } + sum[0] = a0; + sum[1] = a0 >> 8u; + sum[2] = a0 >> 16u; + sum[3] = a0 >> 24u; + sum[4] = b0; + sum[5] = b0 >> 8u; + sum[6] = b0 >> 16u; + sum[7] = b0 >> 24u; + sum[8] = c0; + sum[9] = c0 >> 8u; + sum[10] = c0 >> 16u; + sum[11] = c0 >> 24u; + sum[12] = d0; + sum[13] = d0 >> 8u; + sum[14] = d0 >> 16u; + sum[15] = d0 >> 24u; +} + +Status CreateICCChadMatrix(CIExy w, float result[9]) { + skcms_Matrix3x3 m; + JXL_RETURN_IF_ERROR(skcms_AdaptToXYZD50(w.x, w.y, &m)); + memcpy(result, m.vals, sizeof(float) * 9); + return true; +} + +// Creates RGB to XYZ matrix given RGB primaries and whitepoint in xy. +Status CreateICCRGBMatrix(CIExy r, CIExy g, CIExy b, CIExy w, float result[9]) { + skcms_Matrix3x3 m; + JXL_RETURN_IF_ERROR( + skcms_PrimariesToXYZD50(r.x, r.y, g.x, g.y, b.x, b.y, w.x, w.y, &m)); + memcpy(result, m.vals, sizeof(float) * 9); + return true; +} + +void WriteICCUint32(uint32_t value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 4) icc->resize(pos + 4); + (*icc)[pos + 0] = (value >> 24u) & 255; + (*icc)[pos + 1] = (value >> 16u) & 255; + (*icc)[pos + 2] = (value >> 8u) & 255; + (*icc)[pos + 3] = value & 255; +} + +void WriteICCUint16(uint16_t value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 2) icc->resize(pos + 2); + (*icc)[pos + 0] = (value >> 8u) & 255; + (*icc)[pos + 1] = value & 255; +} + +// Writes a 4-character tag +void WriteICCTag(const char* value, size_t pos, PaddedBytes* JXL_RESTRICT icc) { + if (icc->size() < pos + 4) icc->resize(pos + 4); + memcpy(icc->data() + pos, value, 4); +} + +void WriteICCS15Fixed16(float value, size_t pos, + PaddedBytes* JXL_RESTRICT icc) { + int32_t i = value * 65536.0f + 0.5f; + // Use two's complement + uint32_t u = static_cast(i); + WriteICCUint32(u, pos, icc); +} + +Status CreateICCHeader(const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT header) { + // TODO(lode): choose color management engine name, e.g. "skia" if + // integrated in skia. + static const char* kCmm = "jxl "; + + header->resize(128, 0); + + WriteICCUint32(0, 0, header); // size, correct value filled in at end + WriteICCTag(kCmm, 4, header); + WriteICCUint32(0x04300000u, 8, header); + WriteICCTag("mntr", 12, header); + WriteICCTag(c.IsGray() ? "GRAY" : "RGB ", 16, header); + WriteICCTag("XYZ ", 20, header); + + // Three uint32_t's date/time encoding. + // TODO(lode): encode actual date and time, this is a placeholder + uint32_t year = 2019, month = 12, day = 1; + uint32_t hour = 0, minute = 0, second = 0; + WriteICCUint16(year, 24, header); + WriteICCUint16(month, 26, header); + WriteICCUint16(day, 28, header); + WriteICCUint16(hour, 30, header); + WriteICCUint16(minute, 32, header); + WriteICCUint16(second, 34, header); + + WriteICCTag("acsp", 36, header); + WriteICCTag("APPL", 40, header); + WriteICCUint32(0, 44, header); // flags + WriteICCUint32(0, 48, header); // device manufacturer + WriteICCUint32(0, 52, header); // device model + WriteICCUint32(0, 56, header); // device attributes + WriteICCUint32(0, 60, header); // device attributes + WriteICCUint32(static_cast(c.rendering_intent), 64, header); + + // Mandatory D50 white point of profile connection space + WriteICCUint32(0x0000f6d6, 68, header); + WriteICCUint32(0x00010000, 72, header); + WriteICCUint32(0x0000d32d, 76, header); + + WriteICCTag(kCmm, 80, header); + + return true; +} + +void AddToICCTagTable(const char* tag, size_t offset, size_t size, + PaddedBytes* JXL_RESTRICT tagtable, + std::vector* offsets) { + WriteICCTag(tag, tagtable->size(), tagtable); + // writing true offset deferred to later + WriteICCUint32(0, tagtable->size(), tagtable); + offsets->push_back(offset); + WriteICCUint32(size, tagtable->size(), tagtable); +} + +void FinalizeICCTag(PaddedBytes* JXL_RESTRICT tags, size_t* offset, + size_t* size) { + while ((tags->size() & 3) != 0) { + tags->push_back(0); + } + *offset += *size; + *size = tags->size() - *offset; +} + +// The input text must be ASCII, writing other characters to UTF-16 is not +// implemented. +void CreateICCMlucTag(const std::string& text, PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("mluc", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + WriteICCUint32(1, tags->size(), tags); + WriteICCUint32(12, tags->size(), tags); + WriteICCTag("enUS", tags->size(), tags); + WriteICCUint32(text.size() * 2, tags->size(), tags); + WriteICCUint32(28, tags->size(), tags); + for (size_t i = 0; i < text.size(); i++) { + tags->push_back(0); // prepend 0 for UTF-16 + tags->push_back(text[i]); + } +} + +void CreateICCXYZTag(float xyz[3], PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("XYZ ", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + WriteICCS15Fixed16(xyz[0], tags->size(), tags); + WriteICCS15Fixed16(xyz[1], tags->size(), tags); + WriteICCS15Fixed16(xyz[2], tags->size(), tags); +} + +void CreateICCChadTag(float chad[9], PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("sf32", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + for (size_t i = 0; i < 9; i++) { + WriteICCS15Fixed16(chad[i], tags->size(), tags); + } +} + +void CreateICCCurvCurvTag(const std::vector& curve, + PaddedBytes* JXL_RESTRICT tags) { + size_t pos = tags->size(); + tags->resize(tags->size() + 12 + curve.size() * 2, 0); + WriteICCTag("curv", pos, tags); + WriteICCUint32(0, pos + 4, tags); + WriteICCUint32(curve.size(), pos + 8, tags); + for (size_t i = 0; i < curve.size(); i++) { + WriteICCUint16(curve[i], pos + 12 + i * 2, tags); + } +} + +void CreateICCCurvParaTag(std::vector params, size_t curve_type, + PaddedBytes* JXL_RESTRICT tags) { + WriteICCTag("para", tags->size(), tags); + WriteICCUint32(0, tags->size(), tags); + WriteICCUint16(curve_type, tags->size(), tags); + WriteICCUint16(0, tags->size(), tags); + for (size_t i = 0; i < params.size(); i++) { + WriteICCS15Fixed16(params[i], tags->size(), tags); + } +} + +Status MaybeCreateProfile(const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT icc) { + PaddedBytes header, tagtable, tags; + + // Unknown color_space/transfer_function. + if (c.opaque_icc) return false; // Not an error + + switch (c.GetColorSpace()) { + case ColorSpace::kRGB: + case ColorSpace::kGray: + break; // OK + case ColorSpace::kXYB: + return JXL_FAILURE("XYB ICC not yet implemented"); + case ColorSpace::kUnknown: + default: + return JXL_FAILURE("Unknown CS despite !opaque_icc"); + } + + JXL_RETURN_IF_ERROR(CreateICCHeader(c, &header)); + + std::vector offsets; + // tag count, deferred to later + WriteICCUint32(0, tagtable.size(), &tagtable); + + size_t tag_offset = 0, tag_size = 0; + + CreateICCMlucTag(Description(c), &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("desc", tag_offset, tag_size, &tagtable, &offsets); + + const std::string copyright = + "Copyright 2019 Google LLC, CC-BY-SA 3.0 Unported " + "license(https://creativecommons.org/licenses/by-sa/3.0/legalcode)"; + CreateICCMlucTag(copyright, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("cprt", tag_offset, tag_size, &tagtable, &offsets); + + float d50[3] = {0.964203, 1.0, 0.824905}; + float wtpt[3]; + CIEXYZFromWhiteCIExy(c.GetWhitePoint(), wtpt); + CreateICCXYZTag(c.IsGray() ? wtpt : d50, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("wtpt", tag_offset, tag_size, &tagtable, &offsets); + + if (!c.IsGray()) { + // Chromatic adaptation matrix + float chad[9]; + JXL_RETURN_IF_ERROR(CreateICCChadMatrix(c.GetWhitePoint(), chad)); + + const PrimariesCIExy primaries = c.GetPrimaries(); + float m[9]; + JXL_RETURN_IF_ERROR(CreateICCRGBMatrix(primaries.r, primaries.g, + primaries.b, c.GetWhitePoint(), m)); + float r[3] = {m[0], m[3], m[6]}; + float g[3] = {m[1], m[4], m[7]}; + float b[3] = {m[2], m[5], m[8]}; + + CreateICCChadTag(chad, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("chad", tag_offset, tag_size, &tagtable, &offsets); + + CreateICCXYZTag(r, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("rXYZ", tag_offset, tag_size, &tagtable, &offsets); + + CreateICCXYZTag(g, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("gXYZ", tag_offset, tag_size, &tagtable, &offsets); + + CreateICCXYZTag(b, &tags); + FinalizeICCTag(&tags, &tag_offset, &tag_size); + AddToICCTagTable("bXYZ", tag_offset, tag_size, &tagtable, &offsets); + } + + if (c.tf.IsGamma()) { + float gamma = 1.0 / c.tf.GetGamma(); + CreateICCCurvParaTag({gamma, 1.0, 0.0, 1.0, 0.0}, 3, &tags); + } else { + switch (c.tf.GetTransferFunction()) { + case TransferFunction::kHLG: + CreateICCCurvCurvTag(CreateTableCurve(4096, TF_HLG()), &tags); + break; + case TransferFunction::kPQ: + CreateICCCurvCurvTag(CreateTableCurve(4096, TF_PQ()), &tags); + break; + case TransferFunction::kSRGB: + CreateICCCurvParaTag( + {2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045}, 3, &tags); + break; + case TransferFunction::k709: + CreateICCCurvParaTag( + {1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081}, 3, + &tags); + break; + case TransferFunction::kLinear: + CreateICCCurvParaTag({1.0, 1.0, 0.0, 1.0, 0.0}, 3, &tags); + break; + case TransferFunction::kDCI: + CreateICCCurvParaTag({2.6, 1.0, 0.0, 1.0, 0.0}, 3, &tags); + break; + case TransferFunction::kUnknown: + default: + JXL_ABORT("Unknown TF despite !opaque_icc"); + } + } + FinalizeICCTag(&tags, &tag_offset, &tag_size); + if (c.IsGray()) { + AddToICCTagTable("kTRC", tag_offset, tag_size, &tagtable, &offsets); + } else { + AddToICCTagTable("rTRC", tag_offset, tag_size, &tagtable, &offsets); + AddToICCTagTable("gTRC", tag_offset, tag_size, &tagtable, &offsets); + AddToICCTagTable("bTRC", tag_offset, tag_size, &tagtable, &offsets); + } + + // Tag count + WriteICCUint32(offsets.size(), 0, &tagtable); + for (size_t i = 0; i < offsets.size(); i++) { + WriteICCUint32(offsets[i] + header.size() + tagtable.size(), 4 + 12 * i + 4, + &tagtable); + } + + // ICC profile size + WriteICCUint32(header.size() + tagtable.size() + tags.size(), 0, &header); + + *icc = header; + icc->append(tagtable); + icc->append(tags); + + // The MD5 checksum must be computed on the profile with profile flags, + // rendering intent, and region of the checksum itself, set to 0. + // TODO(lode): manually verify with a reliable tool that this creates correct + // signature (profile id) for ICC profiles. + PaddedBytes icc_sum = *icc; + memset(icc_sum.data() + 44, 0, 4); + memset(icc_sum.data() + 64, 0, 4); + uint8_t checksum[16]; + ICCComputeMD5(icc_sum, checksum); + + memcpy(icc->data() + 84, checksum, sizeof(checksum)); + + return true; +} + +Status MaybeCreateProfile(const ColorEncoding& c, skcms_ICCProfile* profile, + PaddedBytes* bytes) { + // Unknown color_space/transfer_function. + if (c.opaque_icc) return false; // Not an error + + JXL_RETURN_IF_ERROR(MaybeCreateProfile(c, bytes)); + JXL_RETURN_IF_ERROR(skcms_Parse(bytes->data(), bytes->size(), profile)); + + return true; +} + +#else // JPEGXL_ENABLE_SKCMS + +// NOTE: this is only used to provide a reasonable ICC profile that other +// software can read. Our own transforms use ExtraTF instead because that is +// more precise and supports unbounded mode. +template +cmsToneCurve* CreateTableCurve(const cmsContext context, uint32_t N, + const Func& func) { + JXL_ASSERT(N <= 4096); // ICC MFT2 only allows 4K entries + // No point using float - LCMS converts to 16-bit for A2B/MFT. + std::vector table; + table.reserve(N); + for (uint32_t i = 0; i < N; ++i) { + const float x = static_cast(i) / (N - 1); // 1.0 at index N - 1. + // LCMS requires EOTF (e.g. 2.4 exponent). + double y = func.DisplayFromEncoded(static_cast(x)); + JXL_ASSERT(y >= 0.0); + // Clamp to table range - necessary for HLG. + if (y > 1.0) y = 1.0; + // 1.0 corresponds to table value 0xFFFF. + table.push_back(static_cast(std::round(y * 65535.0))); + } + return cmsBuildTabulatedToneCurve16(context, N, table.data()); +} + +Curve CreateCurve(const cmsContext context, const ColorEncoding& c) { + // Exponential with linear part. Note that the LittleCMS API reference and + // tutorial disagree on the type number. + const cmsUInt32Number type = 4; + // First is EOTF exponent as expected by LCMS (c.GetGamma is the OETF, so take + // the reciprocal). Then a,b,c,d: (a*x + b)^gamma, or c*x if x < d. + std::array params; + + if (c.tf.IsGamma()) { + params = {1.0 / c.tf.GetGamma(), 1.0, 0.0, 1.0, 0.0}; + return Curve(cmsBuildParametricToneCurve(context, type, params.data())); + } + switch (c.tf.GetTransferFunction()) { + case TransferFunction::kHLG: + return Curve(CreateTableCurve(context, 4096, TF_HLG())); + case TransferFunction::kPQ: + return Curve(CreateTableCurve(context, 4096, TF_PQ())); + + case TransferFunction::kSRGB: + params = {2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045}; + break; + case TransferFunction::k709: + params = {1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081}; + break; + case TransferFunction::kLinear: + params = {1.0, 1.0, 0.0, 1.0, 0.0}; + break; + case TransferFunction::kDCI: + params = {2.6, 1.0, 0.0, 1.0, 0.0}; + break; + case TransferFunction::kUnknown: + JXL_ABORT("Unknown TF despite !opaque_icc"); + } + // WARNING: using cmsBuildGamma results in a bounded curve - LittleCMS + // clamps negative outputs to zero. To retain unbounded mode, we use the + // same parametric curve type as sRGB. + return Curve(cmsBuildParametricToneCurve(context, type, params.data())); +} + +Profile MaybeCreateProfileRGB(const cmsContext context, const ColorEncoding& c, + const cmsCIExyY& wp_xyY, const Curve& curve) { + const PrimariesCIExy primaries = c.GetPrimaries(); + const cmsCIExyYTRIPLE primaries_xyY = {xyYFromCIExy(primaries.r), + xyYFromCIExy(primaries.g), + xyYFromCIExy(primaries.b)}; + cmsToneCurve* curves[3] = {curve.get(), curve.get(), curve.get()}; + return Profile( + cmsCreateRGBProfileTHR(context, &wp_xyY, &primaries_xyY, curves)); +} + +// Serializes the profile before use to ensure all values are quantized. +Status MaybeCreateProfile(const cmsContext context, const ColorEncoding& c, + PaddedBytes* JXL_RESTRICT icc) { + // Unknown color_space/transfer_function. + if (c.opaque_icc) return false; // Not an error + + // (If color_space == kRGB, we'll use this curve for all channels.) + const Curve curve = CreateCurve(context, c); + if (curve == nullptr) return JXL_FAILURE("Failed to create curve"); + + const cmsCIExyY wp_xyY = xyYFromCIExy(c.GetWhitePoint()); + + Profile profile; + switch (c.GetColorSpace()) { + case ColorSpace::kRGB: + profile = MaybeCreateProfileRGB(context, c, wp_xyY, curve); + break; + case ColorSpace::kGray: + profile.reset(cmsCreateGrayProfileTHR(context, &wp_xyY, curve.get())); + break; + case ColorSpace::kXYB: + JXL_FAILURE("XYB ICC not yet implemented"); + break; + case ColorSpace::kUnknown: + return JXL_FAILURE("Unknown CS despite !opaque_icc"); + } + if (profile.get() == nullptr) { + return JXL_FAILURE("Failed to create profile (cs = %u)", c.GetColorSpace()); + } + + // ICC uses the same values. + cmsSetHeaderRenderingIntent(profile.get(), + static_cast(c.rendering_intent)); + + return EncodeProfile(context, profile, Description(c), icc); +} + +#endif // JPEGXL_ENABLE_SKCMS + +#if JPEGXL_ENABLE_SKCMS + +// Sets *opaque_icc = true if color space is unknown. +ColorSpace ColorSpaceFromProfile(const skcms_ICCProfile& profile, + bool* opaque_icc) { + switch (profile.data_color_space) { + case skcms_Signature_RGB: + return ColorSpace::kRGB; + case skcms_Signature_Gray: + return ColorSpace::kGray; + default: + *opaque_icc = true; + return ColorSpace::kUnknown; + } +} + +// "profile1" is pre-decoded to save time in DetectTransferFunction. +Status ProfileEquivalentToICC(const skcms_ICCProfile& profile1, + const PaddedBytes& icc) { + skcms_ICCProfile profile2; + JXL_RETURN_IF_ERROR(skcms_Parse(icc.data(), icc.size(), &profile2)); + return skcms_ApproximatelyEqualProfiles(&profile1, &profile2); +} + +// vector_out := matmul(matrix, vector_in) +void MatrixProduct(const skcms_Matrix3x3& matrix, const float vector_in[3], + float vector_out[3]) { + for (int i = 0; i < 3; ++i) { + vector_out[i] = 0; + for (int j = 0; j < 3; ++j) { + vector_out[i] += matrix.vals[i][j] * vector_in[j]; + } + } +} + +// Returns white point that was specified when creating the profile. +JXL_MUST_USE_RESULT CIExy UnadaptedWhitePoint(const skcms_ICCProfile& profile) { + float media_white_point_XYZ[3]; + JXL_CHECK(skcms_GetWTPT(&profile, media_white_point_XYZ)); + skcms_Matrix3x3 CHAD; + if (!skcms_GetCHAD(&profile, &CHAD)) { + // If there is no chromatic adaptation matrix, it means that the white point + // is already unadapted. + return CIExyFromXYZ(media_white_point_XYZ); + } + // Otherwise, it has been adapted to the PCS white point using said matrix, + // and the adaptation needs to be undone. + skcms_Matrix3x3 inverse_CHAD; + JXL_CHECK(skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)); + float unadapted_white_point_XYZ[3]; + MatrixProduct(inverse_CHAD, media_white_point_XYZ, unadapted_white_point_XYZ); + return CIExyFromXYZ(unadapted_white_point_XYZ); +} + +Status IdentifyPrimaries(const skcms_ICCProfile& profile, + const CIExy& wp_unadapted, ColorEncoding* c) { + if (!c->HasPrimaries()) return true; + + skcms_Matrix3x3 CHAD, inverse_CHAD; + if (skcms_GetCHAD(&profile, &CHAD)) { + JXL_RETURN_IF_ERROR(skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)); + } else { + static constexpr skcms_Matrix3x3 kLMSFromXYZ = { + {{0.8951, 0.2664, -0.1614}, + {-0.7502, 1.7135, 0.0367}, + {0.0389, -0.0685, 1.0296}}}; + static constexpr skcms_Matrix3x3 kXYZFromLMS = { + {{0.9869929, -0.1470543, 0.1599627}, + {0.4323053, 0.5183603, 0.0492912}, + {-0.0085287, 0.0400428, 0.9684867}}}; + static constexpr float kWpD50XYZ[3] = {0.96420288, 1.0, 0.82490540}; + float wp_unadapted_XYZ[3]; + CIEXYZFromWhiteCIExy(wp_unadapted, wp_unadapted_XYZ); + float wp_D50_LMS[3], wp_unadapted_LMS[3]; + MatrixProduct(kLMSFromXYZ, kWpD50XYZ, wp_D50_LMS); + MatrixProduct(kLMSFromXYZ, wp_unadapted_XYZ, wp_unadapted_LMS); + inverse_CHAD = {{{wp_unadapted_LMS[0] / wp_D50_LMS[0], 0, 0}, + {0, wp_unadapted_LMS[1] / wp_D50_LMS[1], 0}, + {0, 0, wp_unadapted_LMS[2] / wp_D50_LMS[2]}}}; + inverse_CHAD = skcms_Matrix3x3_concat(&kXYZFromLMS, &inverse_CHAD); + inverse_CHAD = skcms_Matrix3x3_concat(&inverse_CHAD, &kLMSFromXYZ); + } + + float XYZ[3]; + PrimariesCIExy primaries; + CIExy* const chromaticities[] = {&primaries.r, &primaries.g, &primaries.b}; + for (int i = 0; i < 3; ++i) { + float RGB[3] = {}; + RGB[i] = 1; + skcms_Transform(RGB, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque, + &profile, XYZ, skcms_PixelFormat_RGB_fff, + skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1); + float unadapted_XYZ[3]; + MatrixProduct(inverse_CHAD, XYZ, unadapted_XYZ); + *chromaticities[i] = CIExyFromXYZ(unadapted_XYZ); + } + return c->SetPrimaries(primaries); +} + +void DetectTransferFunction(const skcms_ICCProfile& profile, + ColorEncoding* JXL_RESTRICT c) { + if (c->tf.SetImplicit()) return; + + for (TransferFunction tf : Values()) { + // Can only create profile from known transfer function. + if (tf == TransferFunction::kUnknown) continue; + + c->tf.SetTransferFunction(tf); + + skcms_ICCProfile profile_test; + PaddedBytes bytes; + if (MaybeCreateProfile(*c, &profile_test, &bytes) && + skcms_ApproximatelyEqualProfiles(&profile, &profile_test)) { + return; + } + } + + c->tf.SetTransferFunction(TransferFunction::kUnknown); + c->opaque_icc = true; +} + +#else // JPEGXL_ENABLE_SKCMS + +uint32_t Type32(const ColorEncoding& c) { + if (c.IsGray()) return TYPE_GRAY_FLT; + return TYPE_RGB_FLT; +} + +uint32_t Type64(const ColorEncoding& c) { + if (c.IsGray()) return TYPE_GRAY_DBL; + return TYPE_RGB_DBL; +} + +// Sets *opaque_icc = true if color space is unknown. +ColorSpace ColorSpaceFromProfile(const Profile& profile, bool* opaque_icc) { + switch (cmsGetColorSpace(profile.get())) { + case cmsSigRgbData: + return ColorSpace::kRGB; + case cmsSigGrayData: + return ColorSpace::kGray; + default: + *opaque_icc = true; + return ColorSpace::kUnknown; + } +} + +// "profile1" is pre-decoded to save time in DetectTransferFunction. +Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1, + const PaddedBytes& icc, const ColorEncoding& c) { + const uint32_t type_src = Type64(c); + + Profile profile2; + JXL_RETURN_IF_ERROR(DecodeProfile(context, icc, &profile2)); + + Profile profile_xyz; + JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz)); + + const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC; + const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION | + cmsFLAGS_HIGHRESPRECALC; + Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src, + profile_xyz.get(), TYPE_XYZ_DBL, + intent, flags)); + Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src, + profile_xyz.get(), TYPE_XYZ_DBL, + intent, flags)); + if (xform1 == nullptr || xform2 == nullptr) { + return JXL_FAILURE("Failed to create transform"); + } + + double in[3]; + double out1[3]; + double out2[3]; + + // Uniformly spaced samples from very dark to almost fully bright. + const double init = 1E-3; + const double step = 0.2; + + if (c.IsGray()) { + // Finer sampling and replicate each component. + for (in[0] = init; in[0] < 1.0; in[0] += step / 8) { + cmsDoTransform(xform1.get(), in, out1, 1); + cmsDoTransform(xform2.get(), in, out2, 1); + if (!ApproxEq(out1[0], out2[0], 2E-4)) { + return false; + } + } + } else { + for (in[0] = init; in[0] < 1.0; in[0] += step) { + for (in[1] = init; in[1] < 1.0; in[1] += step) { + for (in[2] = init; in[2] < 1.0; in[2] += step) { + cmsDoTransform(xform1.get(), in, out1, 1); + cmsDoTransform(xform2.get(), in, out2, 1); + for (size_t i = 0; i < 3; ++i) { + if (!ApproxEq(out1[i], out2[i], 2E-4)) { + return false; + } + } + } + } + } + } + + return true; +} + +// Returns white point that was specified when creating the profile. +// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation +// differs between ICC versions. +JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, + const Profile& profile, + const ColorEncoding& c) { + cmsCIEXYZ XYZ = {1.0, 1.0, 1.0}; + + Profile profile_xyz; + if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ; + // Array arguments are one per profile. + cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; + // Leave white point unchanged - that is what we're trying to extract. + cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC, + INTENT_ABSOLUTE_COLORIMETRIC}; + cmsBool black_compensation[2] = {0, 0}; + cmsFloat64Number adaption[2] = {0.0, 0.0}; + // Only transforming a single pixel, so skip expensive optimizations. + cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC; + Transform xform(cmsCreateExtendedTransform( + context, 2, profiles, black_compensation, intents, adaption, nullptr, 0, + Type64(c), TYPE_XYZ_DBL, flags)); + if (!xform) return XYZ; // TODO(lode): return error + + // xy are relative, so magnitude does not matter if we ignore output Y. + const cmsFloat64Number in[3] = {1.0, 1.0, 1.0}; + cmsDoTransform(xform.get(), in, &XYZ.X, 1); + return XYZ; +} + +Status IdentifyPrimaries(const Profile& profile, const cmsCIEXYZ& wp_unadapted, + ColorEncoding* c) { + if (!c->HasPrimaries()) return true; + + // These were adapted to the profile illuminant before storing in the profile. + const cmsCIEXYZ* adapted_r = static_cast( + cmsReadTag(profile.get(), cmsSigRedColorantTag)); + const cmsCIEXYZ* adapted_g = static_cast( + cmsReadTag(profile.get(), cmsSigGreenColorantTag)); + const cmsCIEXYZ* adapted_b = static_cast( + cmsReadTag(profile.get(), cmsSigBlueColorantTag)); + if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) { + return JXL_FAILURE("Failed to retrieve colorants"); + } + + // TODO(janwas): no longer assume Bradford and D50. + // Undo the chromatic adaptation. + const cmsCIEXYZ d50 = D50_XYZ(); + + cmsCIEXYZ r, g, b; + cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r); + cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g); + cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b); + + const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g), + CIExyFromXYZ(b)}; + return c->SetPrimaries(rgb); +} + +void DetectTransferFunction(const cmsContext context, const Profile& profile, + ColorEncoding* JXL_RESTRICT c) { + if (c->tf.SetImplicit()) return; + + for (TransferFunction tf : Values()) { + // Can only create profile from known transfer function. + if (tf == TransferFunction::kUnknown) continue; + + c->tf.SetTransferFunction(tf); + + PaddedBytes icc_test; + if (MaybeCreateProfile(context, *c, &icc_test) && + ProfileEquivalentToICC(context, profile, icc_test, *c)) { + return; + } + } + + c->tf.SetTransferFunction(TransferFunction::kUnknown); + c->opaque_icc = true; +} + +void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) { + JXL_WARNING("LCMS error %u: %s", code, text); +} + +// Returns a context for the current thread, creating it if necessary. +cmsContext GetContext() { + static thread_local void* context_; + if (context_ == nullptr) { + context_ = cmsCreateContext(nullptr, nullptr); + JXL_ASSERT(context_ != nullptr); + + cmsSetLogErrorHandlerTHR(static_cast(context_), &ErrorHandler); + } + return static_cast(context_); +} + +#endif // JPEGXL_ENABLE_SKCMS + +} // namespace + +// All functions that call lcms directly (except ColorSpaceTransform::Run) must +// lock lcms_mutex. + +Status ColorManagement::SetProfile(PaddedBytes&& icc, + ColorEncoding* JXL_RESTRICT c) { + c->icc.clear(); + if (icc.empty()) return false; + +#if JPEGXL_ENABLE_SKCMS + skcms_ICCProfile profile; + JXL_RETURN_IF_ERROR(skcms_Parse(icc.data(), icc.size(), &profile)); + + // skcms does not return the rendering intent, so get it from the file. It + // is encoded as big-endian 32-bit integer in bytes 60..63. + if (icc.size() < 128) { + return JXL_FAILURE("ICC file too small"); + } + uint32_t rendering_intent = icc[67]; + if (rendering_intent > 3 || icc[64] != 0 || icc[65] != 0 || icc[66] != 0) { + return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent); + } + + c->icc = std::move(icc); + c->received_icc = true; + c->opaque_icc = false; // May be set to true below. + + c->SetColorSpace(ColorSpaceFromProfile(profile, &c->opaque_icc)); + + const CIExy wp_unadapted = UnadaptedWhitePoint(profile); + JXL_RETURN_IF_ERROR(c->SetWhitePoint(wp_unadapted)); + + // Relies on color_space. + JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, c)); + + // Relies on color_space/white point/primaries being set already. + DetectTransferFunction(profile, c); + // ICC and RenderingIntent have the same values (0..3). + c->rendering_intent = static_cast(rendering_intent); +#else // JPEGXL_ENABLE_SKCMS + + std::lock_guard guard(lcms_mutex); + const cmsContext context = GetContext(); + + Profile profile; + JXL_RETURN_IF_ERROR(DecodeProfile(context, icc, &profile)); + + const cmsUInt32Number rendering_intent = + cmsGetHeaderRenderingIntent(profile.get()); + if (rendering_intent > 3) { + return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent); + } + + c->icc = std::move(icc); + c->received_icc = true; + c->opaque_icc = false; // May be set to true below. + + c->SetColorSpace(ColorSpaceFromProfile(profile, &c->opaque_icc)); + + const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, *c); + JXL_RETURN_IF_ERROR(c->SetWhitePoint(CIExyFromXYZ(wp_unadapted))); + + // Relies on color_space. + JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, c)); + + // Relies on color_space/white point/primaries being set already. + DetectTransferFunction(context, profile, c); + + // ICC and RenderingIntent have the same values (0..3). + c->rendering_intent = static_cast(rendering_intent); +#endif // JPEGXL_ENABLE_SKCMS + + if (!c->opaque_icc) { + // Ensure that it is safe to skip the profile, i.e. the reconstructed + // profile will be equivalent: + PaddedBytes icc_new; +#if JPEGXL_ENABLE_SKCMS + JXL_RETURN_IF_ERROR(MaybeCreateProfile(*c, &icc_new)); + if (!ProfileEquivalentToICC(profile, icc_new)) { +#else // JPEGXL_ENABLE_SKCMS + JXL_RETURN_IF_ERROR(MaybeCreateProfile(context, *c, &icc_new)); + if (!ProfileEquivalentToICC(context, profile, icc_new, *c)) { +#endif // JPEGXL_ENABLE_SKCMS + return JXL_FAILURE("Generated profile does not match"); + } + } + + return true; +} + +Status ColorManagement::CreateProfile(ColorEncoding* JXL_RESTRICT c) { + std::lock_guard guard(lcms_mutex); + c->icc.clear(); + c->received_icc = c->opaque_icc = false; + +#if JPEGXL_ENABLE_SKCMS + if (!MaybeCreateProfile(*c, &c->icc)) { +#else // JPEGXL_ENABLE_SKCMS + const cmsContext context = GetContext(); + if (!MaybeCreateProfile(context, *c, &c->icc)) { +#endif // JPEGXL_ENABLE_SKCMS + return JXL_FAILURE("Failed to create profile from fields"); + } + return true; +} + +namespace { + +void InitC2Once(const Primaries pr, const TransferFunction tf, + std::atomic* initialized, + std::array* c2) { + // Thread-safe double-checked locking. + int done = initialized->load(std::memory_order_acquire); + if (done == 1) return; + static std::mutex mutex; + std::lock_guard lock(mutex); + done = initialized->load(std::memory_order_relaxed); + if (done == 1) return; + + { + ColorEncoding* c_rgb = c2->data() + 0; + c_rgb->SetColorSpace(ColorSpace::kRGB); + c_rgb->white_point = WhitePoint::kD65; + c_rgb->primaries = pr; + c_rgb->tf.SetTransferFunction(tf); + JXL_CHECK(ColorManagement::CreateProfile(c_rgb)); + } + + { + ColorEncoding* c_gray = c2->data() + 1; + c_gray->SetColorSpace(ColorSpace::kGray); + c_gray->white_point = WhitePoint::kD65; + c_gray->primaries = pr; + c_gray->tf.SetTransferFunction(tf); + JXL_CHECK(ColorManagement::CreateProfile(c_gray)); + } + + initialized->store(1, std::memory_order_release); +} + +} // namespace + +const ColorEncoding& ColorManagement::SRGB(bool is_gray) { + static std::atomic initialized{0}; + static std::array c2; + InitC2Once(Primaries::kSRGB, TransferFunction::kSRGB, &initialized, &c2); + return c2[is_gray]; +} +const ColorEncoding& ColorManagement::LinearSRGB(bool is_gray) { + static std::atomic initialized{0}; + static std::array c2; + InitC2Once(Primaries::kSRGB, TransferFunction::kLinear, &initialized, &c2); + return c2[is_gray]; +} + +ColorSpaceTransform::~ColorSpaceTransform() { +#if !JPEGXL_ENABLE_SKCMS + std::lock_guard guard(lcms_mutex); + for (void* p : transforms_) { + TransformDeleter()(p); + } +#endif +} + +Status ColorSpaceTransform::Init(const ColorEncoding& c_src, + const ColorEncoding& c_dst, size_t xsize, + const size_t num_threads) { + std::lock_guard guard(lcms_mutex); +#if JXL_CMS_VERBOSE + printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str()); +#endif + +#if JPEGXL_ENABLE_SKCMS + icc_src_ = c_src.icc; + icc_dst_ = c_dst.icc; + JXL_RETURN_IF_ERROR(DecodeProfile(icc_src_, &profile_src_)); + JXL_RETURN_IF_ERROR(DecodeProfile(icc_dst_, &profile_dst_)); +#else // JPEGXL_ENABLE_SKCMS + const cmsContext context = GetContext(); + Profile profile_src, profile_dst; + JXL_RETURN_IF_ERROR(DecodeProfile(context, c_src.icc, &profile_src)); + JXL_RETURN_IF_ERROR(DecodeProfile(context, c_dst.icc, &profile_dst)); +#endif // JPEGXL_ENABLE_SKCMS + + skip_lcms_ = false; + if (c_src.SameColorEncoding(c_dst)) { + skip_lcms_ = true; +#if JXL_CMS_VERBOSE + printf("Skip CMS\n"); +#endif + } + + // Special-case for BT.2100 HLG/PQ and SRGB <=> linear: + const bool src_linear = c_src.tf.IsLinear(); + const bool dst_linear = c_dst.tf.IsLinear(); + if (((c_src.tf.IsPQ() || c_src.tf.IsHLG()) && dst_linear) || + ((c_dst.tf.IsPQ() || c_dst.tf.IsHLG()) && src_linear) || + (c_src.tf.IsSRGB() && dst_linear) || (c_dst.tf.IsSRGB() && src_linear)) { + // Construct new profiles as if the data were already/still linear. + ColorEncoding c_linear_src = c_src; + ColorEncoding c_linear_dst = c_dst; + c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear); + c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear); + PaddedBytes icc_src, icc_dst; +#if JPEGXL_ENABLE_SKCMS + skcms_ICCProfile new_src, new_dst; +#else // JPEGXL_ENABLE_SKCMS + Profile new_src, new_dst; +#endif // JPEGXL_ENABLE_SKCMS + // Only enable ExtraTF if profile creation succeeded. +#if JPEGXL_ENABLE_SKCMS + if (MaybeCreateProfile(c_linear_src, &new_src, &icc_src) && + MaybeCreateProfile(c_linear_dst, &new_dst, &icc_dst)) { +#else // JPEGXL_ENABLE_SKCMS + if (MaybeCreateProfile(context, c_linear_src, &icc_src) && + MaybeCreateProfile(context, c_linear_dst, &icc_dst) && + DecodeProfile(context, icc_src, &new_src) && + DecodeProfile(context, icc_dst, &new_dst)) { +#endif // JPEGXL_ENABLE_SKCMS + if (c_src.SameColorSpace(c_dst)) { + skip_lcms_ = true; + } +#if JXL_CMS_VERBOSE + printf("Special linear <-> HLG/PQ/sRGB; skip=%d\n", skip_lcms_); +#endif +#if JPEGXL_ENABLE_SKCMS + icc_src_ = PaddedBytes(); + profile_src_ = new_src; + icc_dst_ = PaddedBytes(); + profile_dst_ = new_dst; +#else // JPEGXL_ENABLE_SKCMS + profile_src.swap(new_src); + profile_dst.swap(new_dst); +#endif // JPEGXL_ENABLE_SKCMS + if (c_dst.tf.IsLinear()) { + preprocess_ = c_src.tf.IsSRGB() + ? ExtraTF::kSRGB + : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); + } else { + JXL_ASSERT(c_src.tf.IsLinear()); + postprocess_ = c_dst.tf.IsSRGB() + ? ExtraTF::kSRGB + : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); + } + } else { + JXL_WARNING("Failed to create extra linear profiles"); + } + } + +#if JPEGXL_ENABLE_SKCMS + if (!skcms_MakeUsableAsDestination(&profile_dst_)) { + return JXL_FAILURE( + "Failed to make %s usable as a color transform destination", + Description(c_dst).c_str()); + } +#endif // JPEGXL_ENABLE_SKCMS + + // Not including alpha channel (copied separately). + const size_t channels_src = c_src.Channels(); + const size_t channels_dst = c_dst.Channels(); + JXL_CHECK(channels_src == channels_dst); +#if JXL_CMS_VERBOSE + printf("Channels: %zu; Threads: %zu\n", channels_src, num_threads); +#endif + +#if !JPEGXL_ENABLE_SKCMS + // Type includes color space (XYZ vs RGB), so can be different. + const uint32_t type_src = Type32(c_src); + const uint32_t type_dst = Type32(c_dst); + transforms_.clear(); + for (size_t i = 0; i < num_threads; ++i) { + const uint32_t intent = static_cast(c_dst.rendering_intent); + const uint32_t flags = + cmsFLAGS_BLACKPOINTCOMPENSATION | cmsFLAGS_HIGHRESPRECALC; + // NOTE: we're using the current thread's context and assuming all state + // modified by cmsDoTransform resides in the transform, not the context. + transforms_.emplace_back(cmsCreateTransformTHR(context, profile_src.get(), + type_src, profile_dst.get(), + type_dst, intent, flags)); + if (transforms_.back() == nullptr) { + return JXL_FAILURE("Failed to create transform"); + } + } +#endif // !JPEGXL_ENABLE_SKCMS + + // Ideally LCMS would convert directly from External to Image3. However, + // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our + // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must + // be interleaved. Calling cmsDoTransform for each pixel is expensive + // (indirect call). We therefore transform rows, which requires per-thread + // buffers. To avoid separate allocations, we use the rows of an image. + // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit + // outputs (or vice versa), we use floating point input/output. + buf_src_ = ImageF(xsize * channels_src, num_threads); + buf_dst_ = ImageF(xsize * channels_dst, num_threads); + xsize_ = xsize; + return true; +} + +HWY_ATTR void ColorSpaceTransform::Run(const size_t thread, + const float* buf_src, float* buf_dst) { + // No lock needed. + + // If ExtraTF, we need a writable buffer; otherwise, only READ from buf_src. + float* const xform_src = (preprocess_ == ExtraTF::kNone) + ? const_cast(buf_src) + : buf_src_.Row(thread); + +#if JXL_CMS_VERBOSE >= 2 + const size_t kX = 0; // pixel index, multiplied by 3 for RGB +#endif + + switch (preprocess_) { + case ExtraTF::kNone: + break; + case ExtraTF::kPQ: + for (size_t i = 0; i < buf_src_.xsize(); ++i) { + xform_src[i] = static_cast( + TF_PQ().DisplayFromEncoded(static_cast(buf_src[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoPQ %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + case ExtraTF::kHLG: + for (size_t i = 0; i < buf_src_.xsize(); ++i) { + xform_src[i] = static_cast( + TF_HLG().DisplayFromEncoded(static_cast(buf_src[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoHLG %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + case ExtraTF::kSRGB: + HWY_FULL(float) df; + for (size_t i = 0; i < buf_src_.xsize(); i += df.N) { + const auto val = Load(df, buf_src + i); + const auto result = TF_SRGB().DisplayFromEncoded(val); + Store(result, df, xform_src + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("pre in %.4f %.4f %.4f undoSRGB %.4f %.4f %.4f\n", buf_src[3 * kX], + buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX], + xform_src[3 * kX + 1], xform_src[3 * kX + 2]); +#endif + break; + } + +#if JXL_CMS_VERBOSE >= 2 + // Save inputs for printing before in-place transforms overwrite them. + const float in0 = xform_src[3 * kX + 0]; + const float in1 = xform_src[3 * kX + 1]; + const float in2 = xform_src[3 * kX + 2]; +#endif + + if (skip_lcms_) { + if (buf_dst != xform_src) { + memcpy(buf_dst, xform_src, buf_dst_.xsize() * sizeof(*buf_dst)); + } // else: in-place, no need to copy + } else { +#if JPEGXL_ENABLE_SKCMS + JXL_CHECK(skcms_Transform(xform_src, skcms_PixelFormat_RGB_fff, + skcms_AlphaFormat_Opaque, &profile_src_, buf_dst, + skcms_PixelFormat_RGB_fff, + skcms_AlphaFormat_Opaque, &profile_dst_, xsize_)); +#else // JPEGXL_ENABLE_SKCMS + JXL_DASSERT(thread < transforms_.size()); + cmsHTRANSFORM xform = transforms_[thread]; + cmsDoTransform(xform, xform_src, buf_dst, + static_cast(xsize_)); +#endif // JPEGXL_ENABLE_SKCMS + } +#if JXL_CMS_VERBOSE >= 2 + printf("xform skip%d: %.4f %.4f %.4f (%p) -> (%p) %.4f %.4f %.4f\n", + skip_lcms_, in0, in1, in2, xform_src, buf_dst, buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + + switch (postprocess_) { + case ExtraTF::kNone: + break; + case ExtraTF::kPQ: + for (size_t i = 0; i < buf_dst_.xsize(); ++i) { + buf_dst[i] = static_cast( + TF_PQ().EncodedFromDisplay(static_cast(buf_dst[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after PQ enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + case ExtraTF::kHLG: + for (size_t i = 0; i < buf_dst_.xsize(); ++i) { + buf_dst[i] = static_cast( + TF_HLG().EncodedFromDisplay(static_cast(buf_dst[i]))); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after HLG enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + case ExtraTF::kSRGB: + HWY_FULL(float) df; + for (size_t i = 0; i < buf_dst_.xsize(); i += df.N) { + const auto val = Load(df, buf_dst + i); + const auto result = TF_SRGB().EncodedFromDisplay(val); + Store(result, df, buf_dst + i); + } +#if JXL_CMS_VERBOSE >= 2 + printf("after SRGB enc %.4f %.4f %.4f\n", buf_dst[3 * kX], + buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]); +#endif + break; + } +} + +} // namespace jxl diff --git a/jxl/color_management.h b/jxl/color_management.h new file mode 100644 index 000000000..9718ff131 --- /dev/null +++ b/jxl/color_management.h @@ -0,0 +1,104 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COLOR_MANAGEMENT_H_ +#define JXL_COLOR_MANAGEMENT_H_ + +// ICC profiles and color space conversions. + +#include +#include + +#include +#include + +#include "jxl/base/data_parallel.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/status.h" +#include "jxl/color_encoding.h" +#include "jxl/common.h" +#include "jxl/image.h" +#if JPEGXL_ENABLE_SKCMS +#include "skcms.h" +#endif + +namespace jxl { + +// Thread-safe monostate. +struct ColorManagement { + // Returns false and clears c->icc if `icc` is empty or decoding it fails. + // Otherwise, moves `icc` into c->icc and sets other fields based on its + // contents. c->opaque_icc is true if any ICC field cannot be represented. + static Status SetProfile(PaddedBytes&& icc, ColorEncoding* c); + + // Returns true if c->icc was successfully created based on the other fields. + // Called by codecs that provide their own non-ICC metadata. Returning false + // indicates the profile is lost/empty and ColorSpaceTransform will fail. + static Status CreateProfile(ColorEncoding* c); + + // Returns ready-to-use color encodings: + static const ColorEncoding& SRGB(bool is_gray = false); + static const ColorEncoding& LinearSRGB(bool is_gray = false); +}; + +// Run is thread-safe. +class ColorSpaceTransform { + public: + ColorSpaceTransform() = default; + ~ColorSpaceTransform(); + + // Cannot copy (transforms_ holds pointers). + ColorSpaceTransform(const ColorSpaceTransform&) = delete; + ColorSpaceTransform& operator=(const ColorSpaceTransform&) = delete; + + // "Constructor"; allocates for up to `num_threads`, or returns false. + Status Init(const ColorEncoding& c_src, const ColorEncoding& c_dst, + size_t xsize, size_t num_threads); + + float* BufSrc(const size_t thread) { return buf_src_.Row(thread); } + + float* BufDst(const size_t thread) { return buf_dst_.Row(thread); } + + // buf_X can either be from BufX() or caller-allocated, interleaved storage. + // `thread` must be less than the `num_threads` passed to Init. + void Run(size_t thread, const float* buf_src, float* buf_dst); + + private: + enum class ExtraTF { + kNone, + kPQ, + kHLG, + kSRGB, + }; + +#if JPEGXL_ENABLE_SKCMS + // Parsed skcms_ICCProfiles retain pointers to the original data. + PaddedBytes icc_src_, icc_dst_; + skcms_ICCProfile profile_src_, profile_dst_; +#else + // One per thread - cannot share because of caching. + std::vector transforms_; +#endif + + ImageF buf_src_; + ImageF buf_dst_; + size_t xsize_; + bool skip_lcms_ = false; + ExtraTF preprocess_ = ExtraTF::kNone; + ExtraTF postprocess_ = ExtraTF::kNone; +}; + +} // namespace jxl + +#endif // JXL_COLOR_MANAGEMENT_H_ diff --git a/jxl/color_management_test.cc b/jxl/color_management_test.cc new file mode 100644 index 000000000..448fa46e5 --- /dev/null +++ b/jxl/color_management_test.cc @@ -0,0 +1,240 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/color_management.h" + +#include +#include + +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/file_io.h" +#include "jxl/base/thread_pool_internal.h" +#include "jxl/image_test_utils.h" +#include "jxl/testdata_path.h" + +namespace jxl { + +std::ostream& operator<<(std::ostream& os, const CIExy& xy) { + return os << "{x=" << xy.x << ", y=" << xy.y << "}"; +} + +std::ostream& operator<<(std::ostream& os, const PrimariesCIExy& primaries) { + return os << "{r=" << primaries.r << ", g=" << primaries.g + << ", b=" << primaries.b << "}"; +} + +namespace { + +using ::testing::ElementsAre; +using ::testing::FloatNear; + +// Small enough to be fast. If changed, must update Generate*. +static constexpr size_t kWidth = 16; + +struct Globals { + static constexpr size_t kNumThreads = 0; // only have a single row. + + Globals() : pool(kNumThreads) { + in_gray = GenerateGray(); + in_color = GenerateColor(); + out_gray = ImageF(kWidth, 1); + out_color = ImageF(kWidth * 3, 1); + + c_native = ColorManagement::LinearSRGB(/*is_gray=*/false); + c_gray = ColorManagement::LinearSRGB(/*is_gray=*/true); + } + + private: + static ImageF GenerateGray() { + ImageF gray(kWidth, 1); + float* JXL_RESTRICT row = gray.Row(0); + // Increasing left to right + for (int32_t x = 0; x < kWidth; ++x) { + row[x] = x * 1.0f / (kWidth - 1); // [0, 1] + } + return gray; + } + + static ImageF GenerateColor() { + ImageF image(kWidth * 3, 1); + float* JXL_RESTRICT interleaved = image.Row(0); + std::fill(interleaved, interleaved + kWidth * 3, 0.0f); + + // [0, 4): neutral + for (int32_t x = 0; x < 4; ++x) { + interleaved[3 * x + 0] = x * 1.0f / 3; // [0, 1] + interleaved[3 * x + 2] = interleaved[3 * x + 1] = interleaved[3 * x + 0]; + } + + // [4, 13): pure RGB with low/medium/high saturation + for (int32_t c = 0; c < 3; ++c) { + interleaved[3 * (4 + c) + c] = 0.08f + c * 0.01f; + interleaved[3 * (7 + c) + c] = 0.75f + c * 0.01f; + interleaved[3 * (10 + c) + c] = 1.0f; + } + + // [13, 16): impure, not quite saturated RGB + interleaved[3 * 13 + 0] = 0.86f; + interleaved[3 * 13 + 2] = interleaved[3 * 13 + 1] = 0.16f; + interleaved[3 * 14 + 1] = 0.87f; + interleaved[3 * 14 + 2] = interleaved[3 * 14 + 0] = 0.16f; + interleaved[3 * 15 + 2] = 0.88f; + interleaved[3 * 15 + 1] = interleaved[3 * 15 + 0] = 0.16f; + + return image; + } + + public: + ThreadPoolInternal pool; + + // ImageF so we can use VerifyRelativeError; all are interleaved RGB. + ImageF in_gray; + ImageF in_color; + ImageF out_gray; + ImageF out_color; + ColorEncoding c_native; + ColorEncoding c_gray; +}; +static Globals* g; + +class ColorManagementTest : public ::testing::TestWithParam { + public: + static void SetUpTestSuite() { g = new Globals; } + static void TearDownTestSuite() { delete g; } + + static void VerifySameFields(const ColorEncoding& c, + const ColorEncoding& c2) { + ASSERT_EQ(c.rendering_intent, c2.rendering_intent); + ASSERT_EQ(c.GetColorSpace(), c2.GetColorSpace()); + ASSERT_EQ(c.white_point, c2.white_point); + if (c.HasPrimaries()) { + ASSERT_EQ(c.primaries, c2.primaries); + } + ASSERT_TRUE(c.tf.IsSame(c2.tf)); + } + + // "Same" pixels after converting g->c_native -> c -> g->c_native. + static void VerifyPixelRoundTrip(const ColorEncoding& c) { + const ColorEncoding& c_native = c.IsGray() ? g->c_gray : g->c_native; + ColorSpaceTransform xform_fwd; + ColorSpaceTransform xform_rev; + ASSERT_TRUE(xform_fwd.Init(c_native, c, kWidth, g->pool.NumThreads())); + ASSERT_TRUE(xform_rev.Init(c, c_native, kWidth, g->pool.NumThreads())); + + const size_t thread = 0; + const ImageF& in = c.IsGray() ? g->in_gray : g->in_color; + ImageF* JXL_RESTRICT out = c.IsGray() ? &g->out_gray : &g->out_color; + xform_fwd.Run(thread, in.Row(0), xform_fwd.BufDst(thread)); + xform_rev.Run(thread, xform_fwd.BufDst(thread), out->Row(0)); + +#if JPEGXL_ENABLE_SKCMS + double max_l1 = 5E-3; + double max_rel = 6E-3; +#else + double max_l1 = 5E-5; + // Most are lower; reached 3E-7 with D60 AP0. + double max_rel = 4E-7; +#endif + if (c.IsGray()) max_rel = 2E-5; + VerifyRelativeError(in, *out, max_l1, max_rel); + } +}; +INSTANTIATE_TEST_CASE_P(ColorManagementTestInstantiation, ColorManagementTest, + ::testing::ValuesIn(AllEncodings())); + +// Exercises the ColorManagement interface for ALL ColorEncoding synthesizable +// via enums. +TEST_P(ColorManagementTest, VerifyAllProfiles) { + ColorEncoding c = GetParam(); + printf("%s\n", Description(c).c_str()); + + // Can create profile. + ASSERT_TRUE(ColorManagement::CreateProfile(&c)); + + // Can set an equivalent ColorEncoding from the generated ICC profile. + ColorEncoding c3; + ASSERT_TRUE(ColorManagement::SetProfile(std::move(c.icc), &c3)); + VerifySameFields(c, c3); + // (need a profile for VerifyPixelRoundTrip.) + c.icc = std::move(c3.icc); + + VerifyPixelRoundTrip(c); +} + +testing::Matcher CIExyIs(const double x, const double y) { + static constexpr double kMaxError = 1e-4; + return testing::AllOf( + testing::Field("x", &CIExy::x, testing::DoubleNear(x, kMaxError)), + testing::Field("y", &CIExy::y, testing::DoubleNear(y, kMaxError))); +} + +testing::Matcher PrimariesAre( + const testing::Matcher& r, const testing::Matcher& g, + const testing::Matcher& b) { + return testing::AllOf(testing::Field("r", &PrimariesCIExy::r, r), + testing::Field("g", &PrimariesCIExy::g, g), + testing::Field("b", &PrimariesCIExy::b, b)); +} + +TEST_F(ColorManagementTest, sRGBChromaticity) { + const ColorEncoding sRGB = ColorManagement::SRGB(); + EXPECT_THAT(sRGB.GetWhitePoint(), CIExyIs(0.3127, 0.3290)); + EXPECT_THAT(sRGB.GetPrimaries(), + PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), + CIExyIs(0.15, 0.06))); +} + +TEST_F(ColorManagementTest, D2700Chromaticity) { + PaddedBytes icc; + ASSERT_TRUE(jxl::ReadFile( + GetTestDataPath("jxl/color_management/sRGB-D2700.icc"), &icc)); + ColorEncoding sRGB_D2700; + ASSERT_TRUE(jxl::ColorManagement::SetProfile(std::move(icc), &sRGB_D2700)); + + EXPECT_THAT(sRGB_D2700.GetWhitePoint(), CIExyIs(0.45986, 0.41060)); + // The illuminant-relative chromaticities of this profile's primaries are the + // same as for sRGB. It is the PCS-relative chromaticities that would be + // different. + EXPECT_THAT(sRGB_D2700.GetPrimaries(), + PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), + CIExyIs(0.15, 0.06))); +} + +TEST_F(ColorManagementTest, D2700ToSRGB) { + PaddedBytes icc; + ASSERT_TRUE(jxl::ReadFile( + GetTestDataPath("jxl/color_management/sRGB-D2700.icc"), &icc)); + ColorEncoding sRGB_D2700; + ASSERT_TRUE(jxl::ColorManagement::SetProfile(std::move(icc), &sRGB_D2700)); + + ColorSpaceTransform transform; + ASSERT_TRUE(transform.Init(sRGB_D2700, ColorManagement::SRGB(), 1, 1)); + const float sRGB_D2700_values[3] = {0.863, 0.737, 0.490}; + float sRGB_values[3]; + transform.Run(0, sRGB_D2700_values, sRGB_values); + EXPECT_THAT(sRGB_values, + ElementsAre(FloatNear(0.914, 1e-3), FloatNear(0.745, 1e-3), + FloatNear(0.601, 1e-3))); +} + +} // namespace +} // namespace jxl diff --git a/jxl/common.h b/jxl/common.h new file mode 100644 index 000000000..ef1ff0ddb --- /dev/null +++ b/jxl/common.h @@ -0,0 +1,137 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COMMON_H_ +#define JXL_COMMON_H_ + +// Shared constants and helper functions. + +#include + +#include // unique_ptr + +#include "jxl/base/compiler_specific.h" + +namespace jxl { +// Some enums and typedefs used by more than one header file. +enum class BlockType : uint8_t { + kAdd = 0, + kTowardsBlack = 1, + kTowardsWhite = 2, + kNumBlockTypes, +}; + +constexpr size_t kBitsPerByte = 8; // more clear than CHAR_BIT + +constexpr inline size_t RoundUpBitsToByteMultiple(size_t bits) { + return (bits + 7) & ~size_t(7); +} + +constexpr inline size_t RoundUpToBlockDim(size_t dim) { + return (dim + 7) & ~size_t(7); +} + +template +constexpr inline T1 DivCeil(T1 a, T2 b) { + return (a + b - 1) / b; +} + +constexpr double kPi = 3.14159265358979323846264338327950288; + +// Reasonable default for sRGB, matches common monitors. Butteraugli was tuned +// for this, we scale darker/brighter inputs accordingly. +static constexpr int kDefaultIntensityTarget = 250; +static constexpr float kIntensityMultiplier = 1.0f / kDefaultIntensityTarget; + +template +constexpr T Pi(T multiplier) { + return static_cast(multiplier * kPi); +} + +// Block is the square grid of pixels to which an "energy compaction" +// transformation (e.g. DCT) is applied. Each block has its own AC quantizer. +constexpr size_t kBlockDim = 8; + +constexpr size_t kDCTBlockSize = kBlockDim * kBlockDim; + +// Group is the rectangular grid of blocks that can be decoded in parallel. This +// is different for DC. +// TODO(jon) : signal kDcGroupDimInBlocks and kGroupDim (and make them +// variables), +// allowing powers of two between (say) 64 and 1024 +constexpr size_t kDcGroupDimInBlocks = 256; +constexpr size_t kDcGroupDim = kDcGroupDimInBlocks * kBlockDim; +// 512x512 DC = 4096x4096, enough for a 4K frame (3840x2160) +// (setting it to 256 results in four DC groups of size 256x256, 224x256, +// 256x14, 224x14) +constexpr size_t kGroupDim = 256; +static_assert(kGroupDim % kBlockDim == 0, + "Group dim should be divisible by block dim"); +constexpr size_t kGroupDimInBlocks = kGroupDim / kBlockDim; + +// Maximum number of passes in an image. +constexpr size_t kMaxNumPasses = 11; + +// Maximum number of reference frames. +constexpr size_t kMaxNumReferenceFrames = 3; + +// Dimensions of a frame, in pixels, and other derived dimensions. +// Initially set from Preview/SizeHeader, may be overridden by AnimationFrame. +struct FrameDimensions { + void Set(size_t xsize, size_t ysize) { + this->xsize = xsize; + this->ysize = ysize; + xsize_blocks = DivCeil(xsize, kBlockDim); + ysize_blocks = DivCeil(ysize, kBlockDim); + xsize_padded = xsize_blocks * kBlockDim; + ysize_padded = ysize_blocks * kBlockDim; + xsize_groups = DivCeil(xsize, kGroupDim); + ysize_groups = DivCeil(ysize, kGroupDim); + xsize_dc_groups = DivCeil(xsize, kDcGroupDim); + ysize_dc_groups = DivCeil(ysize, kDcGroupDim); + num_groups = xsize_groups * ysize_groups; + num_dc_groups = xsize_dc_groups * ysize_dc_groups; + } + + size_t xsize; + size_t ysize; + size_t xsize_padded; + size_t ysize_padded; + size_t xsize_blocks; + size_t ysize_blocks; + size_t xsize_groups; + size_t ysize_groups; + size_t xsize_dc_groups; + size_t ysize_dc_groups; + size_t num_groups; + size_t num_dc_groups; +}; + +// Can't rely on C++14 yet. +template +std::unique_ptr make_unique(Args&&... args) { + return std::unique_ptr(new T(std::forward(args)...)); +} + +// This leads to somewhat better code than pointer arithmetic. +template +JXL_INLINE T* JXL_RESTRICT ByteOffset(T* JXL_RESTRICT base, + const intptr_t byte_offset) { + const uintptr_t base_addr = reinterpret_cast(base); + return reinterpret_cast(base_addr + byte_offset); +} + +} // namespace jxl + +#endif // JXL_COMMON_H_ diff --git a/jxl/compressed_dc.cc b/jxl/compressed_dc.cc new file mode 100644 index 000000000..b6767b782 --- /dev/null +++ b/jxl/compressed_dc.cc @@ -0,0 +1,809 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/compressed_dc.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "jxl/ac_strategy.h" +#include "jxl/ans_params.h" +#include "jxl/aux_out.h" +#include "jxl/aux_out_fwd.h" +#include "jxl/base/bits.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/profiler.h" +#include "jxl/base/status.h" +#include "jxl/chroma_from_luma.h" +#include "jxl/common.h" +#include "jxl/dec_ans.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/dec_cache.h" +#include "jxl/enc_cache.h" +#include "jxl/entropy_coder.h" +#include "jxl/image.h" +#include "jxl/predictor.h" + +namespace jxl { +namespace { + +// Approximate mapping of actual residual to encoded residual, used for +// computing context. + +constexpr size_t kResidualLut[4][8] = { + {0, 1, 2, 3, 4, 5, 6, 7}, + {0, 1, 2, 3, 3, 4, 4, 5}, + {0, 1, 2, 3, 3, 3, 4, 4}, + {0, 1, 2, 3, 3, 3, 4, 4}, +}; + +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) +class DcCoderBase { + public: + enum { + kNumResidualContexts = 8, + kContextsPerChannel = kNumResidualContexts + 4, + kNumContexts = 3 * kContextsPerChannel, + }; + + uint32_t JXL_INLINE ResidualToEncoded(size_t c, size_t residual) { + if (residual > 7) { + return ((residual + (1 << extra_levels_[c]) / 2) >> extra_levels_[c]) + + extra_levels_[c]; + } + return kResidualLut[extra_levels_[c]][residual]; + } + + size_t Context(size_t c, size_t correct, size_t badness) { + if (correct == 0) { + JXL_ASSERT(badness != 0); + // Discard error sign. Assumes that badness comes from PackSigned. + badness = (badness + 1) >> 1; + badness = ResidualToEncoded(c, badness); + size_t badness_offset = + std::min(badness, kNumResidualContexts) - 1; + return kContextsPerChannel * c + badness_offset; + } + return kContextsPerChannel * c + kNumResidualContexts + + CeilLog2Nonzero(9 - correct); + } + + // Stores the temporary buffer row in the final image. + void FlushRow(size_t y) { + for (size_t c = 0; c < 3; c++) { + float* JXL_RESTRICT dc_dec_row = + dc_dec_rows_base_[c] + y * dc_dec_stride_; + float* JXL_RESTRICT dc_quant_field_row = + dc_quant_field_rows_base_[c] + y * dc_quant_field_stride_; + for (size_t x = 0; x < xsize_; x++) { + dc_quant_field_row[x] = dc_quant_field_[4 * x + c]; + dc_dec_row[x] = dc_dec_[4 * x + c]; + } + } + } + + void StartRow(size_t y) { + if (y != 0) FlushRow(y - 1); + } + + float GetDcValue(size_t c, size_t y, size_t x) { + dc_dec_[4 * x + c] = 0; + return dc_rows_[c][y * dc_stride_ + x] - + dc_dec_[4 * x + 1] * cmap_factor_[c]; + } + + DcCoderBase(size_t xsize, const uint32_t* JXL_RESTRICT extra_levels, + const float* JXL_RESTRICT mul_dc, + const float* JXL_RESTRICT inv_mul_dc, + const float* JXL_RESTRICT cmap_factor, + const float* JXL_RESTRICT* JXL_RESTRICT dc_rows, size_t dc_stride, + float** JXL_RESTRICT dc_dec_rows, size_t dc_dec_stride, + float** JXL_RESTRICT dc_quant_field_rows, + size_t dc_quant_field_stride) + : xsize_(xsize), + mul_dc_{mul_dc[0] / (1 << extra_levels[0]), + mul_dc[1] / (1 << extra_levels[1]), + mul_dc[2] / (1 << extra_levels[2]), 0}, + inv_mul_dc_(inv_mul_dc), + cmap_factor_(cmap_factor), + dc_rows_(dc_rows), + dc_stride_(dc_stride), + dc_dec_rows_base_(dc_dec_rows), + dc_dec_stride_(dc_dec_stride), + dc_quant_field_rows_base_(dc_quant_field_rows), + dc_quant_field_stride_(dc_quant_field_stride), + extra_levels_(extra_levels) {} + + float JXL_INLINE Dequantize(size_t c, int val) { return val * mul_dc_[c]; } + + int Quantize(size_t c, float val) { + JXL_DASSERT(val >= 0); + val *= inv_mul_dc_[c]; + if (extra_levels_[c] == 0) { + return static_cast(val + 0.5f); + } + if (extra_levels_[c] == 1) { + if (val <= 0.25f) { + return 0; + } + if (val < 0.75f) { + return 1; + } + return static_cast(val + 0.5f) + 1; + } + if (extra_levels_[c] == 2) { + if (val <= 0.125f) { + return 0; + } + if (val < 0.375f) { + return 1; + } + if (val < 0.75f) { + return 2; + } + return static_cast(val + 0.5f) + 2; + } + JXL_DASSERT(extra_levels_[c] == 3); + if (val <= 0.0625f) { + return 0; + } + if (val < 0.1875f) { + return 1; + } + if (val < 0.375f) { + return 2; + } + if (val < 0.75f) { + return 3; + } + return static_cast(val + 0.5f) + 3; + } + + uint32_t ComputeResidual(size_t c, int32_t predicted, float actual) { + float fpred = Dequantize(c, predicted); + float fdelta = actual - fpred; + bool negative = fdelta < 0; + fdelta = std::abs(fdelta); + + int32_t delta = Quantize(c, fdelta); + + delta = negative ? -delta : delta; + return PackSigned(delta); + } + + int32_t JXL_INLINE ApplyResidual(size_t c, int32_t predicted, + uint32_t residual, + float* JXL_RESTRICT interval) { + int32_t delta = UnpackSigned(residual); + bool negative = delta < 0; + delta = std::abs(delta); + + if (static_cast(delta) > extra_levels_[c]) { + delta = (delta - extra_levels_[c]) << extra_levels_[c]; + *interval *= 1 << extra_levels_[c]; + } else if (delta != 0) { + delta = 1 << (delta - 1); + *interval *= delta; + } + + delta = negative ? -delta : delta; + return predicted + delta; + } + + // Encoder only + void ComputeDecoded(size_t c, size_t x, size_t y, + const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT residuals, + int32_t* JXL_RESTRICT decoded) { + int32_t prediction = predictions[c]; + + dc_quant_field_[4 * x + c] = mul_dc_[c]; + dc_dec_[4 * x + c] = 0; + + int quant = + ApplyResidual(c, prediction, residuals[c], &dc_quant_field_[4 * x + c]); + + decoded[c] = quant; + + float dcd = Dequantize(c, quant); + dc_dec_[4 * x + c] = dcd + dc_dec_[4 * x + 1] * cmap_factor_[c]; + } + + HWY_ATTR void ComputeDecoded3(size_t x, size_t y, + const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT residuals, + int32_t* JXL_RESTRICT decoded) { +#if HWY_HAS_VARIABLE_SHIFT && HWY_BITS >= 128 + using DU = HWY_CAPPED(uint32_t, 4); + using DI = HWY_CAPPED(int32_t, 4); + using DF = HWY_CAPPED(float, 4); + const DI di; + const DU du; + const auto residual = Load(du, residuals); + + // UnpackSigned + const auto one = Set(du, 1); + const auto is_negative = TestBit(residual, one); // sign bit = LSB + // Workaround for >> rounding to neg infinity instead of zero: + // If positive, LSB is 0 so the add disappears after the shift. + // If negative, LSB is 1 so the add carries and we have incremented the + // shifted result by one. + const auto delta = hwy::ShiftRight<1>(residual + one); + + // Expand extra levels + const auto extra_levels = Load(du, extra_levels_); + // Build mask of "delta > extra_levels" + const auto is_hi = MaskFromVec( + BitCast(du, hwy::ShiftRight<31>(BitCast(di, extra_levels - delta)))); + const auto delta_hi = (delta - extra_levels) << extra_levels; + const auto is_zero = delta == Zero(du); + const auto deltam1 = delta - one; + const auto delta_lo = one << deltam1; + const auto final_delta = + IfThenElse(is_hi, delta_hi, IfThenZeroElse(is_zero, delta_lo)); + + // Apply prediction + const auto prediction = BitCast(du, Load(di, predictions)); + const auto quantized = + BitCast(di, IfThenElse(is_negative, prediction - final_delta, + prediction + final_delta)); + // Not aligned in predictor.h. + StoreU(quantized, di, decoded); + + const auto mul = Load(DF(), mul_dc_); + // Compute adjusted quant interval + const auto mul_factor_hi = one << extra_levels; + const auto mul_factor = BitCast( + di, + IfThenElse(is_hi, mul_factor_hi, IfThenElse(is_zero, one, delta_lo))); + const auto adj_mul = mul * ConvertTo(DF(), mul_factor); + Store(adj_mul, DF(), dc_quant_field_ + 4 * x); + + const auto dequant = ConvertTo(DF(), quantized) * mul; + const auto y_dequant = hwy::Broadcast<1>(dequant); + const auto correlated = + MulAdd(Load(DF(), cmap_factor_), y_dequant, dequant); + Store(correlated, DF(), dc_dec_ + 4 * x); +#else + for (size_t c : {1, 0, 2}) { + ComputeDecoded(c, x, y, predictions, residuals, decoded); + } +#endif + } + + private: + const size_t xsize_; + HWY_ALIGN float const mul_dc_[4]; + const float* JXL_RESTRICT inv_mul_dc_; + const float* JXL_RESTRICT cmap_factor_; + + // Input (encoder-side) values. + const float* JXL_RESTRICT* JXL_RESTRICT dc_rows_; + const size_t dc_stride_; + + // Output (decoder-side) values. + float** JXL_RESTRICT dc_dec_rows_base_; + const size_t dc_dec_stride_; + + float** JXL_RESTRICT dc_quant_field_rows_base_; + const size_t dc_quant_field_stride_; + + // One padding value every 3 to have aligned stores. + HWY_ALIGN float dc_dec_[4 * kDcGroupDimInBlocks]; + HWY_ALIGN float dc_quant_field_[4 * kDcGroupDimInBlocks]; + + const uint32_t* JXL_RESTRICT const extra_levels_; +}; + +class DCEncoder : public DcPredictor { + public: + DCEncoder(size_t xsize, const uint32_t* JXL_RESTRICT extra_levels, + const float* JXL_RESTRICT mul_dc, + const float* JXL_RESTRICT inv_mul_dc, + const float* JXL_RESTRICT cmap_factor, + const float* JXL_RESTRICT* JXL_RESTRICT dc_rows, size_t dc_stride, + float** JXL_RESTRICT dc_dec_rows, size_t dc_dec_stride, + float** JXL_RESTRICT dc_quant_field_rows, + size_t dc_quant_field_stride) + : predictor_(xsize, extra_levels, mul_dc, inv_mul_dc, cmap_factor, + dc_rows, dc_stride, dc_dec_rows, dc_dec_stride, + dc_quant_field_rows, dc_quant_field_stride) {} + + HWY_ATTR void Run(size_t xsize, size_t ysize, + std::vector* JXL_RESTRICT tokens, + AuxOut* JXL_RESTRICT aux_out) { + tokens_ = tokens; + DcPredictor::Run(xsize, ysize, aux_out); + predictor_.FlushRow(ysize - 1); + } + + HWY_ATTR JXL_INLINE void Prediction(size_t x, size_t y, + const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT num_correct, + const uint32_t* JXL_RESTRICT min_error, + int32_t* JXL_RESTRICT decoded) { + uint32_t residuals[3]; + // TODO(veluca): investigate possible SIMD-fication of this code. + + int32_t prediction = predictions[1]; + float dcv = predictor_.GetDcValue(1, y, x); + residuals[1] = predictor_.ComputeResidual(1, prediction, dcv); + predictor_.ComputeDecoded(1, x, y, predictions, residuals, decoded); + + prediction = predictions[0]; + dcv = predictor_.GetDcValue(0, y, x); + residuals[0] = predictor_.ComputeResidual(0, prediction, dcv); + predictor_.ComputeDecoded(0, x, y, predictions, residuals, decoded); + + prediction = predictions[2]; + dcv = predictor_.GetDcValue(2, y, x); + residuals[2] = predictor_.ComputeResidual(2, prediction, dcv); + predictor_.ComputeDecoded(2, x, y, predictions, residuals, decoded); + + int ctx = predictor_.Context(1, num_correct[1], min_error[1]); + TokenizeHybridUint(ctx, residuals[1], tokens_); + ctx = predictor_.Context(0, num_correct[0], min_error[0]); + TokenizeHybridUint(ctx, residuals[0], tokens_); + ctx = predictor_.Context(2, num_correct[2], min_error[2]); + TokenizeHybridUint(ctx, residuals[2], tokens_); + } + + void StartRow(size_t y) { predictor_.StartRow(y); } + + private: + std::vector* JXL_RESTRICT tokens_; + DcCoderBase predictor_; +}; + +class DCDecoder : public DcPredictor { + public: + DCDecoder(size_t xsize, const uint32_t* JXL_RESTRICT extra_levels, + const float* JXL_RESTRICT mul_dc, + const float* JXL_RESTRICT cmap_factor, + float** JXL_RESTRICT dc_dec_rows, size_t dc_dec_stride, + float** JXL_RESTRICT dc_quant_field_rows, + size_t dc_quant_field_stride) + : predictor_(xsize, extra_levels, mul_dc, nullptr, cmap_factor, nullptr, + 0, dc_dec_rows, dc_dec_stride, dc_quant_field_rows, + dc_quant_field_stride) {} + + HWY_ATTR void Run(size_t xsize, size_t ysize, BitReader* JXL_RESTRICT br, + ANSSymbolReader* JXL_RESTRICT decoder, + const std::vector& JXL_RESTRICT context_map, + AuxOut* JXL_RESTRICT aux_out) { + br_ = br; + decoder_ = decoder; + context_map_ = &context_map; + DcPredictor::Run(xsize, ysize, aux_out); + predictor_.FlushRow(ysize - 1); + } + + HWY_ATTR JXL_INLINE void Prediction(size_t x, size_t y, + const int32_t* JXL_RESTRICT predictions, + const uint32_t* JXL_RESTRICT num_correct, + const uint32_t* JXL_RESTRICT min_error, + int32_t* JXL_RESTRICT decoded) { + HWY_ALIGN uint32_t residuals[4]; + residuals[3] = 0; + + for (size_t c : {1, 0, 2}) { + int ctx = predictor_.Context(c, num_correct[c], min_error[c]); + residuals[c] = ReadHybridUint(ctx, br_, decoder_, *context_map_); + } + predictor_.ComputeDecoded3(x, y, predictions, residuals, decoded); + } + void StartRow(size_t y) { predictor_.StartRow(y); } + + private: + BitReader* JXL_RESTRICT br_; + ANSSymbolReader* JXL_RESTRICT decoder_; + const std::vector* JXL_RESTRICT context_map_; + DcCoderBase predictor_; +}; + +void RunDCEncoder(bool progressive16, const uint32_t* JXL_RESTRICT extra_levels, + const Rect& rect, const float* JXL_RESTRICT mul_dc, + const float* JXL_RESTRICT inv_mul_dc, + const float* JXL_RESTRICT cmap_factor, + const float* JXL_RESTRICT* JXL_RESTRICT downsampled_dc_rows, + size_t downsampled_dc_stride, + const float* JXL_RESTRICT* JXL_RESTRICT dc_rows, + size_t dc_stride, float** JXL_RESTRICT dc_dec_rows, + size_t dc_dec_stride, + float** JXL_RESTRICT dc_quant_field_rows, + size_t dc_quant_field_stride, std::vector* tokens, + AuxOut* JXL_RESTRICT aux_out) {} + +void RunDCDecoder(bool progressive16, const uint32_t* JXL_RESTRICT extra_levels, + const Rect& rect, const float* JXL_RESTRICT mul_dc, + const float* JXL_RESTRICT cmap_factor, + const float* JXL_RESTRICT* JXL_RESTRICT downsampled_dc_rows, + size_t downsampled_dc_stride, + float** JXL_RESTRICT dc_dec_rows, size_t dc_dec_stride, + float** JXL_RESTRICT dc_quant_field_rows, + size_t dc_quant_field_stride, BitReader* JXL_RESTRICT br, + ANSSymbolReader* JXL_RESTRICT decoder, + const std::vector& context_map, + AuxOut* JXL_RESTRICT aux_out) {} + +namespace { +HWY_ALIGN const uint32_t kExtraLevels[4][4] = { + {0, 0, 0, /*unused*/ 0}, + {1, 1, 1, /*unused*/ 0}, + {1, 2, 1, /*unused*/ 0}, + {2, 3, 2, /*unused*/ 0}, +}; +} + +} // namespace + +void TokenizeDC(size_t group_index, const Image3F& dc, + PassesEncoderState* JXL_RESTRICT enc_state, AuxOut* aux_out) { + const Rect rect = enc_state->shared.DCGroupRect(group_index); + const Quantizer& quantizer = enc_state->shared.quantizer; + const ColorCorrelationMap& cmap = enc_state->shared.cmap; + Image3F* dc_dec = &enc_state->shared.dc_storage; + Image3F* dc_quant_field = &enc_state->shared.dc_quant_field; + const float* JXL_RESTRICT dc_rows[3] = {rect.ConstPlaneRow(dc, 0, 0), + rect.ConstPlaneRow(dc, 1, 0), + rect.ConstPlaneRow(dc, 2, 0)}; + const size_t dc_stride = dc.PixelsPerRow(); + // Not restrict: the predictor will create aliasing pointers in these arrays, + // and they are not used directly anyway. + float* dc_dec_rows[3] = {rect.PlaneRow(dc_dec, 0, 0), + rect.PlaneRow(dc_dec, 1, 0), + rect.PlaneRow(dc_dec, 2, 0)}; + const size_t dc_dec_stride = dc_dec->PixelsPerRow(); + + float* dc_quant_field_rows[3] = {rect.PlaneRow(dc_quant_field, 0, 0), + rect.PlaneRow(dc_quant_field, 1, 0), + rect.PlaneRow(dc_quant_field, 2, 0)}; + const size_t dc_quant_field_stride = dc_quant_field->PixelsPerRow(); + + DCEncoder coder(rect.xsize(), + kExtraLevels[enc_state->extra_dc_levels[group_index]], + quantizer.MulDC(), quantizer.InvMulDC(), cmap.DCFactors(), + dc_rows, dc_stride, dc_dec_rows, dc_dec_stride, + dc_quant_field_rows, dc_quant_field_stride); + coder.Run(rect.xsize(), rect.ysize(), &enc_state->dc_tokens[group_index], + aux_out); +} + +void DecodeDC(BitReader* br, ANSSymbolReader* decoder, + const std::vector& context_map, const Rect& rect, + const float* mul_dc, const float* cmap_factor, int extra_levels, + Image3F* JXL_RESTRICT dc, Image3F* JXL_RESTRICT dc_quant_field, + AuxOut* aux_out) { + PROFILER_FUNC; + // Not restrict: the predictor will create aliasing pointers in these arrays, + // and they are not used directly anyway. + float* dc_rows[3] = {rect.PlaneRow(dc, 0, 0), rect.PlaneRow(dc, 1, 0), + rect.PlaneRow(dc, 2, 0)}; + const size_t dc_stride = dc->PixelsPerRow(); + float* dc_quant_field_rows[3] = {rect.PlaneRow(dc_quant_field, 0, 0), + rect.PlaneRow(dc_quant_field, 1, 0), + rect.PlaneRow(dc_quant_field, 2, 0)}; + const size_t dc_quant_field_stride = dc_quant_field->PixelsPerRow(); + + DCDecoder coder(rect.xsize(), kExtraLevels[extra_levels], mul_dc, cmap_factor, + dc_rows, dc_stride, dc_quant_field_rows, + dc_quant_field_stride); + coder.Run(rect.xsize(), rect.ysize(), br, decoder, context_map, aux_out); +} + +constexpr size_t kCmapBaseContext = DcCoderBase::kNumContexts; +constexpr size_t kControlFieldBaseContext = kCmapBaseContext + kCmapContexts; +constexpr size_t kNumDCContexts = + kControlFieldBaseContext + kNumControlFieldContexts; + +Status EncodeDCGroup(const PassesEncoderState& enc_state, size_t group_idx, + BitWriter* writer, AuxOut* aux_out) { + const Rect rect = enc_state.shared.DCGroupRect(group_idx); + // Single set of tokens to avoid overhead of multiple ANS streams. + std::vector> tokens(1); + + if (!(enc_state.shared.frame_header.flags & FrameHeader::kUseDcFrame)) { + JXL_ASSERT(enc_state.extra_dc_levels[group_idx] < 4); + // Copy DC tokens from enc cache. + tokens[0] = enc_state.dc_tokens[group_idx]; + + BitWriter::Allotment allotment(writer, 1 + 2 * kBitsPerByte); + // Write number of extra DC levels. + writer->Write(2, enc_state.extra_dc_levels[group_idx]); + ReclaimAndCharge(writer, &allotment, kLayerDC, aux_out); + } + + if (!enc_state.shared.experiments.use_new_cmap) { + JXL_ASSERT(rect.x0() % kColorTileDimInBlocks == 0); + JXL_ASSERT(rect.y0() % kColorTileDimInBlocks == 0); + Rect cmap_rect(rect.x0() / kColorTileDimInBlocks, + rect.y0() / kColorTileDimInBlocks, + DivCeil(rect.xsize(), kColorTileDimInBlocks), + DivCeil(rect.ysize(), kColorTileDimInBlocks)); + + EncodeColorMap(enc_state.shared.cmap, cmap_rect, &tokens[0], + kCmapBaseContext, aux_out); + } + + TokenizeAcStrategy(rect, enc_state.shared.ac_strategy, &tokens[0], + kControlFieldBaseContext); + + TokenizeQuantField(rect, enc_state.shared.raw_quant_field, + enc_state.shared.ac_strategy, &tokens[0], + kControlFieldBaseContext + kAcStrategyContexts); + + if (enc_state.shared.image_features.loop_filter.epf) { + TokenizeARParameters( + rect, enc_state.shared.epf_sharpness, enc_state.shared.ac_strategy, + &tokens[0], + kControlFieldBaseContext + kAcStrategyContexts + kQuantFieldContexts); + } + + std::vector context_map; + EntropyEncodingData codes; + HistogramParams hist_params(enc_state.cparams.speed_tier, kNumDCContexts); + BuildAndEncodeHistograms(hist_params, kNumDCContexts, tokens, &codes, + &context_map, writer, kLayerDC, aux_out); + WriteTokens(tokens[0], codes, context_map, writer, kLayerDC, aux_out); + + return true; +} + +// `rect`: block units. +HWY_ATTR Status DecodeDCGroup(BitReader* reader, size_t group_idx, + PassesDecoderState* dec_state, AuxOut* aux_out) { + PROFILER_FUNC; + int extra_dc_levels = 0; + + if (!(dec_state->shared->frame_header.flags & FrameHeader::kUseDcFrame)) { + extra_dc_levels = reader->ReadFixedBits<2>(); + } + + std::vector context_map; + ANSCode code; + JXL_RETURN_IF_ERROR(DecodeHistograms( + reader, kNumDCContexts, ANS_MAX_ALPHA_SIZE, &code, &context_map)); + ANSSymbolReader decoder(&code, reader); + + const Rect rect = dec_state->shared->DCGroupRect(group_idx); + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + Rect rect0(0, 0, xsize, ysize); + if (!(dec_state->shared->frame_header.flags & FrameHeader::kUseDcFrame)) { + DecodeDC(reader, &decoder, context_map, rect, + dec_state->shared->quantizer.MulDC(), + dec_state->shared->cmap.DCFactors(), extra_dc_levels, + &dec_state->shared_storage.dc_storage, + &dec_state->shared_storage.dc_quant_field, aux_out); + } + + if (!dec_state->shared_storage.experiments.use_new_cmap) { + JXL_ASSERT(rect.x0() % kColorTileDimInBlocks == 0); + JXL_ASSERT(rect.y0() % kColorTileDimInBlocks == 0); + Rect cmap_rect(rect.x0() / kColorTileDimInBlocks, + rect.y0() / kColorTileDimInBlocks, + DivCeil(rect.xsize(), kColorTileDimInBlocks), + DivCeil(rect.ysize(), kColorTileDimInBlocks)); + + // Not all default + JXL_RETURN_IF_ERROR(DecodeColorMap(reader, &decoder, context_map, + &dec_state->shared_storage.cmap, + cmap_rect, kCmapBaseContext, aux_out)); + } + + if (!DecodeAcStrategy(reader, &decoder, context_map, rect, + &dec_state->shared_storage.ac_strategy, + kControlFieldBaseContext)) { + return JXL_FAILURE("Failed to decode AcStrategy."); + } + + if (!DecodeQuantField(reader, &decoder, context_map, rect, + dec_state->shared_storage.ac_strategy, + &dec_state->shared_storage.raw_quant_field, + kControlFieldBaseContext + kAcStrategyContexts)) { + return JXL_FAILURE("Failed to decode QuantField."); + } + + if (dec_state->shared->image_features.loop_filter.epf && + !DecodeARParameters(reader, &decoder, context_map, rect, + dec_state->shared_storage.ac_strategy, + &dec_state->shared_storage.epf_sharpness, + kControlFieldBaseContext + kAcStrategyContexts + + kQuantFieldContexts)) { + return JXL_FAILURE("Failed to decode ARParameters."); + } + + if (!decoder.CheckANSFinalState()) { + return JXL_FAILURE("DC group: ANS checksum failure."); + } + + return true; +} + +namespace { +using D = HWY_FULL(float); +using DScalar = hwy::Scalar; +using V = hwy::VT; + +// TODO(veluca): optimize constants. +const float w1 = 0.20345139757231578f; +const float w2 = 0.0334829185968739f; +const float w0 = 1.0f - 4.0f * (w1 + w2); + +template +HWY_ATTR V MaxWorkaround(V a, V b) { +#if (HWY_ALL_TARGETS & HWY_AVX512) && HWY_COMPILER_CLANG <= 800 + // Prevents "Do not know how to split the result of this operator" error + return IfThenElse(a > b, a, b); +#else + return Max(a, b); +#endif +} + +template +HWY_ATTR JXL_INLINE void ComputePixelChannel( + const D d, const float* JXL_RESTRICT dc_quant_row, + const float* JXL_RESTRICT row_top, const float* JXL_RESTRICT row, + const float* JXL_RESTRICT row_bottom, hwy::VT* JXL_RESTRICT mc, + hwy::VT* JXL_RESTRICT sm, hwy::VT* JXL_RESTRICT gap, size_t x) { + const auto tl = LoadU(d, row_top + x - 1); + const auto tc = Load(d, row_top + x); + const auto tr = LoadU(d, row_top + x + 1); + + const auto ml = LoadU(d, row + x - 1); + *mc = Load(d, row + x); + const auto mr = LoadU(d, row + x + 1); + + const auto bl = LoadU(d, row_bottom + x - 1); + const auto bc = Load(d, row_bottom + x); + const auto br = LoadU(d, row_bottom + x + 1); + + const auto w_center = Set(d, w0); + const auto w_side = Set(d, w1); + const auto w_corner = Set(d, w2); + + const auto corner = tl + tr + bl + br; + const auto side = ml + mr + tc + bc; + *sm = corner * w_corner + side * w_side + *mc * w_center; + + constexpr uint32_t kMask = 0x7FFFFFFF; + float mask; + memcpy(&mask, &kMask, sizeof(float)); + const auto sign_mask = Set(d, mask); + + const auto dc_quant = Load(d, dc_quant_row + x); + *gap = MaxWorkaround(*gap, sign_mask & ((*mc - *sm) / dc_quant)); +} + +template +HWY_ATTR JXL_INLINE void ComputePixel( + const float* JXL_RESTRICT* JXL_RESTRICT dc_quant_rows, + const float* JXL_RESTRICT* JXL_RESTRICT rows_top, + const float* JXL_RESTRICT* JXL_RESTRICT rows, + const float* JXL_RESTRICT* JXL_RESTRICT rows_bottom, + float* JXL_RESTRICT* JXL_RESTRICT out_rows, size_t x) { + const D d; + auto mc_x = Undefined(d); + auto mc_y = Undefined(d); + auto mc_b = Undefined(d); + auto sm_x = Undefined(d); + auto sm_y = Undefined(d); + auto sm_b = Undefined(d); + auto gap = Set(d, 0.5f); + ComputePixelChannel(d, dc_quant_rows[0], rows_top[0], rows[0], rows_bottom[0], + &mc_x, &sm_x, &gap, x); + ComputePixelChannel(d, dc_quant_rows[1], rows_top[1], rows[1], rows_bottom[1], + &mc_y, &sm_y, &gap, x); + ComputePixelChannel(d, dc_quant_rows[2], rows_top[2], rows[2], rows_bottom[2], + &mc_b, &sm_b, &gap, x); + auto factor = MulAdd(Set(d, -4.0f), gap, Set(d, 3.0f)); + factor = ZeroIfNegative(factor); + + auto out = MulAdd(sm_x - mc_x, factor, mc_x); + Store(out, d, out_rows[0] + x); + out = MulAdd(sm_y - mc_y, factor, mc_y); + Store(out, d, out_rows[1] + x); + out = MulAdd(sm_b - mc_b, factor, mc_b); + Store(out, d, out_rows[2] + x); +} + +} // namespace + +HWY_ATTR void AdaptiveDCSmoothing(const Image3F& dc_quant_field, Image3F* dc, + ThreadPool* pool) { + const size_t xsize = dc->xsize(); + const size_t ysize = dc->ysize(); + if (ysize <= 2 || xsize <= 2) return; + + // TODO(veluca): use tile-based processing? + // TODO(veluca): decide if changes to the y channel should be propagated to + // the x and b channels through color correlation. + JXL_ASSERT(w1 + w2 < 0.25f); + + PROFILER_FUNC; + + Image3F smoothed(xsize, ysize); + // Fill in borders that the loop below will not. First and last are unused. + for (size_t c = 0; c < 3; c++) { + for (size_t y : {size_t(0), ysize - 1}) { + memcpy(smoothed.PlaneRow(c, y), dc->PlaneRow(c, y), + xsize * sizeof(float)); + } + } + auto process_row = [&](int y, int /*thread*/) HWY_ATTR { + const float* JXL_RESTRICT rows_top[3]{ + dc->ConstPlaneRow(0, y - 1), + dc->ConstPlaneRow(1, y - 1), + dc->ConstPlaneRow(2, y - 1), + }; + const float* JXL_RESTRICT rows[3] = { + dc->ConstPlaneRow(0, y), + dc->ConstPlaneRow(1, y), + dc->ConstPlaneRow(2, y), + }; + const float* JXL_RESTRICT rows_bottom[3] = { + dc->ConstPlaneRow(0, y + 1), + dc->ConstPlaneRow(1, y + 1), + dc->ConstPlaneRow(2, y + 1), + }; + const float* JXL_RESTRICT rows_dc_quant[3] = { + dc_quant_field.ConstPlaneRow(0, y), + dc_quant_field.ConstPlaneRow(1, y), + dc_quant_field.ConstPlaneRow(2, y), + }; + float* JXL_RESTRICT rows_out[3] = { + smoothed.PlaneRow(0, y), + smoothed.PlaneRow(1, y), + smoothed.PlaneRow(2, y), + }; + for (size_t x : {size_t(0), xsize - 1}) { + for (size_t c = 0; c < 3; c++) { + rows_out[c][x] = rows[c][x]; + } + } + + size_t x = 1; + // First pixels + constexpr size_t kVectorSize = D::N; + for (; x < std::min(kVectorSize, xsize - 1); x++) { + ComputePixel(rows_dc_quant, rows_top, rows, rows_bottom, + rows_out, x); + } + // Full vectors. + for (; x + D::N <= xsize - 1; x += D::N) { + ComputePixel(rows_dc_quant, rows_top, rows, rows_bottom, rows_out, x); + } + // Last pixels. + for (; x < xsize - 1; x++) { + ComputePixel(rows_dc_quant, rows_top, rows, rows_bottom, + rows_out, x); + } + }; + RunOnPool(pool, 1, ysize - 1, ThreadPool::SkipInit(), process_row, + "DCSmoothingRow"); + dc->Swap(smoothed); +} + +} // namespace jxl diff --git a/jxl/compressed_dc.h b/jxl/compressed_dc.h new file mode 100644 index 000000000..1b10b94d4 --- /dev/null +++ b/jxl/compressed_dc.h @@ -0,0 +1,62 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_COMPRESSED_DC_H_ +#define JXL_COMPRESSED_DC_H_ + +#include +#include + +#include +#include + +#include "jxl/aux_out_fwd.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/span.h" +#include "jxl/base/status.h" +#include "jxl/dec_bit_reader.h" +#include "jxl/dec_cache.h" +#include "jxl/enc_ans.h" +#include "jxl/enc_bit_writer.h" +#include "jxl/enc_cache.h" +#include "jxl/image.h" + +// DC handling functions: encoding and decoding of DC to and from bitstream, and +// related function to initialize the per-group decoder cache. + +namespace jxl { + +void TokenizeDC(size_t group_index, const Image3F& dc, + PassesEncoderState* JXL_RESTRICT enc_state, AuxOut* aux_out); + +// Encodes the DC-related information from enc_state: quantized dc itself +// and gradient map. +Status EncodeDCGroup(const PassesEncoderState& enc_state, size_t group_idx, + BitWriter* writer, AuxOut* aux_out); + +// Decodes and dequantizes DC. +Status DecodeDCGroup(BitReader* reader, size_t group_idx, + PassesDecoderState* dec_state, AuxOut* aux_out); + +// Fill border of DC image. +void InitializeDCBorder(Image3F* JXL_RESTRICT dc); + +// Smooth DC in already-smooth areas, to counteract banding. +HWY_ATTR void AdaptiveDCSmoothing(const Image3F& dc_quant_field, Image3F* dc, + ThreadPool* pool); + +} // namespace jxl + +#endif // JXL_COMPRESSED_DC_H_ diff --git a/jxl/compressed_dc_test.cc b/jxl/compressed_dc_test.cc new file mode 100644 index 000000000..816ace617 --- /dev/null +++ b/jxl/compressed_dc_test.cc @@ -0,0 +1,135 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "gtest/gtest.h" +#include "jxl/aux_out.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/padded_bytes.h" +#include "jxl/base/thread_pool_internal.h" +#include "jxl/codec_in_out.h" +#include "jxl/dec_file.h" +#include "jxl/dec_params.h" +#include "jxl/enc_cache.h" +#include "jxl/enc_file.h" +#include "jxl/enc_params.h" +#include "jxl/extras/codec.h" +#include "jxl/image.h" +#include "jxl/image_test_utils.h" +#include "jxl/testdata_path.h" + +namespace jxl { +namespace { + +struct CompressedDCTestParams { + explicit CompressedDCTestParams(const double butteraugli_distance, + const bool fast_mode = false, + const bool shrink8 = false) + : butteraugli_distance(butteraugli_distance), + fast_mode(fast_mode), + shrink8(shrink8) {} + double butteraugli_distance; + bool fast_mode; + bool shrink8; +}; + +std::ostream& operator<<(std::ostream& os, CompressedDCTestParams params) { + auto previous_flags = os.flags(); + os << std::boolalpha; + os << "CompressedDCTestParams{/*butteraugli_distance=*/" + << params.butteraugli_distance << ", /*fast_mode=*/" << params.fast_mode + << ", /*shrink8=*/" << params.shrink8 << "}"; + os.flags(previous_flags); + return os; +} + +class CompressedDCTest : public testing::TestWithParam { + protected: + // Returns compressed size [bytes]. + static void Roundtrip(CodecInOut* io, const CompressParams& cparams, + const DecompressParams& dparams, ThreadPool* pool) { + PaddedBytes compressed; + + Image3F encoding_dc; + Image3F encoding_dec; + AuxOut encoding_info; + encoding_info.testing_aux.dc = &encoding_dc; + encoding_info.testing_aux.decoded = &encoding_dec; + + PassesEncoderState enc_state; + EXPECT_TRUE( + EncodeFile(cparams, io, &enc_state, &compressed, &encoding_info, pool)); + + Image3F decoding_dc; + AuxOut decoding_info; + decoding_info.testing_aux.dc = &decoding_dc; + + EXPECT_TRUE(DecodeFile(dparams, compressed, io, &decoding_info, pool)); + +#ifdef __aarch64__ + // Roundtrip precision is a bit worse in arm64. + // TODO(veluca): Investigate why this is the case. + const float kErrorThreshold = 6e-6; +#else + const float kErrorThreshold = 1e-6; +#endif + + VerifyRelativeError(decoding_dc, encoding_dc, kErrorThreshold, + kErrorThreshold); + VerifyRelativeError(io->Main().color(), encoding_dec, + kErrorThreshold, kErrorThreshold); + } +}; + +INSTANTIATE_TEST_SUITE_P( + CompressedDCTestInstantiation, CompressedDCTest, + testing::Values(CompressedDCTestParams{/*butteraugli_distance=*/1.0, + /*fast_mode=*/false, + /*shrink8=*/true}, + CompressedDCTestParams{/*butteraugli_distance=*/1.0}, + CompressedDCTestParams{/*butteraugli_distance=*/1.5}, + CompressedDCTestParams{/*butteraugli_distance=*/2.0}, + CompressedDCTestParams{/*butteraugli_distance=*/1.0, + /*fast_mode=*/true}, + CompressedDCTestParams{/*butteraugli_distance=*/1.5, + /*fast_mode=*/true}, + CompressedDCTestParams{/*butteraugli_distance=*/2.0, + /*fast_mode=*/true})); + +TEST_P(CompressedDCTest, Roundtrip) { + const std::string pathname = + GetTestDataPath("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + ThreadPoolInternal pool(8); + ASSERT_TRUE(SetFromFile(pathname, &io, &pool)); + + const CompressedDCTestParams& params = GetParam(); + + if (params.shrink8) { + io.ShrinkTo(io.xsize() / 8, io.ysize() / 8); + } + + CompressParams cparams; + cparams.butteraugli_distance = params.butteraugli_distance; + if (params.fast_mode) { + cparams.speed_tier = SpeedTier::kSquirrel; + } + DecompressParams dparams; + + Roundtrip(&io, cparams, dparams, &pool); +} + +} // namespace +} // namespace jxl diff --git a/jxl/compressed_image_test.cc b/jxl/compressed_image_test.cc new file mode 100644 index 000000000..68899f93f --- /dev/null +++ b/jxl/compressed_image_test.cc @@ -0,0 +1,120 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "gtest/gtest.h" +#include "jxl/ac_strategy.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/status.h" +#include "jxl/base/thread_pool_internal.h" +#include "jxl/chroma_from_luma.h" +#include "jxl/codec_in_out.h" +#include "jxl/color_encoding.h" +#include "jxl/color_management.h" +#include "jxl/common.h" +#include "jxl/enc_adaptive_quantization.h" +#include "jxl/enc_butteraugli_comparator.h" +#include "jxl/enc_cache.h" +#include "jxl/enc_params.h" +#include "jxl/enc_xyb.h" +#include "jxl/extras/codec.h" +#include "jxl/frame_header.h" +#include "jxl/gaborish.h" +#include "jxl/image.h" +#include "jxl/image_bundle.h" +#include "jxl/image_ops.h" +#include "jxl/loop_filter.h" +#include "jxl/multiframe.h" +#include "jxl/passes_state.h" +#include "jxl/quant_weights.h" +#include "jxl/quantizer.h" +#include "jxl/testdata_path.h" + +namespace jxl { +namespace { + +// Verifies ReconOpsinImage reconstructs with low butteraugli distance. +void RunRGBRoundTrip(float distance, bool fast) { + ThreadPoolInternal pool(4); + + const std::string& pathname = + GetTestDataPath("wesaturate/500px/u76c0g_bliznaca_srgb8.png"); + CodecInOut io; + JXL_CHECK(SetFromFile(pathname, &io, &pool)); + // This test can only handle a single group. + io.ShrinkTo(std::min(io.xsize(), kGroupDim), std::min(io.ysize(), kGroupDim)); + + Image3F opsin(io.xsize(), io.ysize()); + ToXYB(io.Main(), 1.0f, &pool, &opsin); + opsin = PadImageToMultiple(opsin, kBlockDim); + opsin = GaborishInverse(opsin, 1.0, &pool); + + CompressParams cparams; + cparams.butteraugli_distance = distance; + if (fast) { + cparams.speed_tier = SpeedTier::kSquirrel; + } + + FrameHeader frame_header; + frame_header.animation_frame.nonserialized_have_timecode = false; + LoopFilter loop_filter; + loop_filter.gab = true; + loop_filter.gab_custom = true; + loop_filter.gab_x_weight1 = 0.11501538179658321; + loop_filter.gab_x_weight2 = 0.089979079587015454; + loop_filter.gab_y_weight1 = 0.11501538179658321; + loop_filter.gab_y_weight2 = 0.089979079587015454; + loop_filter.gab_b_weight1 = 0.11501538179658321; + loop_filter.gab_b_weight2 = 0.089979079587015454; + loop_filter.epf = false; + + FrameDimensions frame_dim; + frame_dim.Set(opsin.xsize(), opsin.ysize()); + Multiframe multiframe; + PassesEncoderState enc_state; + JXL_CHECK(InitializePassesSharedState(frame_header, loop_filter, io.metadata, + frame_dim, &multiframe, + &enc_state.shared)); + + enc_state.x_qm_multiplier = 1.0f; + enc_state.shared.quantizer.SetQuant(4.0f, 4.0f, + &enc_state.shared.raw_quant_field); + enc_state.shared.ac_strategy.FillDCT8(); + enc_state.cparams = cparams; + ZeroFillImage(&enc_state.shared.epf_sharpness); + Image3F recon = RoundtripImage(opsin, &enc_state, &pool); + + CodecInOut io1; + io1.metadata.bits_per_sample = io.metadata.bits_per_sample; + io1.metadata.color_encoding = ColorManagement::LinearSRGB(); + io1.SetFromImage(std::move(recon), io1.metadata.color_encoding); + + EXPECT_LE(ButteraugliDistance(io, io1, cparams.hf_asymmetry, + /*distmap=*/nullptr, &pool), + 1.2); +} + +TEST(CompressedImageTest, RGBRoundTrip_1) { RunRGBRoundTrip(1.0, false); } + +TEST(CompressedImageTest, RGBRoundTrip_1_fast) { RunRGBRoundTrip(1.0, true); } + +TEST(CompressedImageTest, RGBRoundTrip_2) { RunRGBRoundTrip(2.0, false); } + +TEST(CompressedImageTest, RGBRoundTrip_2_fast) { RunRGBRoundTrip(2.0, true); } + +} // namespace +} // namespace jxl diff --git a/jxl/convolve.h b/jxl/convolve.h new file mode 100644 index 000000000..4aaa16618 --- /dev/null +++ b/jxl/convolve.h @@ -0,0 +1,1265 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef JXL_CONVOLVE_H_ +#define JXL_CONVOLVE_H_ + +// Fast SIMD 2D convolution. + +#include +#include +#include + +#include +#include +#include + +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/profiler.h" +#include "jxl/base/status.h" +#include "jxl/image.h" +#include "jxl/image_ops.h" + +namespace jxl { + +// Usable by any 3x3 kernel; applied as-is without flipping. +struct Weights3x3 { + // top/middle/bottom left/center/right, replicated 4x via HWY_REP4. + float tl[4]; + float tc[4]; + float tr[4]; + float ml[4]; + float mc[4]; + float mr[4]; + float bl[4]; + float bc[4]; + float br[4]; +}; + +struct WeightsSeparable5 { + // Horizontal 1D, distances 0..2, each replicated 4x. + float horz[3 * 4]; + float vert[3 * 4]; +}; + +struct WeightsSeparable7 { + // Horizontal 1D, distances 0..3, each replicated 4x. + float horz[4 * 4]; + float vert[4 * 4]; +}; + +// For code-folding. +namespace kernel { + +// Holds weights computed at runtime (e.g. inverse of another kernel). +class Variable3 { + public: + explicit Variable3(const float tl, const float tc, const float tr, + const float ml, const float mc, const float mr, + const float bl, const float bc, const float br) { + for (size_t i = 0; i < 4; ++i) { + weights_.tl[i] = tl; + weights_.tc[i] = tc; + weights_.tr[i] = tr; + weights_.ml[i] = ml; + weights_.mc[i] = mc; + weights_.mr[i] = mr; + weights_.bl[i] = bl; + weights_.bc[i] = bc; + weights_.br[i] = br; + } + } + + JXL_INLINE const Weights3x3& Weights() const { return weights_; } + + private: + Weights3x3 weights_; +}; + +// Concentrates energy in low-frequency components (e.g. for antialiasing). +struct Lowpass3 { + JXL_INLINE const Weights3x3& Weights() const { + // Computed by research/convolve_weights.py's cubic spline approximations of + // prolate spheroidal wave functions. + constexpr float w0 = 0.36208932f; + constexpr float w1 = 0.12820096f; + constexpr float w2 = 0.03127668f; + static constexpr Weights3x3 weights = { + {HWY_REP4(w2)}, {HWY_REP4(w1)}, {HWY_REP4(w2)}, + {HWY_REP4(w1)}, {HWY_REP4(w0)}, {HWY_REP4(w1)}, + {HWY_REP4(w2)}, {HWY_REP4(w1)}, {HWY_REP4(w2)}}; + return weights; + } +}; + +struct Lowpass5 { + JXL_INLINE const WeightsSeparable5& Weights() const { + constexpr float w0 = 0.41714928f; + constexpr float w1 = 0.25539268f; + constexpr float w2 = 0.03603267f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; + } +}; + +struct Gaussian5Sigma1 { + JXL_INLINE const WeightsSeparable5& Weights() const { + constexpr float w0 = 0.38774f; + constexpr float w1 = 0.24477f; + constexpr float w2 = 0.06136f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; + } +}; + +struct Gaussian5Sigma2 { + JXL_INLINE const WeightsSeparable5& Weights() const { + constexpr float w0 = 0.250301f; + constexpr float w1 = 0.221461f; + constexpr float w2 = 0.153388f; + static constexpr WeightsSeparable5 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)}}; + return weights; + } +}; + +struct Gaussian7Sigma8 { + JXL_INLINE const WeightsSeparable7& Weights() const { + constexpr float w0 = 0.147332f; + constexpr float w1 = 0.146187f; + constexpr float w2 = 0.142805f; + constexpr float w3 = 0.137341f; + static constexpr WeightsSeparable7 weights = { + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2), HWY_REP4(w3)}, + {HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2), HWY_REP4(w3)}}; + return weights; + } +}; + +} // namespace kernel + +// Non-vectorized implementations for validation. +namespace slow { + +// Separable kernels, any radius. +template +class SeparableConvolution { + public: + template + static void Run(const ImageF& in, const Rect& rect, const Kernel& kernel, + ImageF* out) { + PROFILER_ZONE("slow::Separable::Run"); + const float* horz_weights = &kernel.Weights().horz[0]; + const float* vert_weights = &kernel.Weights().vert[0]; + for (size_t y = 0; y < rect.ysize(); ++y) { + float* const JXL_RESTRICT row_out = out->Row(y); + for (size_t x = 0; x < rect.xsize(); ++x) { + row_out[x] = ConvolvePixel(in, rect, x, y, horz_weights, vert_weights); + } + } + } + + template + static void Run(const Image3F& in, const Rect& rect, const Kernel& kernel, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + Run(in.Plane(c), rect, kernel, const_cast(&out->Plane(c))); + } + } + + private: + static float ConvolvePixel(const ImageF& in, const Rect& rect, + const int64_t x, const int64_t y, + const float* JXL_RESTRICT horz_weights, + const float* JXL_RESTRICT vert_weights) { + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + + float mul = 0.0f; + for (int dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = vert_weights[std::abs(dy) * 4]; + const size_t sy = Wrap()(y + dy, ysize); + JXL_CHECK(sy < ysize); + const float* const JXL_RESTRICT row = rect.ConstRow(in, sy); + for (int dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = horz_weights[std::abs(dx) * 4]; + const size_t sx = Wrap()(x + dx, xsize); + JXL_CHECK(sx < xsize); + mul += row[sx] * wx * wy; + } + } + return mul; + } +}; + +// Hardcoded 5x5 "Laplacian". +template +struct Laplacian5 { + static_assert(kRadius == 2, "Wrong kRadius"); + + template + static void Run(const ImageF& in, const Rect& rect, const Kernel& kernel, + ImageF* out) { + PROFILER_ZONE("slow::Laplacian5::Run"); + JXL_CHECK(SameSize(rect, *out)); + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + for (int64_t y = 0; y < ysize; ++y) { + const float* const JXL_RESTRICT row_t = + rect.ConstRow(in, Wrap()(y - 2, ysize)); + const float* const JXL_RESTRICT row_m = rect.ConstRow(in, y); + const float* const JXL_RESTRICT row_b = + rect.ConstRow(in, Wrap()(y + 2, ysize)); + float* const JXL_RESTRICT row_out = out->Row(y); + + for (int64_t x = 0; x < xsize; ++x) { + const int64_t xm2 = Wrap()(x - 2, xsize); + const int64_t xp2 = Wrap()(x + 2, xsize); + float r = 0.0f; + r += /* */ 1.0f * row_t[x]; + r += 1.0f * row_m[xm2] - 4.0f * row_m[x] + 1.0f * row_m[xp2]; + r += /* */ 1.0f * row_b[x]; + row_out[x] = r; + } + } + } + + template + static void Run(const Image3F& in, const Rect& rect, const Kernel& kernel, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + Run(in.Plane(c), rect, kernel, const_cast(&out->Plane(c))); + } + } +}; + +template +struct General3x3Convolution { + static_assert(kRadius == 1, "Wrong kRadius"); + + template + static void Run(const ImageF& in, const Rect& rect, const Kernel& kernel, + ImageF* out) { + PROFILER_ZONE("slow::General3::Run"); + JXL_CHECK(SameSize(rect, *out)); + const Weights3x3& weights = kernel.Weights(); + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + for (int64_t y = 0; y < ysize; ++y) { + const float* const JXL_RESTRICT row_t = + rect.ConstRow(in, Wrap()(y - 1, ysize)); + const float* const JXL_RESTRICT row_m = rect.ConstRow(in, y); + const float* const JXL_RESTRICT row_b = + rect.ConstRow(in, Wrap()(y + 1, ysize)); + float* const JXL_RESTRICT row_out = out->Row(y); + + for (int64_t x = 0; x < xsize; ++x) { + const int64_t xm1 = Wrap()(x - 1, xsize); + const int64_t xp1 = Wrap()(x + 1, xsize); + const float tl = row_t[xm1]; + const float ml = row_m[xm1]; + const float bl = row_b[xm1]; + const float tr = row_t[xp1]; + const float mr = row_m[xp1]; + const float br = row_b[xp1]; + float r = 0.0f; + r += tl * weights.tl[0] + row_t[x] * weights.tc[0] + tr * weights.tr[0]; + r += ml * weights.ml[0] + row_m[x] * weights.mc[0] + mr * weights.mr[0]; + r += bl * weights.bl[0] + row_b[x] * weights.bc[0] + br * weights.br[0]; + row_out[x] = r; + } + } + } + + template + static void Run(const Image3F& in, const Rect& rect, const Kernel& kernel, + Image3F* out) { + for (size_t c = 0; c < 3; ++c) { + Run(in.Plane(c), rect, kernel, const_cast(&out->Plane(c))); + } + } +}; + +// Slow N*R^2 algorithm in case weights are not separable, but avoids +// bounds-checking overhead for interior pixels. Weights are the lower-right +// quadrant of the kernel and need not be pre-normalized. +template +class SymmetricConvolution { + public: + static void Run(const ImageF& in, const Rect& rect, + const float (&weights)[(kRadius + 1) * (kRadius + 1)], + ThreadPool* pool, ImageF* JXL_RESTRICT out) { + PROFILER_ZONE("slow::Symmetric::Run"); + + float normalized[(kRadius + 1) * (kRadius + 1)]; + NormalizeWeights(weights, &normalized); + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const int64_t iy = task; + + if (iy < kRadius || iy >= ysize - kRadius) { + ConvolveRow(in, rect, iy, normalized, out->Row(iy)); + } else { + ConvolveRow(in, rect, iy, normalized, out->Row(iy)); + } + }, + "Symmetric conv"); + } + + static void Run(const Image3F& in, const Rect& rect, + const float (&weights)[(kRadius + 1) * (kRadius + 1)], + ThreadPool* pool, Image3F* JXL_RESTRICT out) { + PROFILER_ZONE("slow::Symmetric::Run3"); + + float normalized[(kRadius + 1) * (kRadius + 1)]; + NormalizeWeights(weights, &normalized); + const size_t ysize = rect.ysize(); + RunOnPool( + pool, 0, static_cast(ysize), ThreadPool::SkipInit(), + [&](const int task, int /*thread*/) { + const size_t iy = task; + + if (iy < kRadius || iy >= ysize - kRadius) { + for (size_t c = 0; c < 3; ++c) { + ConvolveRow(in.Plane(c), rect, iy, normalized, + out->PlaneRow(c, iy)); + } + } else { + for (size_t c = 0; c < 3; ++c) { + ConvolveRow(in.Plane(c), rect, iy, normalized, + out->PlaneRow(c, iy)); + } + } + }, + "Symmetric conv3"); + } + + private: + // Expands quadrant into entire kernel and normalizes. + static void NormalizeWeights( + const float (&weights)[(kRadius + 1) * (kRadius + 1)], + float (*normalized)[(kRadius + 1) * (kRadius + 1)]) { + double sum = 0.0; + for (int64_t ky = -kRadius; ky <= kRadius; ky++) { + const int64_t wy = std::abs(ky); + for (int64_t kx = -kRadius; kx <= kRadius; kx++) { + const int64_t wx = std::abs(kx); + sum += weights[wy * (kRadius + 1) + wx]; + } + } + const float mul = sum == 0.0f ? 1.0f : 1.0 / sum; + for (size_t i = 0; i < (kRadius + 1) * (kRadius + 1); ++i) { + (*normalized)[i] = weights[i] * mul; + } + } + + template + static float ConvolvePixel( + const ImageF& in, const Rect& rect, const int64_t ix, const int64_t iy, + const float (&weights)[(kRadius + 1) * (kRadius + 1)]) { + float sum = 0.0; + + // ix: image; kx: kernel; wx: weight + for (int64_t ky = -kRadius; ky <= kRadius; ky++) { + const int64_t wy = std::abs(ky); + const int64_t y = WrapY()(iy + ky, rect.ysize()); + const float* JXL_RESTRICT row_in = in.ConstRow(y); + + for (int64_t kx = -kRadius; kx <= kRadius; kx++) { + const int64_t wx = std::abs(kx); + const int64_t x = WrapX()(ix + kx, rect.xsize()); + + sum += row_in[x] * weights[wy * (kRadius + 1) + wx]; + } + } + return sum; + } + + template + static inline void ConvolveRow( + const ImageF& in, const Rect& rect, const int64_t iy, + const float (&weights)[(kRadius + 1) * (kRadius + 1)], + float* JXL_RESTRICT row_out) { + const size_t xsize = rect.xsize(); + size_t ix = 0; + for (; ix < kRadius; ix++) { + row_out[ix] = ConvolvePixel(in, rect, ix, iy, weights); + } + for (; ix < xsize - kRadius; ix++) { + row_out[ix] = + ConvolvePixel(in, rect, ix, iy, weights); + } + for (; ix < xsize; ix++) { + row_out[ix] = ConvolvePixel(in, rect, ix, iy, weights); + } + } +}; + +} // namespace slow + +// Synthesizes left/right neighbors from a vector of center pixels. +class Neighbors { + public: + // TODO(janwas): AVX512 +#if HWY_BITS >= 256 + using D = HWY_CAPPED(float, 8); +#else + using D = HWY_CAPPED(float, 4); +#endif + using V = hwy::VT; + + // Returns l[i] == c[Mirror(i - 1)]. + static HWY_ATTR JXL_INLINE V FirstL1(const V c) { +#if HWY_BITS >= 256 + const D d; + HWY_ALIGN constexpr int lanes[8] = {0, 0, 1, 2, 3, 4, 5, 6}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // ONML'KJII +#elif HWY_BITS == 128 + // c = LKJI +#if HWY_ARCH == HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(2, 1, 0, 0))}; // KJII +#else + const D d; + // TODO(deymo): Figure out if this can be optimized using a single vsri + // instruction to convert LKJI to KJII. + HWY_ALIGN constexpr int lanes[4] = {0, 0, 1, 2}; // KJII + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#else + return c; // Same (the first mirrored value is the last valid one) +#endif + } + + // Returns l[i] == c[Mirror(i - 2)]. + static HWY_ATTR JXL_INLINE V FirstL2(const V c) { +#if HWY_BITS >= 256 + const D d; + HWY_ALIGN constexpr int lanes[8] = {1, 0, 0, 1, 2, 3, 4, 5}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // NMLK'JIIJ +#elif HWY_BITS == 128 + // c = LKJI +#if HWY_ARCH == HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(1, 0, 0, 1))}; // JIIJ +#else + const D d; + HWY_ALIGN constexpr int lanes[4] = {1, 0, 0, 1}; // JIIJ + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#else + const D d; + JXL_ASSERT(false); // unsupported, avoid calling this. + return Zero(d); +#endif + } + + // Returns l[i] == c[Mirror(i - 3)]. + static HWY_ATTR JXL_INLINE V FirstL3(const V c) { +#if HWY_BITS >= 256 + const D d; + HWY_ALIGN constexpr int lanes[8] = {2, 1, 0, 0, 1, 2, 3, 4}; + const auto indices = SetTableIndices(d, lanes); + // c = PONM'LKJI + return TableLookupLanes(c, indices); // MLKJ'IIJK +#elif HWY_BITS == 128 + // c = LKJI +#if HWY_ARCH == HWY_ARCH_X86 + return V{_mm_shuffle_ps(c.raw, c.raw, _MM_SHUFFLE(0, 0, 1, 2))}; // IIJK +#else + const D d; + HWY_ALIGN constexpr int lanes[4] = {2, 1, 0, 0}; // IIJK + const auto indices = SetTableIndices(d, lanes); + return TableLookupLanes(c, indices); +#endif +#else + const D d; + JXL_ASSERT(false); // unsupported, avoid calling this. + return Zero(d); +#endif + } + + // Returns r[i] == c[i + 1]. + static HWY_ATTR JXL_INLINE V R1(const V c, const V n) { +#if HWY_BITS >= 256 + // c = PONM'LKJI, n = xxxx'xxxQ + const V Q_M = ConcatLoHi(n, c); // Right-aligned (lower lane) + return hwy::CombineShiftRightBytes<4>(Q_M, c); // QPON'MLKJ +#elif HWY_BITS == 128 + // c = LKJI, n = xxxM + return hwy::CombineShiftRightBytes<4>(n, c); // MLKJ +#else + return n; +#endif + } + + // Returns r[i] == c[i + 1]. + static HWY_ATTR JXL_INLINE V LastR1(const V c) { +#if HWY_BITS >= 256 + HWY_ALIGN constexpr uint32_t lanes[8] = {1, 2, 3, 4, 5, 6, 7, 7}; + const auto indices = Load(HWY_CAPPED(uint32_t, 8)(), lanes); + // c = PONM'LKJI + return V{_mm256_permutevar8x32_ps(c.raw, indices.raw)}; // PPON'MLKJ +#elif HWY_BITS == 128 + // c = LKJI + const auto L = hwy::Broadcast<3>(c); + return hwy::CombineShiftRightBytes<4>(L, c); // LLKJ +#else + return c; +#endif + } +}; + +// No valid values outside [0, xsize), but the strategy may still safely load +// the preceding vector, and/or round xsize up to the vector lane count. This +// avoids needing PadImage. +// Requires xsize >= kConvolveLanes + kConvolveMaxRadius. +static constexpr size_t kConvolveMaxRadius = 3; + +// For use by SetTableIndices. +static inline const int32_t* MirrorLanes(const size_t mod) { +#if HWY_BITS >= 256 + HWY_CAPPED(float, 8) d; + // last part mirrored + // 01234567| 76543210 loadedReg 76543210 mirroredReg 01234567 + // 01234567|8 8765432 loadedReg 87654321 mirroredReg 23456788 + // 01234567|89 987654 loadedReg 98765432 mirroredReg 45678998 + // 01234567|89A A9876 loadedReg A9876543 mirroredReg 6789AA98 + // 01234567|89AB BA98 + // 01234567|89ABC CBA + // 01234567|89ABCD DC + // 01234567|89ABCDE E loadedReg EDCBA987 mirroredReg EEDCBA98 + HWY_ALIGN static constexpr int32_t idx_lanes[d.N * d.N] = { + 7, 6, 5, 4, 3, 2, 1, 0, // 0 + 7, 7, 6, 5, 4, 3, 2, 1, // 1 + 6, 7, 7, 6, 5, 4, 3, 2, // 2 + 5, 6, 7, 7, 6, 5, 4, 3, // 3 + 4, 5, 6, 7, 7, 6, 5, 4, // 4 + 3, 4, 5, 6, 7, 7, 6, 5, // 5 + 2, 3, 4, 5, 6, 7, 7, 6, // 6 + 1, 2, 3, 4, 5, 6, 7, 7, // 7 + }; + return idx_lanes + mod * d.N; +#elif HWY_BITS == 128 + HWY_CAPPED(float, 4) d; + // 0123| 3210 loadedReg 3210 mirroredReg 0123 + // 0123|4 432 loadedReg 4321 mirroredReg 2344 + // 0123|45 54 loadedReg 5432 mirroredReg 4554 + // 0123|456 6 loadedReg 6543 mirroredReg 6654 + HWY_ALIGN static constexpr int32_t idx_lanes[d.N * d.N] = { + 3, 2, 1, 0, // 0 + 3, 3, 2, 1, // 1 + 2, 3, 3, 2, // 2 + 1, 2, 3, 3, // 3 + }; + return idx_lanes + mod * d.N; +#else + return nullptr; // do not call +#endif +} + +namespace strategy { + +struct StrategyBase { +#if HWY_BITS >= 256 + using D = HWY_CAPPED(float, 8); +#else + using D = HWY_CAPPED(float, 4); +#endif + using V = hwy::VT; +}; + +// 3x3 convolution by symmetric kernel with a single scan through the input. +class Symmetric3 : public StrategyBase { + public: + static constexpr int64_t kRadius = 1; + + // Only accesses pixels in [0, xsize). + template + static HWY_ATTR JXL_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, const Weights3x3& weights, + float* const JXL_RESTRICT row_out) { + const D d; + // t, m, b = top, middle, bottom row; + const float* const JXL_RESTRICT row_t = wrap_row(row_m - stride, stride); + const float* const JXL_RESTRICT row_b = wrap_row(row_m + stride, stride); + + // Must load in advance - compiler doesn't understand LoadDup128 and + // schedules them too late. + const V w0 = LoadDup128(d, weights.mc); + const V w1 = LoadDup128(d, weights.tc); + const V w2 = LoadDup128(d, weights.tl); + + // l, c, r = left, center, right. Leftmost vector: need FirstL1. + { + const V tc = LoadU(d, row_t + 0); + const V mc = LoadU(d, row_m + 0); + const V bc = LoadU(d, row_b + 0); + const V tl = Neighbors::FirstL1(tc); + const V tr = LoadU(d, row_t + 0 + 1); + const V ml = Neighbors::FirstL1(mc); + const V mr = LoadU(d, row_m + 0 + 1); + const V bl = Neighbors::FirstL1(bc); + const V br = LoadU(d, row_b + 0 + 1); + const V conv = + WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + 0); + } + + // Loop as long as we can load enough new values: + size_t x = d.N; + for (; x + d.N + kRadius <= xsize; x += d.N) { + const auto conv = ConvolveValid(row_t, row_m, row_b, x, w0, w1, w2); + Store(conv, d, row_out + x); + } + + // For final (partial) vector: + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + + V tr, mr, br; +#if HWY_BITS == 0 + tr = tc; // Single-lane => mirrored right neighbor = center value. + mr = mc; + br = bc; +#else + if (kSizeModN == 0) { + // The above loop didn't handle the last vector because it needs an + // additional right neighbor (generated via mirroring). + auto mirror = SetTableIndices(d, MirrorLanes(d.N - 1)); + tr = TableLookupLanes(tc, mirror); + mr = TableLookupLanes(mc, mirror); + br = TableLookupLanes(bc, mirror); + } else { + auto mirror = SetTableIndices(d, MirrorLanes((xsize % d.N) - 1)); + // Loads last valid value into uppermost lane and mirrors. + tr = TableLookupLanes(LoadU(d, row_t + xsize - d.N), mirror); + mr = TableLookupLanes(LoadU(d, row_m + xsize - d.N), mirror); + br = TableLookupLanes(LoadU(d, row_b + xsize - d.N), mirror); + } +#endif + + const V tl = LoadU(d, row_t + x - 1); + const V ml = LoadU(d, row_m + x - 1); + const V bl = LoadU(d, row_b + x - 1); + const V conv = WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + Store(conv, d, row_out + x); + } + + private: + // Returns sum{x_i * w_i}. + template + static HWY_ATTR JXL_INLINE V WeightedSum(const V tl, const V tc, const V tr, + const V ml, const V mc, const V mr, + const V bl, const V bc, const V br, + const V w0, const V w1, const V w2) { + const V sum_tb = tc + bc; + + // Faster than 5 mul + 4 FMA. + const V mul0 = mc * w0; + const V sum_lr = ml + mr; + + const V x1 = sum_tb + sum_lr; + const V mul1 = MulAdd(x1, w1, mul0); + + const V sum_t2 = tl + tr; + const V sum_b2 = bl + br; + const V x2 = sum_t2 + sum_b2; + const V mul2 = MulAdd(x2, w2, mul1); + return mul2; + } + + static HWY_ATTR JXL_INLINE V ConvolveValid(const float* JXL_RESTRICT row_t, + const float* JXL_RESTRICT row_m, + const float* JXL_RESTRICT row_b, + const int64_t x, const V w0, + const V w1, const V w2) { + const D d; + const V tc = LoadU(d, row_t + x); + const V mc = LoadU(d, row_m + x); + const V bc = LoadU(d, row_b + x); + const V tl = LoadU(d, row_t + x - 1); + const V tr = LoadU(d, row_t + x + 1); + const V ml = LoadU(d, row_m + x - 1); + const V mr = LoadU(d, row_m + x + 1); + const V bl = LoadU(d, row_b + x - 1); + const V br = LoadU(d, row_b + x + 1); + return WeightedSum(tl, tc, tr, ml, mc, mr, bl, bc, br, w0, w1, w2); + } +}; + +// 5x5 convolution by separable kernel with a single scan through the input. +class Separable5 : public StrategyBase { + public: + static constexpr int64_t kRadius = 2; + + template + static HWY_ATTR JXL_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, + const WeightsSeparable5& weights, float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + + size_t x = 0; + + // Need to loop more than once for scalars (d.N == 1). + for (; x < kRadius; x += d.N) { + const V conv0 = HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2) * wv0; + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + d.N + kRadius <= xsize; x += d.N) { + const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2) * wv0; + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_BITS == 0 + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + HorzConvolveLast(row_m, x, xsize, wh0, wh1, wh2) * wv0; + + const V conv1t = + HorzConvolveLast(row_t1, x, xsize, wh0, wh1, wh2); + const V conv1b = + HorzConvolveLast(row_b1, x, xsize, wh0, wh1, wh2); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = + HorzConvolveLast(row_t2, x, xsize, wh0, wh1, wh2); + const V conv2b = + HorzConvolveLast(row_b2, x, xsize, wh0, wh1, wh2); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + Store(conv2, d, row_out + x); + x += d.N; + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static HWY_ATTR JXL_INLINE V HorzConvolveFirst( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + +#if HWY_BITS == 0 + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); +#else + (void)xsize; + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); +#endif + + const V r1 = LoadU(d, row + x + 1); + const V r2 = LoadU(d, row + x + 2); + + const V mul1 = MulAdd(l1 + r1, wh1, mul0); + const V mul2 = MulAdd(l2 + r2, wh2, mul1); + return mul2; + } + + template + static HWY_ATTR JXL_INLINE V + HorzConvolveLast(const float* const JXL_RESTRICT row, const int64_t x, + const int64_t xsize, const V wh0, const V wh1, const V wh2) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + + V r1, r2; +#if HWY_BITS == 0 + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); +#else + if (kSizeModN == 0) { + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(d.N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(d.N - 1))); + } else { // == 1 + const auto last = LoadU(d, row + xsize - d.N); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(d.N - 1))); + r1 = last; + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } + + // Requires kRadius valid pixels before/after pos. + static HWY_ATTR JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, + const V wh2) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = c * wh0; + + // Loading anew is faster than combining vectors. + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + return mul2; + } +}; // namespace strategy + +// 7x7 convolution by separable kernel with a single scan through the input. +class Separable7 : public StrategyBase { + public: + static constexpr int64_t kRadius = 3; + + template + static HWY_ATTR JXL_INLINE void ConvolveRow( + const float* const JXL_RESTRICT row_m, const size_t xsize, + const int64_t stride, const WrapRow& wrap_row, + const WeightsSeparable7& weights, float* const JXL_RESTRICT row_out) { + const D d; + const int64_t neg_stride = -stride; // allows LEA addressing. + const float* const JXL_RESTRICT row_t3 = + wrap_row(row_m + 3 * neg_stride, stride); + const float* const JXL_RESTRICT row_t2 = + wrap_row(row_m + 2 * neg_stride, stride); + const float* const JXL_RESTRICT row_t1 = + wrap_row(row_m + 1 * neg_stride, stride); + const float* const JXL_RESTRICT row_b1 = + wrap_row(row_m + 1 * stride, stride); + const float* const JXL_RESTRICT row_b2 = + wrap_row(row_m + 2 * stride, stride); + const float* const JXL_RESTRICT row_b3 = + wrap_row(row_m + 3 * stride, stride); + + const V wh0 = LoadDup128(d, weights.horz + 0 * 4); + const V wh1 = LoadDup128(d, weights.horz + 1 * 4); + const V wh2 = LoadDup128(d, weights.horz + 2 * 4); + const V wh3 = LoadDup128(d, weights.horz + 3 * 4); + const V wv0 = LoadDup128(d, weights.vert + 0 * 4); + const V wv1 = LoadDup128(d, weights.vert + 1 * 4); + const V wv2 = LoadDup128(d, weights.vert + 2 * 4); + const V wv3 = LoadDup128(d, weights.vert + 3 * 4); + + size_t x = 0; + + // Need to loop more than once for scalars (d.N == 1). + for (; x < kRadius; x += d.N) { + const V conv0 = + HorzConvolveFirst(row_m, x, xsize, wh0, wh1, wh2, wh3) * wv0; + + const V conv1t = HorzConvolveFirst(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolveFirst(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolveFirst(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolveFirst(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = HorzConvolveFirst(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolveFirst(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Main loop: load inputs without padding + for (; x + d.N + kRadius <= xsize; x += d.N) { + const V conv0 = HorzConvolve(row_m + x, wh0, wh1, wh2, wh3) * wv0; + + const V conv1t = HorzConvolve(row_t1 + x, wh0, wh1, wh2, wh3); + const V conv1b = HorzConvolve(row_b1 + x, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = HorzConvolve(row_t2 + x, wh0, wh1, wh2, wh3); + const V conv2b = HorzConvolve(row_b2 + x, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = HorzConvolve(row_t3 + x, wh0, wh1, wh2, wh3); + const V conv3b = HorzConvolve(row_b3 + x, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + } + + // Last full vector to write (the above loop handled mod >= kRadius) +#if HWY_BITS == 0 + while (x < xsize) { +#else + if (kSizeModN < kRadius) { +#endif + const V conv0 = + HorzConvolveLast(row_m, x, xsize, wh0, wh1, wh2, wh3) * + wv0; + + const V conv1t = + HorzConvolveLast(row_t1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1b = + HorzConvolveLast(row_b1, x, xsize, wh0, wh1, wh2, wh3); + const V conv1 = MulAdd(conv1t + conv1b, wv1, conv0); + + const V conv2t = + HorzConvolveLast(row_t2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2b = + HorzConvolveLast(row_b2, x, xsize, wh0, wh1, wh2, wh3); + const V conv2 = MulAdd(conv2t + conv2b, wv2, conv1); + + const V conv3t = + HorzConvolveLast(row_t3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3b = + HorzConvolveLast(row_b3, x, xsize, wh0, wh1, wh2, wh3); + const V conv3 = MulAdd(conv3t + conv3b, wv3, conv2); + + Store(conv3, d, row_out + x); + x += d.N; + } + + // If mod = 0, the above vector was the last. + if (kSizeModN != 0) { + for (; x < xsize; ++x) { + float mul = 0.0f; + for (int64_t dy = -kRadius; dy <= kRadius; ++dy) { + const float wy = weights.vert[std::abs(dy) * 4]; + const float* clamped_row = wrap_row(row_m + dy * stride, stride); + for (int64_t dx = -kRadius; dx <= kRadius; ++dx) { + const float wx = weights.horz[std::abs(dx) * 4]; + const int64_t clamped_x = Mirror(x + dx, xsize); + mul += clamped_row[clamped_x] * wx * wy; + } + } + row_out[x] = mul; + } + } + } + + private: + // Same as HorzConvolve for the first/last vector in a row. + static HWY_ATTR JXL_INLINE V HorzConvolveFirst( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + +#if HWY_BITS == 0 + const V l1 = LoadU(d, row + Mirror(x - 1, xsize)); + const V l2 = LoadU(d, row + Mirror(x - 2, xsize)); + const V l3 = LoadU(d, row + Mirror(x - 3, xsize)); +#else + const V l1 = Neighbors::FirstL1(c); + const V l2 = Neighbors::FirstL2(c); + const V l3 = Neighbors::FirstL3(c); +#endif + + const V r1 = LoadU(d, row + Mirror(x + 1, xsize)); + const V r2 = LoadU(d, row + Mirror(x + 2, xsize)); + const V r3 = LoadU(d, row + Mirror(x + 3, xsize)); + + const V mul1 = MulAdd(l1 + r1, wh1, mul0); + const V mul2 = MulAdd(l2 + r2, wh2, mul1); + const V mul3 = MulAdd(l3 + r3, wh3, mul2); + return mul3; + } + + template + static HWY_ATTR JXL_INLINE V HorzConvolveLast( + const float* const JXL_RESTRICT row, const int64_t x, const int64_t xsize, + const V wh0, const V wh1, const V wh2, const V wh3) { + const D d; + const V c = LoadU(d, row + x); + const V mul0 = c * wh0; + + JXL_DASSERT(x >= kRadius); + const V l1 = LoadU(d, row + x - 1); + const V l2 = LoadU(d, row + x - 2); + const V l3 = LoadU(d, row + x - 3); + + V r1, r2, r3; +#if HWY_BITS == 0 + r1 = LoadU(d, row + Mirror(x + 1, xsize)); + r2 = LoadU(d, row + Mirror(x + 2, xsize)); + r3 = LoadU(d, row + Mirror(x + 3, xsize)); +#else + switch (kSizeModN) { + case 0: + r3 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(d.N - 3))); + r2 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(d.N - 2))); + r1 = TableLookupLanes(c, SetTableIndices(d, MirrorLanes(d.N - 1))); + break; + case 1: { + const auto last = LoadU(d, row + xsize - d.N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(d.N - 2))); + r2 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(d.N - 1))); + r1 = last; + break; + } + default: + JXL_DASSERT(kSizeModN == 2); + { + const auto last = LoadU(d, row + xsize - d.N); + r3 = TableLookupLanes(last, SetTableIndices(d, MirrorLanes(d.N - 1))); + r2 = last; + r1 = LoadU(d, row + x + 1); + break; + } + } +#endif + + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = l3 + r3; + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } + + // Requires kRadius valid pixels before/after pos. + static HWY_ATTR JXL_INLINE V HorzConvolve(const float* const JXL_RESTRICT pos, + const V wh0, const V wh1, + const V wh2, const V wh3) { + const D d; + const V c = LoadU(d, pos); + const V mul0 = c * wh0; + + // Loading anew is faster than combining vectors. + const V l1 = LoadU(d, pos - 1); + const V r1 = LoadU(d, pos + 1); + const V l2 = LoadU(d, pos - 2); + const V r2 = LoadU(d, pos + 2); + const V l3 = LoadU(d, pos - 3); + const V r3 = LoadU(d, pos + 3); + // Sum of pixels with Manhattan distance i, multiplied by weights[i]. + const V sum1 = l1 + r1; + const V mul1 = MulAdd(sum1, wh1, mul0); + const V sum2 = l2 + r2; + const V mul2 = MulAdd(sum2, wh2, mul1); + const V sum3 = l3 + r3; + const V mul3 = MulAdd(sum3, wh3, mul2); + return mul3; + } +}; // namespace jxl + +} // namespace strategy + +// TODO(janwas): AVX-512 +static constexpr size_t kConvolveLanes = HWY_CAPPED(float, 8)::N; +// 3x3 kernels require inputs at least this wide - for the first vector, they +// load right neighbors (N lanes starting from x + 1). +static constexpr size_t kConvolveMinWidth = kConvolveLanes + 1; + +// Single entry point for convolution. +// "Strategy" (Direct*/Separable*) decides kernel size and how to evaluate it. +template +class ConvolveT { + static constexpr int64_t kRadius = Strategy::kRadius; + + public: + // "Image" is ImageF or Image3F. + template + static HWY_ATTR void Run(const Image& in, const Rect& rect, + const Kernel& kernel, ThreadPool* pool, + const Image* out) { + PROFILER_ZONE("ConvolveT::Run"); + JXL_CHECK(SameSize(rect, *out)); + JXL_CHECK(rect.xsize() >= kConvolveMinWidth); + + static_assert(int64_t(kRadius) <= 3, + "Must handle [0, kRadius) and >= kRadius"); + switch (rect.xsize() % kConvolveLanes) { + case 0: + return RunRows<0>(in, rect, kernel, pool, out); + case 1: + return RunRows<1>(in, rect, kernel, pool, out); + case 2: + return RunRows<2>(in, rect, kernel, pool, out); + default: + return RunRows<3>(in, rect, kernel, pool, out); + } + } + + private: + template + static HWY_ATTR JXL_INLINE void RunRow(const float* JXL_RESTRICT in, + const size_t xsize, + const int64_t stride, + const WrapRow& wrap_row, + const Kernel& kernel, + const float* JXL_RESTRICT out) { + Strategy::template ConvolveRow( + in, xsize, stride, wrap_row, kernel.Weights(), const_cast(out)); + } + + template + static HWY_ATTR JXL_INLINE void RunBorderRows( + const ImageF& in, const Rect& rect, const int64_t ybegin, + const int64_t yend, const Kernel& kernel, const ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + const WrapRowMirror wrap_row(in, rect.ysize()); + for (int64_t y = ybegin; y < yend; ++y) { + RunRow(rect.ConstRow(in, y), rect.xsize(), stride, wrap_row, + kernel, out->Row(y)); + } + } + + // Image3F. + template + static HWY_ATTR JXL_INLINE void RunBorderRows( + const Image3F& in, const Rect& rect, const int64_t ybegin, + const int64_t yend, const Kernel& kernel, const Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + for (int64_t y = ybegin; y < yend; ++y) { + for (size_t c = 0; c < 3; ++c) { + const WrapRowMirror wrap_row(in.Plane(c), rect.ysize()); + RunRow(rect.ConstPlaneRow(in, c, y), rect.xsize(), stride, + wrap_row, kernel, out->PlaneRow(c, y)); + } + } + } + + template + static HWY_ATTR JXL_INLINE void RunInteriorRows( + const ImageF& in, const Rect& rect, const int64_t ybegin, + const int64_t yend, const Kernel& kernel, ThreadPool* pool, + const ImageF* out) { + const int64_t stride = in.PixelsPerRow(); + RunOnPool( + pool, ybegin, yend, ThreadPool::SkipInit(), + [&](const int y, int /*thread*/) HWY_ATTR { + RunRow(rect.ConstRow(in, y), rect.xsize(), stride, + WrapRowUnchanged(), kernel, out->Row(y)); + }, + "Convolve"); + } + + // Image3F. + template + static HWY_ATTR JXL_INLINE void RunInteriorRows( + const Image3F& in, const Rect& rect, const int64_t ybegin, + const int64_t yend, const Kernel& kernel, ThreadPool* pool, + const Image3F* out) { + const int64_t stride = in.PixelsPerRow(); + RunOnPool( + pool, ybegin, yend, ThreadPool::SkipInit(), + [&](const int y, int /*thread*/) HWY_ATTR { + for (size_t c = 0; c < 3; ++c) { + RunRow(rect.ConstPlaneRow(in, c, y), rect.xsize(), + stride, WrapRowUnchanged(), kernel, + out->PlaneRow(c, y)); + } + }, + "Convolve3"); + } + + template + static HWY_ATTR JXL_INLINE void RunRows(const Image& in, const Rect& rect, + const Kernel& kernel, + ThreadPool* pool, const Image* out) { + const int64_t ysize = rect.ysize(); + RunBorderRows(in, rect, 0, std::min(int64_t(kRadius), ysize), + kernel, out); + if (ysize > 2 * int64_t(kRadius)) { + RunInteriorRows(in, rect, int64_t(kRadius), + ysize - int64_t(kRadius), kernel, pool, out); + } + if (ysize > int64_t(kRadius)) { + RunBorderRows(in, rect, ysize - int64_t(kRadius), ysize, + kernel, out); + } + } +}; + +} // namespace jxl + +#endif // JXL_CONVOLVE_H_ diff --git a/jxl/convolve_test.cc b/jxl/convolve_test.cc new file mode 100644 index 000000000..a93fbfeb4 --- /dev/null +++ b/jxl/convolve_test.cc @@ -0,0 +1,236 @@ +// Copyright (c) the JPEG XL Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jxl/convolve.h" + +#include +#include + +#include "gtest/gtest.h" +#include "jxl/base/compiler_specific.h" +#include "jxl/base/data_parallel.h" +#include "jxl/base/thread_pool_internal.h" +#define HWY_USE_GTEST // prevent redefining TEST +#include + +#include "jxl/image_ops.h" +#include "jxl/image_test_utils.h" + +#ifndef JXL_DEBUG_CONVOLVE +#define JXL_DEBUG_CONVOLVE 0 +#endif + +namespace jxl { +namespace { + +HWY_ATTR void TestNeighborsImpl() { + const Neighbors::D d; + const Neighbors::V v = Iota(d, 0); + HWY_ALIGN float actual[hwy::kTestMaxVectorSize / sizeof(float)] = {0}; + + HWY_ALIGN float first_l1[hwy::kTestMaxVectorSize / sizeof(float)] = { + 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + Store(Neighbors::FirstL1(v), d, actual); + EXPECT_EQ(std::vector(first_l1, first_l1 + d.N), + std::vector(actual, actual + d.N)); + +#if HWY_BITS != 0 + HWY_ALIGN float first_l2[hwy::kTestMaxVectorSize / sizeof(float)] = { + 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + Store(Neighbors::FirstL2(v), d, actual); + EXPECT_EQ(std::vector(first_l2, first_l2 + d.N), + std::vector(actual, actual + d.N)); + + HWY_ALIGN float first_l3[hwy::kTestMaxVectorSize / sizeof(float)] = { + 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + Store(Neighbors::FirstL3(v), d, actual); + EXPECT_EQ(std::vector(first_l3, first_l3 + d.N), + std::vector(actual, actual + d.N)); +#endif +} + +TEST(ConvolveTest, TestNeighbors) { TestNeighborsImpl(); } + +// Weights i=0..2 are for Manhattan distance i from center. +template +struct Symmetric3x3Convolution { + static_assert(kRadius == 1, "Wrong kRadius"); + + template + static void Run(const ImageF& in, const Rect& rect, const Kernel& kernel, + ImageF* out) { + PROFILER_ZONE("slow::Symmetric3::Run"); + JXL_CHECK(SameSize(rect, *out)); + const Weights3x3& weights = kernel.Weights(); + + const size_t xsize = rect.xsize(); + const size_t ysize = rect.ysize(); + for (int64_t y = 0; y < ysize; ++y) { + const float* const JXL_RESTRICT row_t = in.ConstRow(Wrap()(y - 1, ysize)); + const float* const JXL_RESTRICT row_m = in.ConstRow(y); + const float* const JXL_RESTRICT row_b = in.ConstRow(Wrap()(y + 1, ysize)); + float* const JXL_RESTRICT row_out = out->Row(y); + + for (int64_t x = 0; x < xsize; ++x) { + float mul = row_m[x] * weights.mc[0]; + const int64_t xm1 = Wrap()(x - 1, xsize); + const int64_t xp1 = Wrap()(x + 1, xsize); + const float tl = row_t[xm1]; + const float ml = row_m[xm1]; + const float bl = row_b[xm1]; + const float tr = row_t[xp1]; + const float mr = row_m[xp1]; + const float br = row_b[xp1]; + mul += (row_t[x] + row_b[x] + ml + mr) * weights.tc[0]; + mul += (tl + tr + bl + br) * weights.tl[0]; + row_out[x] = mul; + } + } + } +}; + +template +ImageF SlowConvolve(const ImageOrView& in, const Rect& rect, + const Kernel& kernel) { + ImageF out(rect.xsize(), rect.ysize()); + Slow::Run(in, rect, kernel, &out); + return out; +} + +// Compares ConvolveT<> against SlowConvolve. +template