From f26681af435188806f55460b32734a105cddef1a Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sat, 29 Feb 2020 22:35:38 -0300 Subject: [PATCH 1/6] Create initial code infrastructure for EasySplash rewrite The idea is to toy with the GStreamer use as a way to render the boot animation. This reduces a lot of platform specific code we have to handle and offer nice possibilities such as audio use during the boot process. Signed-off-by: Otavio Salvador --- .github/workflows/code_style.yml | 29 + .github/workflows/easysplash.yml | 18 - .github/workflows/linux.yml | 68 ++ .github/workflows/security_check.yml | 18 + .gitignore | 1 + CMakeLists.txt | 13 - Cargo.lock | 14 + Cargo.toml | 13 + doc/Animation-Structure.md | 55 - rustfmt.toml | 10 + src/CMakeLists.txt | 149 --- src/animation.cpp | 304 ------ src/animation.hpp | 161 --- src/config.h.in | 12 - src/display.hpp | 68 -- src/easysplashctl.cpp | 127 --- src/eglgles/egl_platform.hpp | 53 - src/eglgles/egl_platform_gbm.cpp | 1209 --------------------- src/eglgles/egl_platform_rpi_dispmanx.cpp | 257 ----- src/eglgles/egl_platform_viv_fb.cpp | 191 ---- src/eglgles/egl_platform_x11.cpp | 285 ----- src/eglgles/gl_misc.cpp | 69 -- src/eglgles/gl_misc.hpp | 28 - src/eglgles/gles_display.cpp | 378 ------- src/eglgles/gles_display.hpp | 61 -- src/eglgles/opengl.hpp | 23 - src/eglgles/texture.cpp | 118 -- src/eglgles/texture.hpp | 53 - src/event_loop.cpp | 269 ----- src/event_loop.hpp | 43 - src/g2d/g2d_display.cpp | 256 ----- src/g2d/g2d_display.hpp | 78 -- src/init/CMakeLists.txt | 22 - src/init/easysplash-quit.service.cmake | 19 - src/init/easysplash-start.init.cmake | 30 - src/init/easysplash-start.service.cmake | 20 - src/linux_framebuffer.cpp | 217 ---- src/linux_framebuffer.hpp | 79 -- src/load_png.cpp | 185 ---- src/load_png.hpp | 25 - src/log.cpp | 119 -- src/log.hpp | 64 -- src/lru_cache.hpp | 102 -- src/main.cpp | 218 ---- src/main.rs | 10 + src/noncopyable.hpp | 40 - src/scope_guard.hpp | 85 -- src/swrender/swrender_display.cpp | 234 ---- src/swrender/swrender_display.hpp | 77 -- src/types.cpp | 76 -- src/types.hpp | 64 -- src/zip_archive.cpp | 323 ------ src/zip_archive.hpp | 74 -- 53 files changed, 163 insertions(+), 6351 deletions(-) create mode 100644 .github/workflows/code_style.yml delete mode 100644 .github/workflows/easysplash.yml create mode 100644 .github/workflows/linux.yml create mode 100644 .github/workflows/security_check.yml create mode 100644 .gitignore delete mode 100644 CMakeLists.txt create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 doc/Animation-Structure.md create mode 100644 rustfmt.toml delete mode 100644 src/CMakeLists.txt delete mode 100644 src/animation.cpp delete mode 100644 src/animation.hpp delete mode 100644 src/config.h.in delete mode 100644 src/display.hpp delete mode 100644 src/easysplashctl.cpp delete mode 100644 src/eglgles/egl_platform.hpp delete mode 100644 src/eglgles/egl_platform_gbm.cpp delete mode 100644 src/eglgles/egl_platform_rpi_dispmanx.cpp delete mode 100644 src/eglgles/egl_platform_viv_fb.cpp delete mode 100644 src/eglgles/egl_platform_x11.cpp delete mode 100644 src/eglgles/gl_misc.cpp delete mode 100644 src/eglgles/gl_misc.hpp delete mode 100644 src/eglgles/gles_display.cpp delete mode 100644 src/eglgles/gles_display.hpp delete mode 100644 src/eglgles/opengl.hpp delete mode 100644 src/eglgles/texture.cpp delete mode 100644 src/eglgles/texture.hpp delete mode 100644 src/event_loop.cpp delete mode 100644 src/event_loop.hpp delete mode 100644 src/g2d/g2d_display.cpp delete mode 100644 src/g2d/g2d_display.hpp delete mode 100644 src/init/CMakeLists.txt delete mode 100644 src/init/easysplash-quit.service.cmake delete mode 100755 src/init/easysplash-start.init.cmake delete mode 100644 src/init/easysplash-start.service.cmake delete mode 100644 src/linux_framebuffer.cpp delete mode 100644 src/linux_framebuffer.hpp delete mode 100644 src/load_png.cpp delete mode 100644 src/load_png.hpp delete mode 100644 src/log.cpp delete mode 100644 src/log.hpp delete mode 100644 src/lru_cache.hpp delete mode 100644 src/main.cpp create mode 100644 src/main.rs delete mode 100644 src/noncopyable.hpp delete mode 100644 src/scope_guard.hpp delete mode 100644 src/swrender/swrender_display.cpp delete mode 100644 src/swrender/swrender_display.hpp delete mode 100644 src/types.cpp delete mode 100644 src/types.hpp delete mode 100644 src/zip_archive.cpp delete mode 100644 src/zip_archive.hpp diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml new file mode 100644 index 0000000..7be226f --- /dev/null +++ b/.github/workflows/code_style.yml @@ -0,0 +1,29 @@ +name: Code Style Check + +on: + push: + branches: + - master + pull_request: + +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --all --tests diff --git a/.github/workflows/easysplash.yml b/.github/workflows/easysplash.yml deleted file mode 100644 index 87ed2a1..0000000 --- a/.github/workflows/easysplash.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: install dependencies - run: sudo apt-get install -y libsystemd-dev - - name: make - run: | - mkdir build - cmake -S . -B build -DDISPLAY_TYPE_SWRENDER=1 - cmake --build build diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..a4d89cf --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,68 @@ +name: CI (Linux) + +on: + push: + branches: + - master + pull_request: + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + - 1.40.0 # MSRV + - stable + - nightly + + name: Test ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: + command: generate-lockfile + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo index + uses: actions/cache@v1 + with: + path: ~/.cargo/git + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo build + uses: actions/cache@v1 + with: + path: target + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} + + - name: Check build + uses: actions-rs/cargo@v1 + with: + command: check + args: --release --all --bins --examples --tests + + - name: Tests + uses: actions-rs/cargo@v1 + timeout-minutes: 10 + with: + command: test + args: --release --all --all-features --no-fail-fast -- --nocapture + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache diff --git a/.github/workflows/security_check.yml b/.github/workflows/security_check.yml new file mode 100644 index 0000000..6539f2e --- /dev/null +++ b/.github/workflows/security_check.yml @@ -0,0 +1,18 @@ +name: Security audit +on: + schedule: + - cron: '0 0 * * *' + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + pull_request: + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 7e9c61d..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# EasySplash - tool for animated splash screens -# Copyright (C) 2014, 2015 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -cmake_minimum_required(VERSION 2.8.9) -project(easysplash) - -set(CMAKE_INCLUDE_CURRENT_DIR ON) - -add_subdirectory(src) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e438556 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" + +[[package]] +name = "easysplash" +version = "1.90.0" +dependencies = [ + "anyhow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d4edb4a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +# EasySplash - tool for animated splash screens +# Copyright (C) 2020 O.S. Systems Software LTDA. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT + +[package] +name = "easysplash" +version = "1.90.0" +authors = ["Otavio Salvador "] +edition = "2018" + +[dependencies] +anyhow = "1.0.26" diff --git a/doc/Animation-Structure.md b/doc/Animation-Structure.md deleted file mode 100644 index ed6f02c..0000000 --- a/doc/Animation-Structure.md +++ /dev/null @@ -1,55 +0,0 @@ -Animation structure -------------------- - -Animations are contained in zip archives. The archive layout must be like this: - - desc.txt - part1/100.png - part1/101.png - part1/102.png - part1/103.png - part2/488.png - part2/489.png - -The names of the part directories can be chosen freely. Only desc.txt needs to always be -named as shown. The PNGs inside the part directories can also be named freely; they will -be lexicographically sorted inside each part directory and played in that order. - -desc.txt is a text file which defines the animation. -The first row is formatted this way: - - [output width] [output height] [fps] - -[output width] and [output height] specify the width and height of a rectangle that is -centered on the screen. Frames will be scaled to fit inside that triangle. [fps] defines -the framerate in frames per second. - -For example, a line "320 240 25" specifies a centered 320x240 output rectangle and -playback at 25 fps. - -The next rows define the parts: - - [mode] [num loops] [pause] [name of part directory] - -[mode] is either "p" or "c". "p" means the part must be played completely, even if -somebody requested EasySplash to stop (by using easysplashctl). Only after this -part finished, EasySplash can stop. "c" means it can be stopped immediately. -[num loops] defines how often this part is played. Minimum value is 1; if 0 is -specified, it is treated as 1. -[pause] is a legacy value and unused. -[name of part directory] specifies the name of the directory the part's frames are stored. - -Example: - - p 2 0 beginning - p 3 0 intermediate - p 1 0 end - -Three parts, all of which cannot be interrupted until they are completed (with the -exception of the last one; more on that later). The first part is played 2 times, -the second 3 times, the third once (or all the time, as explained later). -The first part's PNG frame images are stored in the beginning/ directory in the -zip archive, the second part's in intermediate/, the third one's in end/ . - -The last part has a special meaning. In realtime mode, it is ran in an infinite -loop, and [mode] is assumed to be set to "c" (the value in desc.txt is ignored). diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..da53b4e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,10 @@ +version = "Two" +use_small_heuristics = "Max" +edition = "2018" +reorder_imports = true +condense_wildcard_suffixes = true +merge_imports = true +reorder_impl_items = true +use_field_init_shorthand = true +use_try_shorthand = true +wrap_comments = true diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt deleted file mode 100644 index 16ea6ba..0000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,149 +0,0 @@ -# EasySplash - tool for animated splash screens -# Copyright (C) 2014, 2015 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -find_package(PkgConfig) - -pkg_check_modules(ZLIB zlib) -pkg_check_modules(LIBPNG libpng) - -add_definitions(-Wextra -Wall -Wno-variadic-macros -std=c++11 -pedantic) - -if (NOT CTL_FIFO_PATH) - set(CTL_FIFO_PATH "/tmp/easysplash-ctl") -endif() - -if (NOT EASYSPLASH_PID_FILE) - set(EASYSPLASH_PID_FILE "/tmp/easysplash.pid") -endif() - -if (DISPLAY_TYPE_SWRENDER) - - pkg_check_modules(PIXMAN pixman-1) - set(DISPLAY_CODE swrender/swrender_display.cpp) - set(DISPLAY_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/swrender" ${PIXMAN_INCLUDE_DIRS}) - set(DISPLAY_LIBRARIES ${PIXMAN_LIBRARIES}) - -elseif(DISPLAY_TYPE_G2D) - - find_path(G2D_INCLUDE_DIR g2d.h) - find_library(G2D_LIBRARY NAMES g2d) - set(DISPLAY_CODE g2d/g2d_display.cpp) - set(DISPLAY_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/g2d" "${G2D_INCLUDE_DIR}") - set(DISPLAY_LIBRARIES "${G2D_LIBRARY}") - -elseif(DISPLAY_TYPE_GLES) - - add_definitions(-DLINUX) # necessary for the Vivante EGL headers - - # necessary to make sure the X11 bits in EGL/eglplatform.h are never used - if (NOT EGL_PLATFORM_X11) - add_definitions(-DEGL_NO_X11) - endif() - - if (EGL_PLATFORM_X11) - find_path(X11_INCLUDE_DIR X11/Xlib.h) - find_library(X11_LIBRARY NAMES X11) - set(EGL_PLATFORM_CODE eglgles/egl_platform_x11.cpp) - set(EGL_PLATFORM_INCLUDE_DIRS "${X11_INCLUDE_DIR}") - set(EGL_PLATFORM_LIBRARIES "${X11_LIBRARY}") - elseif (EGL_PLATFORM_VIV_FB) - add_definitions(-DEGL_API_FB) # necessary for the Vivante EGL headers in Framebuffer mode - set(EGL_PLATFORM_CODE eglgles/egl_platform_viv_fb.cpp) - elseif (EGL_PLATFORM_RPI_DISPMANX) - find_path(BCM_HOST_INCLUDE_DIR bcm_host.h) - find_library(VCHOSTIF_LIBRARY vchostif) - find_library(BCM_HOST_LIBRARY bcm_host) - find_library(DL_LIBRARY dl) - set(EGL_PLATFORM_INCLUDE_DIRS "${BCM_HOST_INCLUDE_DIR}") - set(EGL_PLATFORM_CODE eglgles/egl_platform_rpi_dispmanx.cpp) - set(EGL_PLATFORM_LIBRARIES ${BCM_HOST_LIBRARY} ${VCHOSTIF_LIBRARY} ${DL_LIBRARY}) - elseif (EGL_PLATFORM_GBM) - pkg_check_modules(LIBUDEV libudev) - pkg_check_modules(LIBDRM libdrm) - pkg_check_modules(GBM gbm) - set(EGL_PLATFORM_CODE eglgles/egl_platform_gbm.cpp) - set(EGL_PLATFORM_INCLUDE_DIRS ${LIBUDEV_INCLUDE_DIRS} ${LIBDRM_INCLUDE_DIRS} ${GBM_INCLUDE_DIRS}) - set(EGL_PLATFORM_LIBRARIES ${LIBUDEV_LIBRARIES} ${LIBDRM_LIBRARIES} ${GBM_LIBRARIES}) - else() - message(FATAL_ERROR "No EGL platform set") - endif() - - find_path(EGL_INCLUDE_DIR EGL/eglvivante.h) - find_path(EGL_INCLUDE_DIR EGL/egl.h) - find_library(EGL_LIBRARY EGL) - find_path(GLES_INCLUDE_DIR GLES2/gl2.h) - find_path(GLES_INCLUDE_DIR GLES2/gl2ext.h) - find_library(GLES_LIBRARY GLESv2) - - set(DISPLAY_CODE ${EGL_PLATFORM_CODE} eglgles/gles_display.cpp eglgles/gl_misc.cpp eglgles/texture.cpp) - set(DISPLAY_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/eglgles" ${EGL_PLATFORM_INCLUDE_DIRS} "${EGL_INCLUDE_DIR}" "${GLES_INCLUDE_DIR}") - set(DISPLAY_LIBRARIES ${EGL_PLATFORM_LIBRARIES} "${EGL_LIBRARY}" "${GLES_LIBRARY}") - -else() - message(FATAL_ERROR "Display type not set. Set one in the command line arguments. Example: -DDISPLAY_TYPE_SWRENDER=1") -endif() - -option(ENABLE_SYSVINIT_SUPPORT "Enable Sysvinit support" ON) -if (ENABLE_SYSVINIT_SUPPORT) - set(WITH_SYSVINIT "ON") -else() - set(WITH_SYSVINIT "OFF") -endif() - -option(ENABLE_SYSTEMD_SUPPORT "Enable SystemD support" ON) - -if (ENABLE_SYSTEMD_SUPPORT) - pkg_check_modules(SYSTEMD "systemd" REQUIRED) - pkg_check_modules(LIBSYSTEMD "libsystemd" REQUIRED) - if (SYSTEMD_FOUND AND "${SYSTEMD_SYSTEM_UNIT_DIR}" STREQUAL "") - execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=systemdsystemunitdir systemd OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_DIR) - string(REGEX REPLACE "[ \t\n]+" "" SYSTEMD_SYSTEM_UNIT_DIR "${SYSTEMD_SYSTEM_UNIT_DIR}") - elseif (NOT SYSTEMD_FOUND AND SYSTEMD_SYSTEM_UNIT_DIR) - message (FATAL_ERROR "Variable SYSTEMD_SYSTEM_UNIT_DIR is defined, but we can't find systemd using pkg-config") - endif() - - if (SYSTEMD_FOUND) - set(WITH_SYSTEMD "ON") - add_definitions(-DWITH_SYSTEMD) - message(STATUS "systemd services install dir: ${SYSTEMD_SYSTEM_UNIT_DIR}") - else() - set(WITH_SYSTEMD "OFF") - endif() -endif() - -include_directories(${LIBPNG_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS} ${LIBSYSTEMD_INCLUDE_DIRS} ${DISPLAY_INCLUDE_DIRS}) - -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/config.h") - -add_executable( - easysplash - animation.cpp - event_loop.cpp - linux_framebuffer.cpp - load_png.cpp - log.cpp - main.cpp - types.cpp - zip_archive.cpp - ${DISPLAY_CODE} -) - -add_executable( - easysplashctl - easysplashctl.cpp -) - -target_link_libraries(easysplash ${LIBPNG_LIBRARIES} ${ZLIB_LIBRARIES} ${LIBSYSTEMD_LIBRARIES} ${DISPLAY_LIBRARIES}) - -if (NOT CMAKE_INSTALL_SBINDIR) - set(CMAKE_INSTALL_SBINDIR "sbin") -endif() - -install(TARGETS easysplash DESTINATION ${CMAKE_INSTALL_SBINDIR}) -install(TARGETS easysplashctl DESTINATION ${CMAKE_INSTALL_SBINDIR}) - -add_subdirectory(init) diff --git a/src/animation.cpp b/src/animation.cpp deleted file mode 100644 index dfdd2f6..0000000 --- a/src/animation.cpp +++ /dev/null @@ -1,304 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include "animation.hpp" -#include "zip_archive.hpp" -#include "log.hpp" -#include "load_png.hpp" - - -namespace std -{ - - -size_t hash < ::easysplash::animation_position > ::operator()(::easysplash::animation_position const & p_animation_position) const -{ - return std::hash < unsigned int > ()(p_animation_position.m_part_nr) - ^ std::hash < unsigned int > ()(p_animation_position.m_part_loop_nr) - ^ std::hash < unsigned int > ()(p_animation_position.m_frame_nr_in_part); -} - - -} // namespace std end - - -namespace easysplash -{ - - -animation_position::animation_position() - : m_part_nr(0) - , m_part_loop_nr(0) - , m_frame_nr_in_part(0) -{ -} - - -animation::part::part() - : m_play_until_complete(false) -{ -} - - -animation::animation(zip_archive &p_zip_archive, display &p_display) - : m_fps(0) - , m_total_num_frames(0) - , m_output_width(0) - , m_output_height(0) - , m_zip_archive(p_zip_archive) - , m_display(p_display) - , m_image_handle_cache(10, [&](animation_position const &p_key, display::image_handle & p_value) { - m_display.unload_image(p_value); - LOG_MSG(trace, "cache cleanup: unloading image at position " << p_key); - }) -{ - zip_archive::entry const *desc_entry = find_entry_by_name(p_zip_archive, "desc.txt"); - if (desc_entry == NULL) - { - LOG_MSG(error, "file desc.txt was not found in zip archive"); - return; - } - - datablock desc_data = p_zip_archive.uncompress_data(*desc_entry); - datablock_buf desc_readbuf(desc_data); - std::istream desc_input(&desc_readbuf); - - std::string line; - - { - std::getline(desc_input, line); - std::stringstream sstr(line); - sstr >> m_output_width >> m_output_height >> m_fps; - } - - while (true) - { - std::getline(desc_input, line); - if (!desc_input) - break; - - part new_part; - - std::string type, path; - int pause; // dummy value, not currently used - - std::stringstream sstr(line); - sstr >> type >> new_part.m_num_times_to_play >> pause >> path; - if (path.empty() || type.empty()) - { - LOG_MSG(error, "line \"" << line << "\" in desc.txt file is invalid"); - continue; - } - - if (new_part.m_num_times_to_play == 0) - new_part.m_num_times_to_play = 1; - - new_part.m_play_until_complete = (type == "p"); - - // Find all entries which start with the given path, and execute the specified - // function for eatch matching entry (the matching entries are passed to the function) - p_zip_archive.find_entries_with_path(path, [&](zip_archive::entry const &p_entry) { - if (p_entry.m_compressed_size == 0) - return; // skip empty files and directories - - new_part.m_frames.push_back(&p_entry); - }); - - // As explained, in the header, m_total_num_frames sums up the number of all parts, - // each number multiplied by the number of loops - m_total_num_frames += new_part.m_frames.size() * new_part.m_num_times_to_play; - m_parts.push_back(std::move(new_part)); - } -} - - -std::size_t animation::get_num_parts() const -{ - return m_parts.size(); -} - - -animation::part const * animation::get_part(std::size_t const p_index) const -{ - return (p_index < m_parts.size()) ? &(m_parts[p_index]) : nullptr; -} - - -unsigned int animation::get_fps() const -{ - return m_fps; -} - - -unsigned int animation::get_total_num_frames() const -{ - return m_total_num_frames; -} - - -long animation::get_output_width() const -{ - return m_output_width; -} - - -long animation::get_output_height() const -{ - return m_output_height; -} - - -display::image_handle animation::get_image_handle_at(animation_position const &p_animation_position) -{ - // Check if the position is beyond the list of parts - if (p_animation_position.m_part_nr >= m_parts.size()) - { - LOG_MSG(warning, "animation position (" << p_animation_position << ") has invalid part nr"); - return display::invalid_image_handle; - } - - animation::part const &part = m_parts[p_animation_position.m_part_nr]; - - // Check if the position is beyond the list of frames inside the part - if (p_animation_position.m_frame_nr_in_part >= part.m_frames.size()) - { - LOG_MSG(warning, "animation position (" << p_animation_position << ") has invalid frame nr"); - return display::invalid_image_handle; - } - - display::image_handle * cached_handle = m_image_handle_cache.get_entry(p_animation_position); - if (cached_handle == nullptr) - { - LOG_MSG(trace, "no frame at position " << p_animation_position << " in LRU cache - loading"); - - // Get the zip archive entry for the current position - zip_archive::entry const &p_entry = *(part.m_frames[p_animation_position.m_frame_nr_in_part]); - - image frame = load_png(m_zip_archive.uncompress_data(p_entry)); - if (!is_valid(frame)) - { - LOG_MSG(error, "could not load PNG \"" << p_entry.m_filename << "\""); - return display::invalid_image_handle; - } - - // Create a display image_handle for each frame - display::image_handle handle = m_display.load_image(std::move(frame)); - if (handle != display::invalid_image_handle) - { - LOG_MSG(trace, "loaded frame \"" << p_entry.m_filename << "\""); - - cached_handle = &(m_image_handle_cache.add_entry(p_animation_position, std::move(handle))); - } - else - { - LOG_MSG(error, "could not load frame \"" << p_entry.m_filename << "\" into display"); - return display::invalid_image_handle; - } - } - else - { - LOG_MSG(trace, "retrieved frame at position " << p_animation_position << " from LRU cache"); - } - - return *cached_handle; -} - - - - -bool is_valid(animation const &p_animation) -{ - return (p_animation.get_fps() != 0) - && (p_animation.get_output_width() != 0) - && (p_animation.get_output_height() != 0) - && (p_animation.get_num_parts() != 0); -} - - -bool operator < (animation_position const &p_first, animation_position const &p_second) -{ - if (p_first.m_part_nr < p_second.m_part_nr) - return true; - else if (p_first.m_part_nr > p_second.m_part_nr) - return false; - - if (p_first.m_part_loop_nr < p_second.m_part_loop_nr) - return true; - else if (p_first.m_part_loop_nr > p_second.m_part_loop_nr) - return false; - - return p_first.m_frame_nr_in_part < p_second.m_frame_nr_in_part; -} - - -bool operator == (animation_position const &p_first, animation_position const &p_second) -{ - return (p_first.m_part_nr == p_second.m_part_nr) - && (p_first.m_part_loop_nr == p_second.m_part_loop_nr) - && (p_first.m_frame_nr_in_part == p_second.m_frame_nr_in_part); -} - - -bool operator != (animation_position const &p_first, animation_position const &p_second) -{ - return !(p_first == p_second); -} - - -std::ostream& operator << (std::ostream &p_out, animation_position const &p_animation_position) -{ - p_out << "[ part nr: " << p_animation_position.m_part_nr << " relative frame nr in part: " << p_animation_position.m_frame_nr_in_part << " ]"; - return p_out; -} - - -void update_position(animation_position &p_animation_position, animation const &p_animation, unsigned int const p_increase) -{ - p_animation_position.m_frame_nr_in_part += p_increase; - if (p_animation_position.m_part_nr >= p_animation.get_num_parts()) - p_animation_position.m_part_nr = p_animation.get_num_parts() - 1; - - // After m_frame_nr_in_part was incremented by p_increase, its value - // may lie outside of the bounds of the part. This means the new position - // is actually beyond the current part, so try moving to the next part, - // and see if the m_frame_nr_in_part value there is inside the bounds. If - // not, move to the next part again etc. until m_frame_nr_in_part fits in - // the part's bounds, or until the last part is reached. - while (true) - { - animation::part const &part = *(p_animation.get_part(p_animation_position.m_part_nr)); - - // If m_frame_nr_in_part lies within the bounds of the current part, - // exit, since we are done. - if (p_animation_position.m_frame_nr_in_part < part.m_frames.size()) - break; - - // If the current part is the last one, simply do a modulo on - // m_frame_nr_in_part, thereby looping inside the last part. - if (p_animation_position.m_part_nr == (p_animation.get_num_parts() - 1)) - { - p_animation_position.m_frame_nr_in_part = p_animation_position.m_frame_nr_in_part % part.m_frames.size(); - break; - } - - // If this point is reached, it means that (1) the current part is not - // the last part in the animation and (2) m_frame_nr_in_part still lies - // outside of the part's bounds. Increase the part's loop count, and - // if the maximum number of loops is reached, advance to the next part. - p_animation_position.m_part_loop_nr++; - p_animation_position.m_frame_nr_in_part -= part.m_frames.size(); - if (p_animation_position.m_part_loop_nr >= part.m_num_times_to_play) - { - p_animation_position.m_part_loop_nr = 0; - p_animation_position.m_part_nr++; - } - } -} - - -} // namespace easysplash end diff --git a/src/animation.hpp b/src/animation.hpp deleted file mode 100644 index 322538e..0000000 --- a/src/animation.hpp +++ /dev/null @@ -1,161 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_ANIMATION_HPP -#define EASYSPLASH_ANIMATION_HPP - -#include -#include -#include -#include -#include - -#include "display.hpp" -#include "types.hpp" -#include "zip_archive.hpp" -#include "lru_cache.hpp" - - -namespace easysplash -{ - - -/* animation_position structure: - * - * This is the current playback position in the frame. It is defined by: - * m_part_nr: the number of the part it is currently in - * m_part_loop_nr: what loop nr the current position is in (this can only be nonzero if - * the part's m_num_times_to_play value is greater than 1) - * m_frame_nr_in_part: the frame number inside this part (0 = first frame of this part) - */ - -struct animation_position -{ - unsigned int m_part_nr, m_part_loop_nr; - unsigned int m_frame_nr_in_part; - - animation_position(); -}; - - -} - - -namespace std -{ - - -template < > -struct hash < ::easysplash::animation_position > -{ - size_t operator()(::easysplash::animation_position const & p_animation_position) const; -}; - - -} // namespace std end - - -namespace easysplash -{ - - -/* animation structure: - * - * The animation is defined by a framerate (fps), output width&height, and one or more parts. - * the parts, fps and output width & height are read from the desc.txt file in the animation - * zip archive. - * output width & height defines the size of the rectangle the frames will be painted in. - * The rectangle will also be drawn in the center of the screen. - * - * m_total_num_frames is calculated as the sum of all number of frames of each part, multiplied - * by how often the corresponding part is looped. For example, if desc.txt defines three parts, - * part 1 with 5 frames and 1 loop, part 2 with 11 frames and 3 loops, part 3 with 6 frames and - * 2 loops, m_total_num_frames = 5*1 + 11*3 + 6*2 = 50. - * - * Parts are defined by a m_play_until_complete flag that indicates if the splash screen - * can be stopped immediately or only after this part is done, a m_num_times_to_play value - * which defines how often the part shall be played, and an array of zip archive entries which - * contain the necessary information to load the individual frame. - * - * The last part has a special meaning in realtime mode. It will be repeated endlessly, - * and the m_num_times_to_play value is ignored. m_play_until_complete is also ignored; - * the animation will behave as if m_play_until_complete were false. - */ -class animation -{ -public: - struct part - { - bool m_play_until_complete; - unsigned int m_num_times_to_play; - std::vector < zip_archive::entry const * > m_frames; - - part(); - }; - - explicit animation(zip_archive &p_zip_archive, display &p_display); - - std::size_t get_num_parts() const; - part const * get_part(std::size_t const p_index) const; - - unsigned int get_fps() const; - unsigned int get_total_num_frames() const; - - long get_output_width() const; - long get_output_height() const; - - /* Retrieves the image handle for the frame at the given position. If no frame can be - * found, display::invalid_image_handle is returned. - */ - display::image_handle get_image_handle_at(animation_position const &p_animation_position); - -private: - animation(animation const &) = delete; - animation& operator = (animation const &) = delete; - - std::vector < part > m_parts; - unsigned int m_fps; - unsigned int m_total_num_frames; - long m_output_width, m_output_height; - - zip_archive &m_zip_archive; - display &m_display; - - typedef lru_cache < animation_position, display::image_handle > image_handle_cache; - image_handle_cache m_image_handle_cache; -}; - - -bool operator < (animation_position const &p_first, animation_position const &p_second); -bool operator == (animation_position const &p_first, animation_position const &p_second); -bool operator != (animation_position const &p_first, animation_position const &p_second); -std::ostream& operator << (std::ostream &p_out, animation_position const &p_animation_position); - - -/* Checks if the animation is valid. An animation is valid if it has at least one part, - * and the m_fps, m_output_width, m_output_height values are nonzero. - */ -bool is_valid(animation const &p_animation); - -/* Loads the animation from a zip archive and allocates images in the display for each frame. - * If loading fails, the returned animation object will be invalid. Check with is_valid() for that. - */ -animation load_animation(zip_archive &p_zip_archive, display &p_display); - -/* Advances the position by calling update_position() and a given number of frames to - * advance. If m_part_nr refers to the last part in the frame, and the positiion would - * be advanced beyond the bounds of that part, it loops back, as if m_num_times_to_play - * were set to some infinite value for the last part. - */ -void update_position(animation_position &p_animation_position, animation const &p_animation, unsigned int const p_increase); - - -} // namespace easysplash end - - -#endif diff --git a/src/config.h.in b/src/config.h.in deleted file mode 100644 index 71c790c..0000000 --- a/src/config.h.in +++ /dev/null @@ -1,12 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - -#cmakedefine CTL_FIFO_PATH "@CTL_FIFO_PATH@" -#cmakedefine EASYSPLASH_PID_FILE "@EASYSPLASH_PID_FILE@" -#cmakedefine DISPLAY_TYPE_SWRENDER -#cmakedefine DISPLAY_TYPE_G2D -#cmakedefine DISPLAY_TYPE_GLES diff --git a/src/display.hpp b/src/display.hpp deleted file mode 100644 index a213dac..0000000 --- a/src/display.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_DISPLAY_HPP -#define EASYSPLASH_DISPLAY_HPP - -#include -#include "types.hpp" - - -namespace easysplash -{ - - -/* display: - * - * This is an abstract interface for display logic. A display has a width, height - * (both in pixels), and can load/unload and draw images. "Loading" an image means - * the display takes in an instance of image, and transfers the pixels to some form - * of internal representation (for example, textures in OpenGL based displays). - * These representations are referred to by their image_handle. To manually unload - * an image, unload_image() can be used. Note that images are automatically unloaded - * when the display is shut down. - * - * swap_buffers() is used to bring the current draw buffer to the screen. This is - * internally typically done by either blitting the back buffer to the front, or - * by flipping back- and front buffer. - */ -class display -{ -public: - typedef std::uintptr_t image_handle; - enum { invalid_image_handle = 0 }; - - virtual ~display() - { - } - - virtual bool is_valid() const = 0; - - virtual long get_width() const = 0; - virtual long get_height() const = 0; - - // useful variant for temporary images - virtual image_handle load_image(image &&p_image) - { - image tmp_img(std::move(p_image)); - return load_image(tmp_img); // uses the second overload below - } - - virtual image_handle load_image(image const &p_image) = 0; - virtual void unload_image(image_handle const p_image_handle) = 0; - - virtual void draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2) = 0; - - virtual void swap_buffers() = 0; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/easysplashctl.cpp b/src/easysplashctl.cpp deleted file mode 100644 index 8746e05..0000000 --- a/src/easysplashctl.cpp +++ /dev/null @@ -1,127 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014, 2015 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -int main(int argc, char *argv[]) -{ - // Get progress percentage from arguments - - if (argc < 2) - { - std::cerr << "Usage: " << argv[0] << " [PROGRESS (0-100)] [--wait-until-finished]\n"; - return -1; - } - - int progress_int = std::stoi(argv[1]); - if ((progress_int < 0) || (progress_int > 100)) - { - std::cerr << "Progress value " << progress_int << " is outside of the bounds 0-100\n"; - return -1; - } - - bool wait_until_finished = false; - if ((argc >= 3) && (std::string(argv[2]) == "--wait-until-finished") && (progress_int == 100)) - { - wait_until_finished = true; - } - - - // Read the EasySplash PID - // Do this here, _before_ the percentage is sent. This avoids theoretical - // race conditions where the percentage is 100 and EasySplash terminates - // before the PID could be read. - int easysplash_pid = 0; - if (wait_until_finished) - { - std::ifstream pidfile(EASYSPLASH_PID_FILE); - if (!pidfile) - { - std::cerr << "Could not open EasySplash PID file " << EASYSPLASH_PID_FILE << " - cannot wait\n"; - return -1; - } - - pidfile >> easysplash_pid; - if (!pidfile) - { - std::cerr << "Could not read PID from EasySplash PID file " << EASYSPLASH_PID_FILE << " - cannot wait\n"; - return -1; - } - - std::cerr << "Easysplash PID: " << easysplash_pid << "\n"; - } - - - // Send percentage (stored in one byte, which can hold the 0-100 range) - // over the FIFO to EasySplash - std::uint8_t progress = progress_int; - - int fifo_fd = open(CTL_FIFO_PATH, O_RDWR | O_NONBLOCK); - if (fifo_fd == -1) - { - std::cerr << "Could not open EasySplash FIFO \"" << CTL_FIFO_PATH << "\": " << std::strerror(errno) << "\n"; - return -1; - } - - int ret = write(fifo_fd, &progress, sizeof(progress)); - if (ret == -1) - std::cerr << "Could not send progress to EasySplash: " << std::strerror(errno) << "\n"; - - close(fifo_fd); - - - // Now wait for EasySplash to finish if necessary - if (wait_until_finished) - { - std::cerr << "100% reached, will wait until easysplash is finished\n"; - while (true) - { - // The common way of watching PIDs of non-child processes is to - // periodically call kill(pid, 0). ESRCH is returned when the - // watched process is gone. - - int ret = kill(easysplash_pid, 0); - - if (ret >= 0) - { - // Process exists. Wait 200ms before the next check. - usleep(200 * 1000); - continue; - } - else - { - // Return value -1 means either the process ended, or - // something else happened. Output error if it isn't - // the former. - int err = errno; - if (err == ESRCH) - { - std::cerr << "EasySplash process terminated successfully\n"; - break; - } - else - { - std::cerr << "Error while watching the PID " << easysplash_pid << ": " << std::strerror(err) << "\n"; - return -1; - } - } - } - } - - return (ret == -1) ? -1 : 0; -} diff --git a/src/eglgles/egl_platform.hpp b/src/eglgles/egl_platform.hpp deleted file mode 100644 index 7b2e675..0000000 --- a/src/eglgles/egl_platform.hpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_EGL_PLATFORM_HPP -#define EASYSPLASH_EGL_PLATFORM_HPP - -#include "opengl.hpp" - - -namespace easysplash -{ - - -class egl_platform -{ -public: - egl_platform(); - ~egl_platform(); - - bool make_current(); - bool swap_buffers(); - - long get_display_width() const; - long get_display_height() const; - - bool is_valid() const - { - return m_is_valid; - } - - -private: - EGLNativeDisplayType m_native_display; - EGLNativeWindowType m_native_window; - EGLDisplay m_egl_display; - EGLContext m_egl_context; - EGLSurface m_egl_surface; - bool m_is_valid; - - struct internal; - internal *m_internal; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/eglgles/egl_platform_gbm.cpp b/src/eglgles/egl_platform_gbm.cpp deleted file mode 100644 index 0a15467..0000000 --- a/src/eglgles/egl_platform_gbm.cpp +++ /dev/null @@ -1,1209 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2017 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -/* NOTE: "xf86" is correct even though we don't use X */ -#include -#include - -#include "scope_guard.hpp" -#include "egl_platform.hpp" -#include "gl_misc.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -// TODO: CRTC ID 0 has been confirmed by several sources as a safe -// value for designating an invalid CRTC ID. However, no source that -// can be referenced has been found so far. Find one and add a link here. -static std::uint32_t const INVALID_CRTC = std::uint32_t(0); - - -static std::string get_name_for_drm_connector(drmModeConnector *connector) -{ - switch (connector->connector_type) - { - case DRM_MODE_CONNECTOR_Unknown: return "Unknown"; - case DRM_MODE_CONNECTOR_VGA: return "VGA"; - case DRM_MODE_CONNECTOR_DVII: return "DVI-I"; - case DRM_MODE_CONNECTOR_DVID: return "DVI-D"; - case DRM_MODE_CONNECTOR_DVIA: return "DVI-A"; - case DRM_MODE_CONNECTOR_Composite: return "Composite"; - case DRM_MODE_CONNECTOR_SVIDEO: return "S-Video"; - case DRM_MODE_CONNECTOR_LVDS: return "LVDS"; - case DRM_MODE_CONNECTOR_Component: return "Component"; - case DRM_MODE_CONNECTOR_9PinDIN: return "9-Pin DIN"; - case DRM_MODE_CONNECTOR_DisplayPort: return "DisplayPort"; - case DRM_MODE_CONNECTOR_HDMIA: return "HDMI-A"; - case DRM_MODE_CONNECTOR_HDMIB: return "HDMI-B"; - case DRM_MODE_CONNECTOR_TV: return "TV"; - case DRM_MODE_CONNECTOR_eDP: return "Embedded DisplayPort (eDP)"; - case DRM_MODE_CONNECTOR_VIRTUAL: return "Virtual"; - case DRM_MODE_CONNECTOR_DSI: return "Display Serial Interface (DSI)"; - case DRM_MODE_CONNECTOR_DPI: return "DisplayPort (DPI)"; - default: - { - std::stringstream sstr; - sstr << "< unknown connector type " << connector->connector_type << " >"; - return sstr.str(); - } - } -} - - -static std::string get_name_for_drm_encoder(drmModeEncoder *encoder) -{ - switch (encoder->encoder_type) - { - case DRM_MODE_ENCODER_NONE: return "none"; - case DRM_MODE_ENCODER_DAC: return "DAC"; - case DRM_MODE_ENCODER_TMDS: return "TMDS"; - case DRM_MODE_ENCODER_LVDS: return "LVDS"; - case DRM_MODE_ENCODER_TVDAC: return "TVDAC"; - case DRM_MODE_ENCODER_VIRTUAL: return "Virtual"; - case DRM_MODE_ENCODER_DSI: return "DSI"; - default: - { - std::stringstream sstr; - sstr << "< unknown encoder type " << encoder->encoder_type << " >"; - return sstr.str(); - } - } -} - - -static std::string gbm_format_to_string(uint32_t format) -{ - if (format == GBM_BO_FORMAT_XRGB8888) - format = GBM_FORMAT_XRGB8888; - if (format == GBM_BO_FORMAT_ARGB8888) - format = GBM_FORMAT_ARGB8888; - - switch(format) - { - case GBM_FORMAT_C8: return "C8"; - case GBM_FORMAT_RGB332: return "RGB332"; - case GBM_FORMAT_BGR233: return "BGR233"; - case GBM_FORMAT_NV12: return "NV12"; - case GBM_FORMAT_XRGB4444: return "XRGB4444"; - case GBM_FORMAT_XBGR4444: return "XBGR4444"; - case GBM_FORMAT_RGBX4444: return "RGBX4444"; - case GBM_FORMAT_BGRX4444: return "BGRX4444"; - case GBM_FORMAT_XRGB1555: return "XRGB1555"; - case GBM_FORMAT_XBGR1555: return "XBGR1555"; - case GBM_FORMAT_RGBX5551: return "RGBX5551"; - case GBM_FORMAT_BGRX5551: return "BGRX5551"; - case GBM_FORMAT_ARGB4444: return "ARGB4444"; - case GBM_FORMAT_ABGR4444: return "ABGR4444"; - case GBM_FORMAT_RGBA4444: return "RGBA4444"; - case GBM_FORMAT_BGRA4444: return "BGRA4444"; - case GBM_FORMAT_ARGB1555: return "ARGB1555"; - case GBM_FORMAT_ABGR1555: return "ABGR1555"; - case GBM_FORMAT_RGBA5551: return "RGBA5551"; - case GBM_FORMAT_BGRA5551: return "BGRA5551"; - case GBM_FORMAT_RGB565: return "RGB565"; - case GBM_FORMAT_BGR565: return "BGR565"; - case GBM_FORMAT_YUYV: return "YUYV"; - case GBM_FORMAT_YVYU: return "YVYU"; - case GBM_FORMAT_UYVY: return "UYVY"; - case GBM_FORMAT_VYUY: return "VYUY"; - case GBM_FORMAT_RGB888: return "RGB888"; - case GBM_FORMAT_BGR888: return "BGR888"; - case GBM_FORMAT_XRGB8888: return "XRGB8888"; - case GBM_FORMAT_XBGR8888: return "XBGR8888"; - case GBM_FORMAT_RGBX8888: return "RGBX8888"; - case GBM_FORMAT_BGRX8888: return "BGRX8888"; - case GBM_FORMAT_AYUV: return "AYUV"; - case GBM_FORMAT_XRGB2101010: return "XRGB2101010"; - case GBM_FORMAT_XBGR2101010: return "XBGR2101010"; - case GBM_FORMAT_RGBX1010102: return "RGBX1010102"; - case GBM_FORMAT_BGRX1010102: return "BGRX1010102"; - case GBM_FORMAT_ARGB8888: return "ARGB8888"; - case GBM_FORMAT_ABGR8888: return "ABGR8888"; - case GBM_FORMAT_RGBA8888: return "RGBA8888"; - case GBM_FORMAT_BGRA8888: return "BGRA8888"; - case GBM_FORMAT_ARGB2101010: return "ARGB2101010"; - case GBM_FORMAT_ABGR2101010: return "ABGR2101010"; - case GBM_FORMAT_RGBA1010102: return "RGBA1010102"; - case GBM_FORMAT_BGRA1010102: return "BGRA1010102"; - - default: - { - std::stringstream sstr; - sstr << "< unknown format " << format << " >"; - return sstr.str(); - } - } - - return 0; -} - - -static int gbm_depth_from_format(uint32_t format) -{ - if (format == GBM_BO_FORMAT_XRGB8888) - format = GBM_FORMAT_XRGB8888; - if (format == GBM_BO_FORMAT_ARGB8888) - format = GBM_FORMAT_ARGB8888; - - switch(format) - { - case GBM_FORMAT_C8: - case GBM_FORMAT_RGB332: - case GBM_FORMAT_BGR233: - return 8; - - case GBM_FORMAT_NV12: - case GBM_FORMAT_XRGB4444: - case GBM_FORMAT_XBGR4444: - case GBM_FORMAT_RGBX4444: - case GBM_FORMAT_BGRX4444: - return 12; - - case GBM_FORMAT_XRGB1555: - case GBM_FORMAT_XBGR1555: - case GBM_FORMAT_RGBX5551: - case GBM_FORMAT_BGRX5551: - return 15; - - case GBM_FORMAT_ARGB4444: - case GBM_FORMAT_ABGR4444: - case GBM_FORMAT_RGBA4444: - case GBM_FORMAT_BGRA4444: - case GBM_FORMAT_ARGB1555: - case GBM_FORMAT_ABGR1555: - case GBM_FORMAT_RGBA5551: - case GBM_FORMAT_BGRA5551: - case GBM_FORMAT_RGB565: - case GBM_FORMAT_BGR565: - case GBM_FORMAT_YUYV: - case GBM_FORMAT_YVYU: - case GBM_FORMAT_UYVY: - case GBM_FORMAT_VYUY: - return 16; - - case GBM_FORMAT_RGB888: - case GBM_FORMAT_BGR888: - case GBM_FORMAT_XRGB8888: - case GBM_FORMAT_XBGR8888: - case GBM_FORMAT_RGBX8888: - case GBM_FORMAT_BGRX8888: - case GBM_FORMAT_AYUV: - return 24; - - case GBM_FORMAT_XRGB2101010: - case GBM_FORMAT_XBGR2101010: - case GBM_FORMAT_RGBX1010102: - case GBM_FORMAT_BGRX1010102: - return 30; - - case GBM_FORMAT_ARGB8888: - case GBM_FORMAT_ABGR8888: - case GBM_FORMAT_RGBA8888: - case GBM_FORMAT_BGRA8888: - case GBM_FORMAT_ARGB2101010: - case GBM_FORMAT_ABGR2101010: - case GBM_FORMAT_RGBA1010102: - case GBM_FORMAT_BGRA1010102: - return 32; - - default: - LOG_MSG(error, "unknown GBM format " << format); - } - - return 0; -} - - -static int gbm_bpp_from_format(uint32_t format) -{ - if (format == GBM_BO_FORMAT_XRGB8888) - format = GBM_FORMAT_XRGB8888; - if (format == GBM_BO_FORMAT_ARGB8888) - format = GBM_FORMAT_ARGB8888; - - switch(format) - { - case GBM_FORMAT_C8: - case GBM_FORMAT_RGB332: - case GBM_FORMAT_BGR233: - return 8; - - case GBM_FORMAT_NV12: - return 12; - - case GBM_FORMAT_XRGB4444: - case GBM_FORMAT_XBGR4444: - case GBM_FORMAT_RGBX4444: - case GBM_FORMAT_BGRX4444: - case GBM_FORMAT_ARGB4444: - case GBM_FORMAT_ABGR4444: - case GBM_FORMAT_RGBA4444: - case GBM_FORMAT_BGRA4444: - case GBM_FORMAT_XRGB1555: - case GBM_FORMAT_XBGR1555: - case GBM_FORMAT_RGBX5551: - case GBM_FORMAT_BGRX5551: - case GBM_FORMAT_ARGB1555: - case GBM_FORMAT_ABGR1555: - case GBM_FORMAT_RGBA5551: - case GBM_FORMAT_BGRA5551: - case GBM_FORMAT_RGB565: - case GBM_FORMAT_BGR565: - case GBM_FORMAT_YUYV: - case GBM_FORMAT_YVYU: - case GBM_FORMAT_UYVY: - case GBM_FORMAT_VYUY: - return 16; - - case GBM_FORMAT_RGB888: - case GBM_FORMAT_BGR888: - return 24; - - case GBM_FORMAT_XRGB8888: - case GBM_FORMAT_XBGR8888: - case GBM_FORMAT_RGBX8888: - case GBM_FORMAT_BGRX8888: - case GBM_FORMAT_ARGB8888: - case GBM_FORMAT_ABGR8888: - case GBM_FORMAT_RGBA8888: - case GBM_FORMAT_BGRA8888: - case GBM_FORMAT_XRGB2101010: - case GBM_FORMAT_XBGR2101010: - case GBM_FORMAT_RGBX1010102: - case GBM_FORMAT_BGRX1010102: - case GBM_FORMAT_ARGB2101010: - case GBM_FORMAT_ABGR2101010: - case GBM_FORMAT_RGBA1010102: - case GBM_FORMAT_BGRA1010102: - case GBM_FORMAT_AYUV: - return 32; - - default: - LOG_MSG(error, "unknown GBM format " << format); - } - - return 0; -} - - -struct drm_fb -{ - struct gbm_bo *bo; - uint32_t fb_id; -}; - - -static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) -{ - int drm_fd = gbm_device_get_fd(gbm_bo_get_device(bo)); - drm_fb *fb = reinterpret_cast < drm_fb * > (data); - - if (fb->fb_id) - drmModeRmFB(drm_fd, fb->fb_id); - - delete fb; -} - -static drm_fb * drm_fb_get_from_bo(struct gbm_bo *bo) -{ - // We want to use this buffer object (abbr. "bo") as a scanout buffer. - // To that end, we associate the bo with the DRM by using drmModeAddFB(). - // However, this needs to be called only once, and the counterpart, - // drmModeRmFB(), needs to be called when the bo is cleaned up. - // - // To fulfill these requirements, add extra framebuffer information to the - // bo as "user data". This way, if this user data pointer is NULL, it means - // that no framebuffer information was generated yet & the bo was not set - // as a scanout buffer with drmModeAddFB() yet, and we have perform these - // steps. Otherwise, if it is non-NULL, we know we do not have to set up - // anything (since it was done already) and just return the pointer to the - // framebuffer information. - drm_fb *fb = reinterpret_cast < drm_fb * > (gbm_bo_get_user_data(bo)); - if (fb != nullptr) - { - // The bo was already set up as a scanout framebuffer. Just - // return the framebuffer information. - return fb; - } - - // If this point is reached, then we have to setup the bo as a scanout framebuffer. - - int drm_fd = gbm_device_get_fd(gbm_bo_get_device(bo)); - - fb = new drm_fb; - fb->bo = bo; - - uint32_t width = gbm_bo_get_width(bo); - uint32_t height = gbm_bo_get_height(bo); - uint32_t stride = gbm_bo_get_stride(bo); - uint32_t format = gbm_bo_get_format(bo); - uint32_t handle = gbm_bo_get_handle(bo).u32; - - int depth = gbm_depth_from_format(format); - int bpp = gbm_bpp_from_format(format); - - LOG_MSG(debug, "Attempting to add GBM BO as scanout framebuffer " - << " width/height: " << width << "/" << height << " pixels " - << " stride: " << stride << " bytes format: " << gbm_format_to_string(format) - << " depth: " << depth << " bits total bpp: " << bpp << " bits" - ); - - // Set the bo as a scanout framebuffer - int ret = drmModeAddFB( - drm_fd, - width, height, - depth, bpp, - stride, - handle, - &fb->fb_id - ); - if (ret != 0) - { - LOG_MSG(error, "Failed to add GBM BO as scanout framebuffer: " << std::strerror(errno) << " (" << errno << ")"); - delete fb; - return nullptr; - } - - // Add the framebuffer information to the bo as user data, and also install a callback - // that cleans up this extra information whenever the bo itself is discarded - gbm_bo_set_user_data(bo, fb, drm_fb_destroy_callback); - - return fb; -} - - -struct egl_platform::internal -{ - struct gbm_device *m_gbm_dev; - struct gbm_surface *m_gbm_surf; - struct gbm_bo *m_gbm_current_bo, *m_gbm_previous_bo; - int m_waiting_for_flip; - - int m_drm_fd; - drmModeRes *m_drm_mode_resources; - drmModeConnector *m_drm_mode_connector; - drmModeModeInfo *m_drm_mode_info; - int m_crtc_index; - std::uint32_t m_crtc_id; - - - internal() - : m_gbm_dev(nullptr) - , m_gbm_surf(nullptr) - , m_gbm_current_bo(nullptr) - , m_gbm_previous_bo(nullptr) - , m_waiting_for_flip(0) - , m_drm_fd(-1) - , m_drm_mode_resources(nullptr) - , m_drm_mode_connector(nullptr) - , m_drm_mode_info(nullptr) - , m_crtc_index(-1) - , m_crtc_id(INVALID_CRTC) - { - // Structures are not initialized here, but in the main egl_platform constructor. - } - - - ~internal() - { - // NOTE: Unlike with the constructor, the destructor *does* shut down structures - // and states. This makes it easier to use the emergency scoped cleanup in the - // egl_platform constructor (its cleanup steps are exactly what the egl_platform - // destructor also needs to do, so by doing the actual shutdown in here, we avoid - // code duplication in the cleanup & in the egl_platform destructor). - - shutdown_gbm(); - shutdown_drm(); - - if (m_drm_fd < 0) - close(m_drm_fd); - } - - - bool find_drm_node() - { - // Check if a device node was explicitely defined by the environment variable. - - char *drm_node_name = std::getenv("EASYSPLASH_GBM_DRM_DEVICE"); - if (drm_node_name != nullptr) - { - LOG_MSG(debug, "Attempting to open device \"" << drm_node_name << "\" (specified by the EASYSPLASH_GBM_DRM_DEVICE environment variable)"); - m_drm_fd = open(drm_node_name, O_RDWR | O_CLOEXEC); - if (m_drm_fd < 0) - { - LOG_MSG(error, "Could not open DRM device \"" << drm_node_name << "\": " << std::strerror(errno) << " (" << errno << ")"); - } - - return (m_drm_fd >= 0); - } - else - LOG_MSG(debug, "EASYSPLASH_GBM_DRM_DEVICE environment variable is not set - trying to autodetect device"); - - // Either the environment variable was not set, or the device node it - // specifies could not be opened. Try to autodetect a suitable device node. - - struct udev *udev_ctx = nullptr; - udev_enumerate *udev_enum = nullptr; - - auto cleanup = make_scope_guard([&]() { - if (udev_enum != nullptr) - { - udev_enumerate_unref(udev_enum); - udev_enum = nullptr; - LOG_MSG(debug, "Cleaned up udev enumerate object"); - } - - if (udev_ctx != nullptr) - { - udev_unref(udev_ctx); - udev_ctx = nullptr; - LOG_MSG(debug, "Cleaned up udev context"); - } - }); - - // XXX: What if udev isn't available while booting? Use a hardcoded - // /dev/dri/cardX node name as fallback? - udev_ctx = udev_new(); - if (udev_ctx == nullptr) - { - LOG_MSG(error, "Could not create udev context"); - return false; - } - LOG_MSG(debug, "Created udev context"); - - udev_enum = udev_enumerate_new(udev_ctx); - if (udev_enum == nullptr) - { - LOG_MSG(error, "Could not create udev enumerate object"); - return false; - } - LOG_MSG(debug, "Created udev enumerate object"); - - // TODO: To be 100% sure we pick the right device, also check - // if this is a GPU, because a pure scanout device could also - // have a DRM subsystem for example. However, currently it is - // unclear how to do that. By trying to create an EGL context? - udev_enumerate_add_match_subsystem(udev_enum, "drm"); - if (udev_enumerate_scan_devices(udev_enum) < 0) - { - LOG_MSG(error, "Could not scan with udev for devices with a drm subsystem"); - return false; - } - LOG_MSG(debug, "Scanned for udev devices with a drm subsytem"); - - m_drm_fd = -1; - - udev_list_entry *entry; - udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(udev_enum)) - { - char const *syspath = udev_list_entry_get_name(entry); - udev_device *udevice = udev_device_new_from_syspath(udev_ctx, syspath); - char const *devnode = udev_device_get_devnode(udevice); - - if (std::strstr(devnode, "/dev/dri/card") != devnode) - continue; - - LOG_MSG(debug, "Found udev device with syspath \"" << syspath << "\" and device node \"" << devnode << "\""); - - m_drm_fd = open(devnode, O_RDWR | O_CLOEXEC); - if (m_drm_fd < 0) - { - LOG_MSG(warning, "Cannot open device node \"" << devnode << "\": " << std::strerror(errno) << " (" << errno << ")"); - continue; - } - - LOG_MSG(debug, "Device node \"" << devnode << "\" is a valid DRM device node"); - break; - } - - return (m_drm_fd >= 0); - } - - - bool setup_drm() - { - // Scoped cleanup in case an error happens in the middle of this function - auto cleanup = make_scope_guard([&] { shutdown_drm(); }); - - - // Get the DRM mode resources - - m_drm_mode_resources = drmModeGetResources(m_drm_fd); - if (m_drm_mode_resources == nullptr) - { - LOG_MSG(error, "Could not get DRM resources: " << std::strerror(errno) << " (" << errno << ")"); - return false; - } - - LOG_MSG(debug, "Got DRM resources"); - - - // Find a connected connector. The connector is where the pixel data is finally - // sent to, and typically connects to some form of display, like an HDMI TV, - // an LVDS panel etc. - // If the EASYSPLASH_GBM_DRM_CONNECTOR environment variable is set, select - // the first connector with the name the environment variable specifies. - - { - char *drm_connector_name = std::getenv("EASYSPLASH_GBM_DRM_CONNECTOR"); - drmModeConnector *connected_connector = nullptr; - - LOG_MSG(debug, "Checking " << m_drm_mode_resources->count_connectors << " DRM connector(s)"); - if (drm_connector_name != nullptr) - LOG_MSG(debug, "EASYSPLASH_GBM_DRM_CONNECTOR environment variable set to \"" << drm_connector_name << "\"; will use this name to match connector(s) against"); - for (int i = 0; i < m_drm_mode_resources->count_connectors; ++i) - { - drmModeConnector *candidate_connector = drmModeGetConnector(m_drm_fd, m_drm_mode_resources->connectors[i]); - std::string candidate_name = get_name_for_drm_connector(candidate_connector); - LOG_MSG(debug, "Found DRM connector #" << i << " \"" << candidate_name - << "\" with ID " << candidate_connector->connector_id); - - /* If we already picked a connector, and connected_connector is therefore - * non-NULL, then are just printing information about the other connectors - * for logging purposes by now, so don't actually do anything with this - * connector. Just loop instead. */ - if (connected_connector != nullptr) - { - drmModeFreeConnector (candidate_connector); - continue; - } - - // If the EASYSPLASH_GBM_DRM_CONNECTOR environment variable is - // set, check if this connector's name matches that of the - // environment variable. If not, move on to the next connector. - if ((drm_connector_name != nullptr) && (candidate_name != drm_connector_name)) - { - drmModeFreeConnector (candidate_connector); - continue; - } - - if (candidate_connector->connection == DRM_MODE_CONNECTED) - { - if (drm_connector_name != nullptr) - LOG_MSG(debug, "Picking DRM connector #" << i << " because is connected and has a matching name \"" << candidate_name << "\""); - LOG_MSG(debug, "Picking DRM connector #" << i << " because is connected"); - connected_connector = candidate_connector; - } - else - { - LOG_MSG(warning, "DRM connector #" << i << " has a matching name \"" << candidate_name << "\" but is not connected; not picking it"); - drmModeFreeConnector(candidate_connector); - } - } - - if (connected_connector == nullptr) - { - LOG_MSG(error, "No connected DRM connector found"); - return false; - } - - m_drm_mode_connector = connected_connector; - } - - - // Check out what modes are supported by the chosen connector, - // and pick either the "preferred" mode or the one with the largest - // pixel area. - - { - int selected_mode_index = -1; - int selected_mode_area = -1; - bool preferred_mode_found = false; - - LOG_MSG(debug, "Checking " << m_drm_mode_connector->count_modes << " DRM mode(s) from selected connector"); - - for (int i = 0; i < m_drm_mode_connector->count_modes; ++i) - { - drmModeModeInfo *current_mode = &(m_drm_mode_connector->modes[i]); - int current_mode_area = current_mode->hdisplay * current_mode->vdisplay; - - LOG_MSG(debug, "Found DRM mode #" << i - << " width/height " << current_mode->hdisplay << "/" << current_mode->vdisplay - << " hsync/vsync start " << current_mode->hsync_start << "/" << current_mode->vsync_start - << " hsync/vsync end " << current_mode->hsync_end << "/" << current_mode->vsync_end - << " htotal/vtotal " << current_mode->htotal << "/" << current_mode->vtotal - << " hskew " << current_mode->hskew - << " vscan " << current_mode->vscan - << " vrefresh " << current_mode->vrefresh - << " preferred " << !!(current_mode->type & DRM_MODE_TYPE_PREFERRED) - ); - - if (!preferred_mode_found && ((current_mode->type & DRM_MODE_TYPE_PREFERRED) || (current_mode_area > selected_mode_area))) - { - m_drm_mode_info = current_mode; - selected_mode_area = current_mode_area; - selected_mode_index = i; - - if (current_mode->type & DRM_MODE_TYPE_PREFERRED) - preferred_mode_found = true; - } - } - - if (m_drm_mode_info == nullptr) - { - LOG_MSG(error, "No usable DRM mode found"); - return false; - } - - LOG_MSG(debug, "Selected DRM mode #" << selected_mode_index << " (is preferred: " << preferred_mode_found << ")"); - } - - - // Find an encoder that is attached to the chosen connector. Also find the index/id of - // the CRTC associated with this encoder. The encoder takes pixel data from the CRTC and - // transmits it to the connector. The CRTC roughly represents the scanout framebuffer. - // - // Ultimately, we only care about the CRTC index & ID, so the encoder reference is discarded - // here once these are found. The CRTC index is the index in the m_drm_mode_resources' - // CRTC array, while the ID is an identifier used by the DRM to refer to the CRTC universally. - // (We need the CRTC information for page flipping and DRM scanout framebuffer configuration.) - - { - drmModeEncoder *selected_encoder = nullptr; - - LOG_MSG(debug, "Checking " << m_drm_mode_resources->count_encoders << " DRM encoder(s)"); - for (int i = 0; i < m_drm_mode_resources->count_encoders; ++i) - { - drmModeEncoder *candidate_encoder = drmModeGetEncoder(m_drm_fd, m_drm_mode_resources->encoders[i]); - LOG_MSG(debug, "Found DRM encoder #" << i << " \"" << get_name_for_drm_encoder(candidate_encoder) << "\""); - - if ((selected_encoder == nullptr) && (candidate_encoder->encoder_id == m_drm_mode_connector->encoder_id)) - { - selected_encoder = candidate_encoder; - LOG_MSG(debug, "DRM encoder #" << i << " corresponds to selected DRM connector -> selected"); - } - else - drmModeFreeEncoder(candidate_encoder); - } - - if (selected_encoder == nullptr) - { - LOG_MSG(debug, "No encoder found; searching for CRTC ID in the connector"); - m_crtc_id = find_crtc_id_for_connector(); - } - else - { - LOG_MSG(debug, "Using CRTC ID from selected encoder"); - m_crtc_id = selected_encoder->crtc_id; - drmModeFreeEncoder(selected_encoder); - } - - if (m_crtc_id == INVALID_CRTC) - { - LOG_MSG(error, "No CRTC found"); - return false; - } - - LOG_MSG(debug, "CRTC with ID " << m_crtc_id << " found; now locating it in the DRM mode resources CRTC array"); - - for (int i = 0; i < m_drm_mode_resources->count_crtcs; ++i) - { - if (m_drm_mode_resources->crtcs[i] == m_crtc_id) - { - m_crtc_index = i; - break; - } - } - - if (m_crtc_index < 0) - { - LOG_MSG(error, "No matching CRTC entry in DRM resources found"); - return false; - } - - LOG_MSG(debug, "CRTC with ID " << m_crtc_id << " can be found at index #" << m_crtc_index << " in the DRM mode resources CRTC array"); - } - - - // We are done. Dismiss the emergency scoped cleanup. - cleanup.dismiss(); - - LOG_MSG(debug, "DRM structures initialized"); - - return true; - } - - - void shutdown_drm() - { - m_drm_mode_info = nullptr; - - m_crtc_index = -1; - m_crtc_id = INVALID_CRTC; - - if (m_drm_mode_connector != nullptr) - { - drmModeFreeConnector(m_drm_mode_connector); - m_drm_mode_connector = nullptr; - } - - if (m_drm_mode_resources != nullptr) - { - drmModeFreeResources(m_drm_mode_resources); - m_drm_mode_resources = nullptr; - } - - LOG_MSG(debug, "DRM structures shut down"); - } - - - bool setup_gbm() - { - m_gbm_dev = gbm_create_device(m_drm_fd); - if (m_gbm_dev == nullptr) - { - LOG_MSG(error, "Creating GBM device failed"); - return false; - } - - LOG_MSG(debug, "GBM structures initialized"); - - return true; - } - - - void shutdown_gbm() - { - if (m_gbm_surf != nullptr) - { - if (m_gbm_current_bo != nullptr) - { - gbm_surface_release_buffer(m_gbm_surf, m_gbm_current_bo); - m_gbm_current_bo = nullptr; - } - - gbm_surface_destroy(m_gbm_surf); - m_gbm_surf = nullptr; - } - - if (m_gbm_dev != nullptr) - { - gbm_device_destroy(m_gbm_dev); - m_gbm_dev = nullptr; - } - - LOG_MSG(debug, "GBM structures shut down"); - } - - -private: - std::uint32_t find_crtc_id_for_encoder(drmModeEncoder const *encoder) - { - for (int i = 0; i < m_drm_mode_resources->count_crtcs; ++i) - { - // possible_crtcs is a bitmask as described here: - // https://dvdhrm.wordpress.com/2012/09/13/linux-drm-mode-setting-api - std::uint32_t const crtc_mask = 1 << i; - std::uint32_t const crtc_id = m_drm_mode_resources->crtcs[i]; - - if (encoder->possible_crtcs & crtc_mask) - return crtc_id; - } - - // No match found - return INVALID_CRTC; - } - - - std::uint32_t find_crtc_id_for_connector() - { - for (int i = 0; i < m_drm_mode_connector->count_encoders; ++i) - { - std::uint32_t encoder_id = m_drm_mode_connector->encoders[i]; - drmModeEncoder *encoder = drmModeGetEncoder(m_drm_fd, encoder_id); - - if (encoder != nullptr) - { - std::uint32_t crtc_id = find_crtc_id_for_encoder(encoder); - drmModeFreeEncoder(encoder); - - if (crtc_id != INVALID_CRTC) - return crtc_id; - } - } - - // No match found - return INVALID_CRTC; - } -}; // internal class end - - - - -egl_platform::egl_platform() - : m_native_display(0) - , m_native_window(0) - , m_egl_display(EGL_NO_DISPLAY) - , m_egl_context(EGL_NO_CONTEXT) - , m_egl_surface(EGL_NO_SURFACE) - , m_is_valid(false) -{ - // Scoped cleanup in case an error happens in the middle of the constructor - - m_internal = new internal; - auto internal_guard = make_scope_guard([&]() { - // The internal class' destructor takes care of cleaning up GBM, DRM etc. - delete m_internal; - // Make sure the m_internal pointer is set to null to avoid segfaults - // in the egl_platform destructor - m_internal = nullptr; - - m_native_display = 0; - m_native_window = 0; - }); - - - // Initialize display - - if (!m_internal->find_drm_node()) - { - LOG_MSG(error, "Could not find DRM node"); - return; - } - - if (!m_internal->setup_drm()) - { - LOG_MSG(error, "Could not setup DRM connection"); - return; - } - - if (!m_internal->setup_gbm()) - { - LOG_MSG(error, "Could not setup GBM device"); - return; - } - - { - EGLint ver_major, ver_minor; - - // With Mesa, the GBM device *is* the "native display"; - // See the EGL_MESA_platform_gbm documentation for more - m_native_display = m_internal->m_gbm_dev; - - m_egl_display = eglGetDisplay(m_native_display); - if (m_egl_display == EGL_NO_DISPLAY) - { - LOG_MSG(error, "eglGetDisplay failed: " << egl_get_last_error_string()); - return; - } - - if (!eglInitialize(m_egl_display, &ver_major, &ver_minor)) - { - LOG_MSG(error, "eglInitialize failed: " << egl_get_last_error_string()); - return; - } - - LOG_MSG(info, "EGL initialized, version " << ver_major << "." << ver_minor); - } - - - // Initialize window & context - - { - EGLint num_configs; - EGLConfig chosen_config; - EGLint gbm_format; - - // We want a config for an EGL context with OpenGL ES 2.x support and an RGB colorbuffer. - // Also, we want to be able to use a window with this context. - static EGLint const eglconfig_attribs[] = - { - EGL_RED_SIZE, 1, - EGL_GREEN_SIZE, 1, - EGL_BLUE_SIZE, 1, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_NONE - }; - - static EGLint const ctx_attribs[] = - { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - - if (!eglGetConfigs(m_egl_display, nullptr, 0, &num_configs)) - { - LOG_MSG(error, "eglGetConfigs failed: " << egl_get_last_error_string()); - return; - } - - if (!eglChooseConfig(m_egl_display, eglconfig_attribs, &chosen_config, 1, &num_configs)) - { - LOG_MSG(error, "eglChooseConfig failed: " << egl_get_last_error_string()); - return; - } - if (num_configs == 0) - { - LOG_MSG(error, "eglChooseConfig found no matching config"); - return; - } - - if (!eglGetConfigAttrib(m_egl_display, chosen_config, EGL_NATIVE_VISUAL_ID, &gbm_format)) - { - LOG_MSG(error, "eglGetConfigAttrib failed: " << egl_get_last_error_string()); - return; - } - - LOG_MSG(debug, "GBM format (= native EGL visual ID): " << gbm_format_to_string(gbm_format)); - - // Setup the GBM surface that will act as window, a container - // for the EGL context. We'll use this surface to render into - // with the GPU, and also as a scanout buffer for the DRM CRTC. - // Note that unlike with the "classic framebuffer", we do not paint - // into an existing framebuffer memory block; we have to explicitely - // assign a buffer object to the CRTC as scanout framebuffer instead. - m_internal->m_gbm_surf = gbm_surface_create( - m_internal->m_gbm_dev, - m_internal->m_drm_mode_info->hdisplay, m_internal->m_drm_mode_info->vdisplay, - gbm_format, - GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING - ); - if (m_internal->m_gbm_surf == nullptr) - { - LOG_MSG(error, "Could not create GBM surface"); - return; - } - LOG_MSG(debug, "Created GBM surface for rendering and scanout"); - - // With Mesa, the GBM surface *is* the "native window"; - // See the EGL_MESA_platform_gbm documentation for more - m_native_window = m_internal->m_gbm_surf; - - eglBindAPI(EGL_OPENGL_ES_API); - - m_egl_context = eglCreateContext(m_egl_display, chosen_config, EGL_NO_CONTEXT, ctx_attribs); - if (m_egl_context == EGL_NO_CONTEXT) - { - LOG_MSG(error, "eglCreateContext failed: " << egl_get_last_error_string()); - return; - } - LOG_MSG(debug, "Created EGL context with OpenGL ES 2.x support"); - - m_egl_surface = eglCreateWindowSurface(m_egl_display, chosen_config, m_native_window, nullptr); - if (m_egl_surface == EGL_NO_SURFACE) - { - LOG_MSG(error, "eglCreateWindowSurface failed: " << egl_get_last_error_string()); - return; - } - LOG_MSG(debug, "Created EGL window with context attached"); - - if (!make_current()) - return; - - // Explicitely set the viewport to make sure it is expanded to - // encompass the whole GBM surface - glViewport(0, 0, m_internal->m_drm_mode_info->hdisplay, m_internal->m_drm_mode_info->vdisplay); - - // Create the first BO for setting up the CRTC. To do that, we call eglSwapBuffers(), - // which creates a BO. Also, this way, we start with a non-NULL m_gbm_current_bo , - // which is important for the code in swap_buffers() see the comments there for more - // details about how EGL and GBM work together to implement page flipping). - if (!eglSwapBuffers(m_egl_display, m_egl_surface)) - { - LOG_MSG(error, "eglSwapBuffers failed: " << egl_get_last_error_string()); - return; - } - - // Lock the BO that was created by eglSwapBuffer(). This makes it unavailable - // for rendering, but that's fine, since this is just the first BO that will be - // anyway swapped later. - m_internal->m_gbm_current_bo = gbm_surface_lock_front_buffer(m_internal->m_gbm_surf); - drm_fb *framebuf = drm_fb_get_from_bo(m_internal->m_gbm_current_bo); - - // Set m_gbm_current_bo as the data source for the CRTC. Later, during page - // flipping, CRTC setup will be copied over to other BOs. - if (drmModeSetCrtc(m_internal->m_drm_fd, m_internal->m_crtc_id, framebuf->fb_id, 0, 0, &(m_internal->m_drm_mode_connector->connector_id), 1, m_internal->m_drm_mode_info) != 0) - { - LOG_MSG(error, "Could not set DRM CRTC: " << std::strerror(errno) << " (" << errno << ")"); - } - - LOG_MSG(debug, "Page flipping set up"); - } - - - // Construction complete. Dismiss the scoped guard and mark ourselves as valid. - internal_guard.dismiss(); - LOG_MSG(debug, "Mesa GBM EGL platform initialized"); - m_is_valid = true; -} - - -egl_platform::~egl_platform() -{ - // Cleanup the EGL display before everything else, since it relies - // on a valid DRM & GBM setup. - if (m_egl_display != EGL_NO_DISPLAY) - { - LOG_MSG(debug, "Shutting down Mesa GBM EGL display"); - eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglTerminate(m_egl_display); - } - - delete m_internal; - - LOG_MSG(debug, "Mesa GBM EGL platform shut down"); -} - - -bool egl_platform::make_current() -{ - if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context)) - { - LOG_MSG(error, "eglMakeCurrent failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -bool egl_platform::swap_buffers() -{ - // Rendering, page flipping etc. are connect this way: - // - // The frames are stored in buffer objects (BOs). Inside the eglSwapBuffers() - // call, GBM creates new BOs if necessary. BOs can be "locked" for rendering, - // meaning that EGL cannot use them as a render target. If all available - // BOs are locked, the GBM code inside eglSwapBuffers() creates a new, - // unlocked one. We make use of this to implement triple buffering. - // - // There are 3 BOs in play: - // - // * next_bo: The BO we just rendered into. - // * m_gbm_current_bo: The currently displayed BO. - // * m_gbm_previous_bo: The previously displayed BO. - // - // m_gbm_current_bo and m_gbm_previous_bo are involed in page flipping. - // next_bo is not. - // - // Once rendering is done, the next_bo is retrieved and locked. Then, we - // wait until any ongoing page flipping finishes. Once it does, the - // m_gbm_current_bo is displayed on screen, and the m_gbm_previous_bo isn't - // anymore. At this point, it is safe to release the m_gbm_previous_bo, - // which unlocks it and makes it available again as a render target. Then we - // initiate the next page flipping; this time, we flip to next_bo. At that - // point, next_bo becomes m_gbm_current_bo, and m_gbm_current_bo becomes - // m_gbm_previous_bo. - - // Call eglSwapBuffers to get a new unlocked framebuffer. If - // none is available, a new one is created automatically. - if (!eglSwapBuffers(m_egl_display, m_egl_surface)) - { - LOG_MSG(error, "eglSwapBuffers failed: " << egl_get_last_error_string()); - return false; - } - - struct gbm_bo *next_bo = gbm_surface_lock_front_buffer(m_internal->m_gbm_surf); - drm_fb *framebuf = drm_fb_get_from_bo(next_bo); - - // Setup an emergency scoped cleanup in case there is an error, - // to make sure the new "front buffer" is cleaned up and nothing leaks. - auto cleanup = make_scope_guard([&]() { - gbm_surface_release_buffer(m_internal->m_gbm_surf, next_bo); - }); - - // Page flipping handler that sets m_waiting_for_flip to 0, signaling - // that the page flipping is done. The DRM FD will also have input - // data available by then. This is handled by drmHandleEvent(). - auto page_flip_handler = [](int /*fd*/, unsigned int /*frame*/, unsigned int /*sec*/, unsigned int /*usec*/, void *data) -> void - { - int *waiting_for_flip = reinterpret_cast < int* > (data); - *waiting_for_flip = 0; - }; - - // Set up a poll() structure that we'll use to listen for DRM events - struct pollfd pfd = {}; - pfd.fd = m_internal->m_drm_fd; - pfd.events = POLLIN; - pfd.revents = 0; - - // Initially set all handler function pointers to NULL. - drmEventContext evctx = {}; - evctx.version = DRM_EVENT_CONTEXT_VERSION; - evctx.page_flip_handler = page_flip_handler; - - // Wait until any ongoing page flipping is done. After this is done, - // m_gbm_previous_bo is no longer involved in any page flipping, and can be - // safely released. - while (m_internal->m_waiting_for_flip) - { - int ret = poll(&pfd, 1, -1); - - if (ret < 0) - { - if (errno == EINTR) - LOG_MSG(debug, "Signal caught during poll() call"); - else - LOG_MSG(error, "poll() call failed: " << std::strerror(errno) << " (" << errno << ")"); - - return false; - } - - drmHandleEvent(m_internal->m_drm_fd, &evctx); - } - - // Release m_gbm_previous_bo, since it is no longer shown on screen. This does - // not deallocate the buffer, it just returns it to the GBM buffer pool. - if (m_internal->m_gbm_previous_bo != nullptr) - gbm_surface_release_buffer(m_internal->m_gbm_surf, m_internal->m_gbm_previous_bo); - - // Presently, m_gbm_current_bo is shown on screen. Schedule the next page - // flip, this time flip to next_bo. The flip happens asynchronously, so - // we can continue and render etc. in the meantime. - m_internal->m_waiting_for_flip = 1; - int ret = drmModePageFlip(m_internal->m_drm_fd, m_internal->m_crtc_id, framebuf->fb_id, DRM_MODE_PAGE_FLIP_EVENT, &(m_internal->m_waiting_for_flip)); - if (ret != 0) - { - // NOTE: According to libdrm sources, the page is _not_ - // considered flipped if drmModePageFlip() reports an error, - // so we do not update the m_gbm_current_bo pointer here - LOG_MSG(error, "Could not flip DRM pages: " << std::strerror(-ret) << " (" << (-ret) << ")"); - return false; - } - - // At this point, we relabel the m_gbm_current_bo as the m_gbm_previous_bo. - // This may not actually be the case yet, but it will be soon - latest - // when the wait loop above finishes. - // Also, next_bo becomes m_gbm_current_bo. - m_internal->m_gbm_previous_bo = m_internal->m_gbm_current_bo; - m_internal->m_gbm_current_bo = next_bo; - - // All done. We can dismiss the emergency cleanup procedure. - cleanup.dismiss(); - - return true; -} - - -long egl_platform::get_display_width() const -{ - return m_internal->m_drm_mode_info->hdisplay; -} - - -long egl_platform::get_display_height() const -{ - return m_internal->m_drm_mode_info->vdisplay; -} - - -} // namespace easysplash end diff --git a/src/eglgles/egl_platform_rpi_dispmanx.cpp b/src/eglgles/egl_platform_rpi_dispmanx.cpp deleted file mode 100644 index 33e0a71..0000000 --- a/src/eglgles/egl_platform_rpi_dispmanx.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014, 2015 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include "scope_guard.hpp" -#include "egl_platform.hpp" -#include "gl_misc.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -struct egl_platform::internal -{ - DISPMANX_DISPLAY_HANDLE_T m_dispman_display; - - internal() - : m_dispman_display(DISPMANX_NO_HANDLE) - { - // Device 0 is the LCD - m_dispman_display = vc_dispmanx_display_open(0); - if (m_dispman_display == DISPMANX_NO_HANDLE) - { - LOG_MSG(error, "could not open dispmanx display"); - return; - } - - uint32_t width, height; - graphics_get_display_size(0/* LCD */, &width, &height); - LOG_MSG(debug, "LCD display size: " << width << "x" << height << " pixels"); - - VC_RECT_T dst_rect; - dst_rect.x = 0; - dst_rect.y = 0; - dst_rect.width = width; - dst_rect.height = height; - - VC_RECT_T src_rect; - src_rect.x = 0; - src_rect.y = 0; - src_rect.width = width << 16; - src_rect.height = height << 16; - - DISPMANX_UPDATE_HANDLE_T dispman_update = vc_dispmanx_update_start(0); - if (dispman_update == DISPMANX_NO_HANDLE) - { - LOG_MSG(error, "could not start dispmanx update"); - return; - } - - DISPMANX_ELEMENT_HANDLE_T dispman_element = vc_dispmanx_element_add( - dispman_update, - m_dispman_display, - 0, - &dst_rect, - 0, - &src_rect, - DISPMANX_PROTECTION_NONE, - 0, - 0, - DISPMANX_NO_ROTATE - ); - - vc_dispmanx_update_submit_sync(dispman_update); - - if (dispman_element == DISPMANX_NO_HANDLE) - { - LOG_MSG(error, "could not add dispmanx element to update"); - return; - } - - m_dispman_window.element = dispman_element; - m_dispman_window.width = width; - m_dispman_window.height = height; - - LOG_MSG(debug, "dispmanx window created successfully"); - } - - ~internal() - { - if (m_dispman_display != DISPMANX_NO_HANDLE) - { - DISPMANX_UPDATE_HANDLE_T dispman_update = vc_dispmanx_update_start(0); - vc_dispmanx_element_remove(dispman_update, m_dispman_window.element); - vc_dispmanx_update_submit_sync(dispman_update); - vc_dispmanx_display_close(m_dispman_display); - } - } - - bool is_valid() const - { - return m_dispman_display != DISPMANX_NO_HANDLE; - } - - EGLNativeWindowType get_egl_native_window() - { - return reinterpret_cast < EGLNativeWindowType > (&m_dispman_window); - } - - EGL_DISPMANX_WINDOW_T m_dispman_window; -}; - - -egl_platform::egl_platform() - : m_native_display(EGL_DEFAULT_DISPLAY) - , m_native_window(0) - , m_egl_display(EGL_NO_DISPLAY) - , m_egl_context(EGL_NO_CONTEXT) - , m_egl_surface(EGL_NO_SURFACE) - , m_is_valid(false) -{ - bcm_host_init(); - auto bcm_host_guard = make_scope_guard([&]() { bcm_host_deinit(); }); - - m_internal = new internal; - auto internal_guard = make_scope_guard([&]() { delete m_internal; }); - - if (!m_internal->is_valid()) - return; - - // Initialize the EGL display - { - EGLint ver_major, ver_minor; - - m_egl_display = eglGetDisplay(m_native_display); - if (m_egl_display == EGL_NO_DISPLAY) - { - LOG_MSG(error, "eglGetDisplay failed: " << egl_get_last_error_string()); - return; - } - - if (!eglInitialize(m_egl_display, &ver_major, &ver_minor)) - { - LOG_MSG(error, "eglInitialize failed: " << egl_get_last_error_string()); - return; - } - - LOG_MSG(info, "Broadcom Display manager service EGL platform initialized, using EGL " << ver_major << "." << ver_minor); - } - - - // Initialize window & context - - { - EGLint num_configs; - EGLConfig config; - - static EGLint const eglconfig_attribs[] = - { - EGL_RED_SIZE, 1, - EGL_GREEN_SIZE, 1, - EGL_BLUE_SIZE, 1, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_NONE - }; - - static EGLint const ctx_attribs[] = - { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - - if (!eglChooseConfig(m_egl_display, eglconfig_attribs, &config, 1, &num_configs)) - { - LOG_MSG(error, "eglChooseConfig failed: " << egl_get_last_error_string()); - return; - } - - eglBindAPI(EGL_OPENGL_ES_API); - - m_egl_context = eglCreateContext(m_egl_display, config, EGL_NO_CONTEXT, ctx_attribs); - if (m_egl_context == EGL_NO_CONTEXT) - { - LOG_MSG(error, "eglCreateContext failed: " << egl_get_last_error_string()); - return; - } - - m_egl_surface = eglCreateWindowSurface(m_egl_display, config, m_internal->get_egl_native_window(), NULL); - if (m_egl_surface == EGL_NO_SURFACE) - { - LOG_MSG(error, "eglCreateWindowSurface failed: " << egl_get_last_error_string()); - return; - } - - if (!make_current()) - return; - - glViewport(0, 0, m_internal->m_dispman_window.width, m_internal->m_dispman_window.height); - } - - internal_guard.dismiss(); - bcm_host_guard.dismiss(); - - m_is_valid = true; -} - - -egl_platform::~egl_platform() -{ - if (m_egl_display != EGL_NO_DISPLAY) - { - LOG_MSG(debug, "Shutting down dispmanx display"); - eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglTerminate(m_egl_display); - } - - delete m_internal; - - bcm_host_deinit(); -} - - -bool egl_platform::make_current() -{ - if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context)) - { - LOG_MSG(error, "eglMakeCurrent failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -bool egl_platform::swap_buffers() -{ - if (!eglSwapBuffers(m_egl_display, m_egl_surface)) - { - LOG_MSG(error, "eglSwapBuffers failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -long egl_platform::get_display_width() const -{ - return m_internal->m_dispman_window.width; -} - - -long egl_platform::get_display_height() const -{ - return m_internal->m_dispman_window.height; -} - - -} // namespace easysplash end diff --git a/src/eglgles/egl_platform_viv_fb.cpp b/src/eglgles/egl_platform_viv_fb.cpp deleted file mode 100644 index db51699..0000000 --- a/src/eglgles/egl_platform_viv_fb.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014, 2015 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include "scope_guard.hpp" -#include "egl_platform.hpp" -#include "gl_misc.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -struct egl_platform::internal -{ - long m_display_width, m_display_height; -}; - - -egl_platform::egl_platform() - : m_native_display(0) - , m_native_window(0) - , m_egl_display(EGL_NO_DISPLAY) - , m_egl_context(EGL_NO_CONTEXT) - , m_egl_surface(EGL_NO_SURFACE) - , m_is_valid(false) -{ - m_internal = new internal; - auto internal_guard = make_scope_guard([&]() { delete m_internal; }); - - // Initialize display - - // Force the driver to configure the framebuffer for double buffering and vsync - // unless the EASYSPLASH_NO_FB_MULTI_BUFFER environment variable is set - if (getenv("EASYSPLASH_NO_FB_MULTI_BUFFER") == nullptr) - { - LOG_MSG(info, "Enabling double buffering and vsync"); - static std::string envvar("FB_MULTI_BUFFER=2"); - putenv(&(envvar[0])); - } - - // Disable Vivante framebuffer clear - if (getenv("EASYSPLASH_NO_DISABLE_CLEAR") == nullptr) - { - LOG_MSG(info, "Disabling framebuffer clear"); - static std::string envvar("GPU_VIV_DISABLE_CLEAR_FB=1"); - putenv(&(envvar[0])); - } - - // Initialize the EGL display - { - EGLint ver_major, ver_minor; - - m_native_display = fbGetDisplayByIndex(0); - - m_egl_display = eglGetDisplay(m_native_display); - if (m_egl_display == EGL_NO_DISPLAY) - { - LOG_MSG(error, "eglGetDisplay failed: " << egl_get_last_error_string()); - return; - } - - if (!eglInitialize(m_egl_display, &ver_major, &ver_minor)) - { - LOG_MSG(error, "eglInitialize failed: " << egl_get_last_error_string()); - return; - } - - LOG_MSG(info, "Framebuffer EGL platform initialized, using EGL " << ver_major << "." << ver_minor); - } - - - // Initialize window & context - - { - EGLint num_configs; - EGLConfig config; - int actual_x, actual_y, actual_width, actual_height; - - static EGLint const eglconfig_attribs[] = - { - EGL_RED_SIZE, 1, - EGL_GREEN_SIZE, 1, - EGL_BLUE_SIZE, 1, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_NONE - }; - - static EGLint const ctx_attribs[] = - { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - - if (!eglChooseConfig(m_egl_display, eglconfig_attribs, &config, 1, &num_configs)) - { - LOG_MSG(error, "eglChooseConfig failed: " << egl_get_last_error_string()); - return; - } - - m_native_window = fbCreateWindow(m_native_display, 0, 0, 0, 0); - - fbGetWindowGeometry(m_native_window, &actual_x, &actual_y, &actual_width, &actual_height); - LOG_MSG(debug, "fbGetWindowGeometry: x/y " << actual_x << "/" << actual_y << " width/height " << actual_width << "/" << actual_height); - - eglBindAPI(EGL_OPENGL_ES_API); - - m_egl_context = eglCreateContext(m_egl_display, config, EGL_NO_CONTEXT, ctx_attribs); - if (m_egl_context == EGL_NO_CONTEXT) - { - LOG_MSG(error, "eglCreateContext failed: " << egl_get_last_error_string()); - return; - } - - m_egl_surface = eglCreateWindowSurface(m_egl_display, config, m_native_window, NULL); - if (m_egl_surface == EGL_NO_SURFACE) - { - LOG_MSG(error, "eglCreateWindowSurface failed: " << egl_get_last_error_string()); - return; - } - - if (!make_current()) - return; - - m_internal->m_display_width = actual_width; - m_internal->m_display_height = actual_height; - - glViewport(actual_x, actual_y, actual_width, actual_height); - } - - internal_guard.dismiss(); - m_is_valid = true; -} - - -egl_platform::~egl_platform() -{ - if (m_egl_display != EGL_NO_DISPLAY) - { - LOG_MSG(debug, "Shutting down VIV FB EGL display"); - eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglTerminate(m_egl_display); - } - - delete m_internal; -} - - -bool egl_platform::make_current() -{ - if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context)) - { - LOG_MSG(error, "eglMakeCurrent failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -bool egl_platform::swap_buffers() -{ - if (!eglSwapBuffers(m_egl_display, m_egl_surface)) - { - LOG_MSG(error, "eglSwapBuffers failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -long egl_platform::get_display_width() const -{ - return m_internal->m_display_width; -} - - -long egl_platform::get_display_height() const -{ - return m_internal->m_display_height; -} - - -} // namespace easysplash end diff --git a/src/eglgles/egl_platform_x11.cpp b/src/eglgles/egl_platform_x11.cpp deleted file mode 100644 index 332aab2..0000000 --- a/src/eglgles/egl_platform_x11.cpp +++ /dev/null @@ -1,285 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include - -#include -#include -#include -#include - -#include "scope_guard.hpp" -#include "egl_platform.hpp" -#include "gl_misc.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -struct egl_platform::internal -{ - Atom m_wm_delete_atom; - long m_display_width, m_display_height; -}; - - -egl_platform::egl_platform() - : m_native_display(0) - , m_native_window(0) - , m_egl_display(EGL_NO_DISPLAY) - , m_egl_context(EGL_NO_CONTEXT) - , m_egl_surface(EGL_NO_SURFACE) - , m_is_valid(false) -{ - m_internal = new internal; - auto internal_guard = make_scope_guard([&]() - { - if (m_native_display != NULL) - XCloseDisplay(reinterpret_cast < Display* > (m_native_display)); - delete m_internal; - }); - - Display *x11_display; - - - // Initialize display - - { - EGLint ver_major, ver_minor; - - x11_display = XOpenDisplay(NULL); - if (x11_display == NULL) - { - LOG_MSG(error, "could not open X display"); - return; - } - - m_native_display = EGLNativeDisplayType(x11_display); - - m_egl_display = eglGetDisplay(m_native_display); - if (m_egl_display == EGL_NO_DISPLAY) - { - LOG_MSG(error, "eglGetDisplay failed: " << egl_get_last_error_string()); - XCloseDisplay(x11_display); - return; - } - - if (!eglInitialize(m_egl_display, &ver_major, &ver_minor)) - { - LOG_MSG(error, "eglInitialize failed: " << egl_get_last_error_string()); - XCloseDisplay(x11_display); - return; - } - - LOG_MSG(info, "X11 EGL platform initialized, using EGL " << ver_major << "." << ver_minor); - } - - - // Initialize window & context - - { - EGLint num_configs; - EGLConfig config; - Window x11_window; - - static EGLint const eglconfig_attribs[] = - { - EGL_RED_SIZE, 1, - EGL_GREEN_SIZE, 1, - EGL_BLUE_SIZE, 1, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_NONE - }; - - static EGLint const ctx_attribs[] = - { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE - }; - - if (!eglChooseConfig(m_egl_display, eglconfig_attribs, &config, 1, &num_configs)) - { - LOG_MSG(error, "eglChooseConfig failed: " << egl_get_last_error_string()); - return; - } - - // Create X11 window - - { - EGLint native_visual_id; - XVisualInfo visual_info_template; - XVisualInfo *visual_info; - int num_matching_visuals; - XSetWindowAttributes attr; - int screen_num; - Window root_window; - int x_coord, y_coord; - unsigned int chosen_width, chosen_height; - - LOG_MSG(debug, "creating new X11 window with EGL context"); - - if (!eglGetConfigAttrib(m_egl_display, config, EGL_NATIVE_VISUAL_ID, &native_visual_id)) - { - LOG_MSG(error, "eglGetConfigAttrib failed: " << egl_get_last_error_string()); - return; - } - - screen_num = DefaultScreen(x11_display); - root_window = RootWindow(x11_display, screen_num); - - std::memset(&visual_info_template, 0, sizeof(visual_info_template)); - visual_info_template.visualid = native_visual_id; - - visual_info = XGetVisualInfo(x11_display, VisualIDMask, &visual_info_template, &num_matching_visuals); - if (visual_info == nullptr) - { - LOG_MSG(error, "Could not get visual info for native visual ID " << native_visual_id); - return; - } - - std::memset(&attr, 0, sizeof(attr)); - attr.background_pixmap = None; - attr.background_pixel = BlackPixel(x11_display, screen_num); - attr.border_pixmap = CopyFromParent; - attr.border_pixel = BlackPixel(x11_display, screen_num); - attr.backing_store = NotUseful; - attr.override_redirect = False; - attr.cursor = None; - - x_coord = 0; - y_coord = 0; - chosen_width = 1024; - chosen_height = 768; - - x11_window = XCreateWindow( - x11_display, root_window, - x_coord, - y_coord, - chosen_width, - chosen_height, - 0, visual_info->depth, InputOutput, visual_info->visual, - CWBackPixel | CWColormap | CWBorderPixel | CWBackingStore | CWOverrideRedirect, - &attr - ); - - XFree(visual_info); - - m_native_window = EGLNativeWindowType(x11_window); - - m_internal->m_wm_delete_atom = XInternAtom(x11_display, "WM_DELETE_WINDOW", True); - XSetWMProtocols(x11_display, x11_window, &(m_internal->m_wm_delete_atom), 1); - - XStoreName(x11_display, x11_window, "EGL window"); - - XSizeHints sizehints; - sizehints.x = 0; - sizehints.y = 0; - sizehints.width = sizehints.min_width = sizehints.max_width = chosen_width; - sizehints.height = sizehints.min_height = sizehints.max_height = chosen_height; - sizehints.flags = PPosition | PSize; - XSetWMNormalHints(x11_display, x11_window, &sizehints); - - XClearWindow(x11_display, x11_window); - XMapRaised(x11_display, x11_window); - - XSync(x11_display, False); - } - - eglBindAPI(EGL_OPENGL_ES_API); - - m_egl_context = eglCreateContext(m_egl_display, config, EGL_NO_CONTEXT, ctx_attribs); - if (m_egl_context == EGL_NO_CONTEXT) - { - LOG_MSG(error, "eglCreateContext failed: " << egl_get_last_error_string()); - return; - } - - m_egl_surface = eglCreateWindowSurface(m_egl_display, config, m_native_window, NULL); - if (m_egl_surface == EGL_NO_SURFACE) - { - LOG_MSG(error, "eglCreateWindowSurface failed: " << egl_get_last_error_string()); - return; - } - - if (!make_current()) - return; - - { - XWindowAttributes window_attr; - XGetWindowAttributes(x11_display, x11_window, &window_attr); - - m_internal->m_display_width = window_attr.width; - m_internal->m_display_height = window_attr.height; - - LOG_MSG(debug, "X11 window size is " << m_internal->m_display_width << "x" << m_internal->m_display_height); - } - - glViewport(0, 0, m_internal->m_display_width, m_internal->m_display_height); - } - - internal_guard.dismiss(); - m_is_valid = true; -} - - -egl_platform::~egl_platform() -{ - if (m_egl_display != EGL_NO_DISPLAY) - { - LOG_MSG(debug, "Shutting down X11 EGL display"); - eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglTerminate(m_egl_display); - } - - if (m_native_display != NULL) - XCloseDisplay(reinterpret_cast < Display* > (m_native_display)); - - delete m_internal; -} - - -bool egl_platform::make_current() -{ - if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context)) - { - LOG_MSG(error, "eglMakeCurrent failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -bool egl_platform::swap_buffers() -{ - if (!eglSwapBuffers(m_egl_display, m_egl_surface)) - { - LOG_MSG(error, "eglSwapBuffers failed: " << egl_get_last_error_string()); - return false; - } - else - return true; -} - - -long egl_platform::get_display_width() const -{ - return m_internal->m_display_width; -} - - -long egl_platform::get_display_height() const -{ - return m_internal->m_display_height; -} - - -} // namespace easysplash end diff --git a/src/eglgles/gl_misc.cpp b/src/eglgles/gl_misc.cpp deleted file mode 100644 index 98454fe..0000000 --- a/src/eglgles/gl_misc.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include "gl_misc.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -std::string egl_get_last_error_string(void) -{ - return egl_get_error_string(eglGetError()); -} - - -std::string egl_get_error_string(EGLint err) -{ - if (err == EGL_SUCCESS) - return "success"; - - switch (err) - { - case EGL_NOT_INITIALIZED: return "not initialized"; - case EGL_BAD_ACCESS: return "bad access"; - case EGL_BAD_ALLOC: return "bad alloc"; - case EGL_BAD_ATTRIBUTE: return "bad attribute"; - case EGL_BAD_CONTEXT: return "bad context"; - case EGL_BAD_CONFIG: return "bad config"; - case EGL_BAD_CURRENT_SURFACE: return "bad current surface"; - case EGL_BAD_DISPLAY: return "bad display"; - case EGL_BAD_SURFACE: return "bad surface"; - case EGL_BAD_MATCH: return "bad match"; - case EGL_BAD_PARAMETER: return "bad parameter"; - case EGL_BAD_NATIVE_PIXMAP: return "bad native pixmap"; - case EGL_BAD_NATIVE_WINDOW: return "bad native window"; - case EGL_CONTEXT_LOST: return "context lost"; - default: return ""; - } -} - - -bool gles_check_gl_error(std::string const &p_category, std::string const &p_label) -{ - GLenum err = glGetError(); - if (err == GL_NO_ERROR) - return true; - - switch (err) - { - case GL_INVALID_ENUM: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: invalid enum"); break; - case GL_INVALID_VALUE: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: invalid value"); break; - case GL_INVALID_OPERATION: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: invalid operation"); break; - case GL_INVALID_FRAMEBUFFER_OPERATION: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: invalid framebuffer operation"); break; - case GL_OUT_OF_MEMORY: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: out of memory"); break; - default: LOG_MSG(error, "[" << p_category << "] [" << p_label << "] error: unknown GL error 0x" << std::hex << err << std::dec); break; - } - - return false; -} - - -} // namespace easysplash end diff --git a/src/eglgles/gl_misc.hpp b/src/eglgles/gl_misc.hpp deleted file mode 100644 index 6eb2ad5..0000000 --- a/src/eglgles/gl_misc.hpp +++ /dev/null @@ -1,28 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_GL_MISC_HPP -#define EASYSPLASH_GL_MISC_HPP - -#include -#include "opengl.hpp" - - -namespace easysplash -{ - - -std::string egl_get_last_error_string(void); -std::string egl_get_error_string(EGLint err); -bool gles_check_gl_error(std::string const &p_category, std::string const &p_label); - - -} // namespace easysplash end - - -#endif diff --git a/src/eglgles/gles_display.cpp b/src/eglgles/gles_display.cpp deleted file mode 100644 index fef3e2a..0000000 --- a/src/eglgles/gles_display.cpp +++ /dev/null @@ -1,378 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include "gles_display.hpp" -#include "linux_framebuffer.hpp" -#include "log.hpp" -#include "gl_misc.hpp" - - -namespace easysplash -{ - - -namespace -{ - - -static char const * simple_vertex_shader = - "attribute vec2 position; \n" - "attribute vec2 texcoords; \n" - "varying vec2 uv; \n" - "uniform vec4 frame_rect; \n" - "void main(void) \n" - "{ \n" - " uv = texcoords; \n" - " gl_Position = vec4(position * frame_rect.xy + frame_rect.zw, 1.0, 1.0); \n" - "} \n" - ; - -static char const * simple_fragment_shader = - "precision mediump float;\n" - "varying vec2 uv; \n" - "uniform sampler2D tex; \n" - "void main(void) \n" - "{ \n" - " vec4 texel = texture2D(tex, uv); \n" - " gl_FragColor = vec4(texel.rgb, 1.0); \n" - "} \n" - ; - - -static GLfloat const vertex_data[] = { - -1, -1, 0, 1, - -1, 1, 0, 0, - 1, -1, 1, 1, - 1, 1, 1, 0, -}; -static unsigned int const vertex_data_size = sizeof(GLfloat)*16; -static unsigned int const vertex_size = sizeof(GLfloat)*4; -static unsigned int const vertex_position_num = 2; -static unsigned int const vertex_position_offset = sizeof(GLfloat)*0; -static unsigned int const vertex_texcoords_num = 2; -static unsigned int const vertex_texcoords_offset = sizeof(GLfloat)*2; - - -bool build_shader(GLuint &p_shader, GLenum p_shader_type, char const *p_code) -{ - GLint compilation_status, info_log_length; - char const *shader_type_name; - - switch (p_shader_type) - { - case GL_VERTEX_SHADER: shader_type_name = "vertex shader"; break; - case GL_FRAGMENT_SHADER: shader_type_name = "fragment shader"; break; - default: - LOG_MSG(error, "unknown shader type 0x" << std::hex << p_shader_type << std::dec); - return false; - } - - glGetError(); /* clear out any existing error */ - - p_shader = glCreateShader(p_shader_type); - if (!gles_check_gl_error(shader_type_name, "glCreateShader")) - return false; - - glShaderSource(p_shader, 1, &p_code, NULL); - if (!gles_check_gl_error(shader_type_name, "glShaderSource")) - return false; - - glCompileShader(p_shader); - if (!gles_check_gl_error(shader_type_name, "glCompileShader")) - return false; - - glGetShaderiv(p_shader, GL_COMPILE_STATUS, &compilation_status); - if (compilation_status == GL_FALSE) - { - LOG_MSG(error, "compiling " << shader_type_name << " failed"); - glGetShaderiv(p_shader, GL_INFO_LOG_LENGTH, &info_log_length); - std::string info_log; - info_log.resize(info_log_length); - glGetShaderInfoLog(p_shader, info_log_length, NULL, &(info_log[0])); - LOG_MSG(debug, "compilation log:\n" << info_log); - return false; - } - else - LOG_MSG(debug, "successfully compiled " << shader_type_name); - - return true; -} - - -bool destroy_shader(GLuint &p_shader, GLenum p_shader_type) -{ - char const *shader_type_name; - - if (p_shader == 0) - return true; - - switch (p_shader_type) - { - case GL_VERTEX_SHADER: shader_type_name = "vertex shader"; break; - case GL_FRAGMENT_SHADER: shader_type_name = "fragment shader"; break; - default: - LOG_MSG(error, "unknown shader type 0x" << std::hex << p_shader_type << std::dec); - return false; - } - - glGetError(); /* clear out any existing error */ - - glDeleteShader(p_shader); - p_shader = 0; - if (!gles_check_gl_error(shader_type_name, "glDeleteShader")) - return false; - - return true; -} - - -bool link_program(GLuint &p_program, GLuint p_vertex_shader, GLuint p_fragment_shader) -{ - GLint link_status, info_log_length; - - glGetError(); /* clear out any existing error */ - - p_program = glCreateProgram(); - if (!gles_check_gl_error("program", "glCreateProgram")) - return false; - - glAttachShader(p_program, p_vertex_shader); - if (!gles_check_gl_error("program vertex", "glAttachShader")) - return false; - - glAttachShader(p_program, p_fragment_shader); - if (!gles_check_gl_error("program fragment", "glAttachShader")) - return false; - - glLinkProgram(p_program); - if (!gles_check_gl_error("program", "glLinkProgram")) - return false; - - glGetProgramiv(p_program, GL_LINK_STATUS, &link_status); - if (link_status == GL_FALSE) - { - LOG_MSG(error, "linking program failed"); - glGetProgramiv(p_program, GL_INFO_LOG_LENGTH, &info_log_length); - std::string info_log; - info_log.resize(info_log_length); - glGetProgramInfoLog(p_program, info_log_length, NULL, &(info_log[0])); - LOG_MSG(debug, "linker log:\n" << info_log); - return false; - } - else - LOG_MSG(debug, "successfully linked program"); - - glUseProgram(p_program); - - return true; -} - - -bool destroy_program(GLuint &p_program, GLuint p_vertex_shader, GLuint p_fragment_shader) -{ - if (p_program == 0) - return true; - - glGetError(); /* clear out any existing error */ - - glUseProgram(0); - if (!gles_check_gl_error("program", "glUseProgram")) - return false; - - glDetachShader(p_program, p_vertex_shader); - if (!gles_check_gl_error("program vertex", "glDetachShader")) - return false; - - glDetachShader(p_program, p_fragment_shader); - if (!gles_check_gl_error("program fragment", "glDetachShader")) - return false; - - glDeleteProgram(p_program); - p_program = 0; - if (!gles_check_gl_error("program", "glDeleteProgram")) - return false; - - return true; -} - - -bool build_vertex_buffer(GLuint &p_vertex_buffer) -{ - glGetError(); /* clear out any existing error */ - - glGenBuffers(1, &p_vertex_buffer); - glBindBuffer(GL_ARRAY_BUFFER, p_vertex_buffer); - /* TODO: This has to be called twice, otherwise the vertex data gets corrupted after the first few - * rendered frames. Is this a Vivante driver bug? */ - glBufferData(GL_ARRAY_BUFFER, vertex_data_size, vertex_data, GL_STATIC_DRAW); - glBufferData(GL_ARRAY_BUFFER, vertex_data_size, vertex_data, GL_STATIC_DRAW); - if (!gles_check_gl_error("vertex buffer", "glBufferData")) - return false; - - return true; -} - - -bool destroy_vertex_buffer(GLuint &p_vertex_buffer) -{ - glGetError(); /* clear out any existing error */ - - if (p_vertex_buffer != 0) - { - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteBuffers(1, &p_vertex_buffer); - p_vertex_buffer = 0; - } - - return true; -} - - -} // unnamed namespace end - - - - -gles_display::gles_display() - : m_is_valid(false) - , m_vertex_shader(0) - , m_fragment_shader(0) - , m_program(0) - , m_vertex_buffer(0) -{ - if (!m_egl_platform.is_valid()) - return; - - glViewport(0, 0, get_width(), get_height()); - - if (!build_shader(m_vertex_shader, GL_VERTEX_SHADER, simple_vertex_shader)) - return; - if (!build_shader(m_fragment_shader, GL_FRAGMENT_SHADER, simple_fragment_shader)) - return; - if (!link_program(m_program, m_vertex_shader, m_fragment_shader)) - return; - - m_tex_uloc = glGetUniformLocation(m_program, "tex"); - m_frame_rect_uloc = glGetUniformLocation(m_program, "frame_rect"); - m_position_aloc = glGetAttribLocation(m_program, "position"); - m_texcoords_aloc = glGetAttribLocation(m_program, "texcoords"); - - /* set texture unit value for tex uniform */ - glUniform1i(m_tex_uloc, 0); - - glUniform4f(m_frame_rect_uloc, 1.0f, 1.0f, 0.0f, 0.0f); - - /* build vertex and index buffer objects */ - if (!build_vertex_buffer(m_vertex_buffer)) - return; - - /* enable vertex attrib array and set up pointers */ - glEnableVertexAttribArray(m_position_aloc); - if (!gles_check_gl_error("position vertex attrib", "glEnableVertexAttribArray")) - return; - glEnableVertexAttribArray(m_texcoords_aloc); - if (!gles_check_gl_error("texcoords vertex attrib", "glEnableVertexAttribArray")) - return; - - glVertexAttribPointer(m_position_aloc, vertex_position_num, GL_FLOAT, GL_FALSE, vertex_size, (GLvoid const*)((uintptr_t)vertex_position_offset)); - if (!gles_check_gl_error("position vertex attrib", "glVertexAttribPointer")) - return; - glVertexAttribPointer(m_texcoords_aloc, vertex_texcoords_num, GL_FLOAT, GL_FALSE, vertex_size, (GLvoid const*)((uintptr_t)vertex_texcoords_offset)); - if (!gles_check_gl_error("texcoords vertex attrib", "glVertexAttribPointer")) - return; - - enable_framebuffer_cursor(false); - - m_is_valid = true; -} - - -gles_display::~gles_display() -{ - enable_framebuffer_cursor(true); - - bool ret = true; - - glDisableVertexAttribArray(m_position_aloc); - ret = gles_check_gl_error("position vertex attrib", "glDisableVertexAttribArray") && ret; - glDisableVertexAttribArray(m_texcoords_aloc); - ret = gles_check_gl_error("texcoords vertex attrib", "glDisableVertexAttribArray") && ret; - /* destroy vertex and index buffer objects */ - ret = destroy_vertex_buffer(m_vertex_buffer) && ret; - - /* destroy shaders and program */ - ret = destroy_program(m_program, m_vertex_shader, m_fragment_shader) && ret; - ret = destroy_shader(m_vertex_shader, GL_VERTEX_SHADER) && ret; - ret = destroy_shader(m_fragment_shader, GL_FRAGMENT_SHADER) && ret; -} - - -bool gles_display::is_valid() const -{ - return m_is_valid && m_egl_platform.is_valid(); -} - - -long gles_display::get_width() const -{ - return m_egl_platform.get_display_width(); -} - - -long gles_display::get_height() const -{ - return m_egl_platform.get_display_height(); -} - - -display::image_handle gles_display::load_image(image const &p_image) -{ - texture tex(create_texture_from(p_image)); - display::image_handle imghandle = display::image_handle(tex.get_name()); - m_textures[tex.get_name()] = std::move(tex); - return imghandle; -} - - -void gles_display::unload_image(image_handle const p_image_handle) -{ - auto iter = m_textures.find(GLuint(p_image_handle)); - if (iter != m_textures.end()) - m_textures.erase(iter); -} - - -void gles_display::draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2) -{ - long draw_w = p_x2 - p_x1 + 1; - long draw_h = p_y2 - p_y1 + 1; - - glUniform4f( - m_frame_rect_uloc, - float(draw_w) / get_width(), float(draw_h) / get_height(), - float((p_x1 + p_x2 - get_width()) / 2) / get_width(), float((p_y1 + p_y2 - get_height()) / 2) / get_height() - ); - - glBindTexture(texture::target, GLuint(p_image_handle)); - if (!gles_check_gl_error("render", "glBindTexture")) - return; - - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - if (!gles_check_gl_error("render", "glDrawArrays")) - return; -} - - -void gles_display::swap_buffers() -{ - m_egl_platform.swap_buffers(); -} - - -} // namespace easysplash end diff --git a/src/eglgles/gles_display.hpp b/src/eglgles/gles_display.hpp deleted file mode 100644 index d142ced..0000000 --- a/src/eglgles/gles_display.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_GLES_DISPLAY_HPP -#define EASYSPLASH_GLES_DISPLAY_HPP - -#include -#include "display.hpp" -#include "texture.hpp" -#include "opengl.hpp" -#include "egl_platform.hpp" - - -namespace easysplash -{ - - -class gles_display - : public display -{ -public: - gles_display(); - ~gles_display(); - - virtual bool is_valid() const; - - virtual long get_width() const; - virtual long get_height() const; - - virtual image_handle load_image(image const &p_image); - virtual void unload_image(image_handle const p_image_handle); - - virtual void draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2); - - virtual void swap_buffers(); - - -private: - egl_platform m_egl_platform; - - typedef std::map < GLuint, texture > textures; - textures m_textures; - - bool m_is_valid; - - GLuint m_vertex_shader, m_fragment_shader, m_program; - GLuint m_vertex_buffer; - GLint m_tex_uloc, m_frame_rect_uloc; - GLint m_position_aloc, m_texcoords_aloc; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/eglgles/opengl.hpp b/src/eglgles/opengl.hpp deleted file mode 100644 index d7c102f..0000000 --- a/src/eglgles/opengl.hpp +++ /dev/null @@ -1,23 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_OPENGL_HPP -#define EASYSPLASH_OPENGL_HPP - - -#if !defined(GL_GLEXT_PROTOTYPES) -# define GL_GLEXT_PROTOTYPES -#endif - -#include -#include -#include -#include - - -#endif diff --git a/src/eglgles/texture.cpp b/src/eglgles/texture.cpp deleted file mode 100644 index 3f00498..0000000 --- a/src/eglgles/texture.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include "texture.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -texture::texture() - : m_width(0) - , m_height(0) - , m_name(0) -{ -} - - -texture::texture(GLsizei const p_width, GLsizei const p_height, GLint const p_internal_format, GLenum const p_format, GLenum const p_type) - : m_width(p_width) - , m_height(p_height) - , m_internal_format(p_internal_format) -{ - glGenTextures(1, &m_name); - glBindTexture(target, m_name); - glTexImage2D(target, 0, p_internal_format, p_width, p_height, 0, p_format, p_type, 0); - glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(target, 0); -} - - -texture::texture(texture &&p_other) - : m_width(p_other.m_width) - , m_height(p_other.m_height) - , m_internal_format(p_other.m_internal_format) - , m_name(p_other.m_name) -{ - p_other.m_name = 0; -} - - -texture::~texture() -{ - glBindTexture(target, 0); - if (m_name != 0) - glDeleteTextures(1, &m_name); -} - - -texture& texture::operator = (texture &&p_other) -{ - m_width = p_other.m_width; - m_height = p_other.m_height; - m_internal_format = p_other.m_internal_format; - m_name = p_other.m_name; - p_other.m_name = 0; - - return *this; -} - - -GLsizei texture::get_width() const -{ - return m_width; -} - - -GLsizei texture::get_height() const -{ - return m_height; -} - - -GLuint texture::get_name() const -{ - return m_name; -} - - -bool is_valid(texture const &p_texture) -{ - return (p_texture.get_name() != 0); -} - - -texture create_texture_from(image const &p_image) -{ - if (!is_valid(p_image)) - { - LOG_MSG(error, "could not create texture from invalid image"); - return texture(); - } - - texture tex(p_image.m_width, p_image.m_height, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE); - if (!is_valid(tex)) - { - LOG_MSG(error, "newly created texture is invalid"); - return texture(); - } - - glBindTexture(texture::target, tex.get_name()); - glTexSubImage2D(texture::target, 0, 0, 0, p_image.m_width, p_image.m_height, GL_RGBA, GL_UNSIGNED_BYTE, &(p_image.m_pixels[0])); - glBindTexture(texture::target, 0); - - return tex; -} - - -} // namespace easysplash end diff --git a/src/eglgles/texture.hpp b/src/eglgles/texture.hpp deleted file mode 100644 index 9ceda19..0000000 --- a/src/eglgles/texture.hpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_TEXTURE_HPP -#define EASYSPLASH_TEXTURE_HPP - -#include "opengl.hpp" -#include "noncopyable.hpp" -#include "types.hpp" - - -namespace easysplash -{ - - -class texture: - private noncopyable -{ -public: - enum { target = GL_TEXTURE_2D }; - - texture(); - explicit texture(GLsizei const p_width, GLsizei const p_height, GLint const p_internal_format, GLenum const p_format, GLenum const p_type); - texture(texture &&p_other); - ~texture(); - - texture& operator = (texture &&p_other); - - GLsizei get_width() const; - GLsizei get_height() const; - GLuint get_name() const; - - -protected: - GLsizei m_width, m_height; - GLint m_internal_format; - GLuint m_name; -}; - - -bool is_valid(texture const &p_texture); -texture create_texture_from(image const &p_image); - - -} // namespace easysplash end - - -#endif diff --git a/src/event_loop.cpp b/src/event_loop.cpp deleted file mode 100644 index c4c2123..0000000 --- a/src/event_loop.cpp +++ /dev/null @@ -1,269 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "event_loop.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -namespace -{ - - -volatile sig_atomic_t sigint_fd = -1; -struct sigaction old_sigint_action; - -void sigint_handler(int) -{ - if (sigint_fd != -1) - write(sigint_fd, "1", 1); -} - - -} // unnamed namespace end - - -event_loop::event_loop(display &p_display, bool const p_non_realtime_mode) - : m_display(p_display) - , m_fifo_fd(-1) - , m_non_realtime_mode(p_non_realtime_mode) -{ - // Only one instance of this class may exist. Check the SIGINT FD to verify this. - assert(sigint_fd == -1); - - - // Set up the SIGINT handler - - sigaction(SIGINT, NULL, &old_sigint_action); - if (old_sigint_action.sa_handler != SIG_IGN) - { - m_sigint_fds[0] = m_sigint_fds[1] = -1; - if (pipe(m_sigint_fds) == -1) - { - LOG_MSG(error, "Could not create SIGINT pipe: " << std::strerror(errno)); - return; - } - sigint_fd = m_sigint_fds[1]; - - struct sigaction new_sigint_action; - new_sigint_action.sa_handler = sigint_handler; - new_sigint_action.sa_flags = 0; - sigfillset(&(new_sigint_action.sa_mask)); - - sigaction(SIGINT, &new_sigint_action, NULL); - } - - - // Create and open the FIFO - - remove(CTL_FIFO_PATH); // cleanup any leftover FIFO - if (mkfifo(CTL_FIFO_PATH, 0666) == -1) - { - LOG_MSG(error, "Could not create FIFO \"" << CTL_FIFO_PATH << "\": " << std::strerror(errno)); - return; - } - - // Using O_RDWR instead of O_RDONLY, since the latter will - // cause poll() to return POLLHUP all the time after one - // message was sent over the FIFO - // O_NONBLOCK is necessary for poll() to work properly - m_fifo_fd = open(CTL_FIFO_PATH, O_RDWR | O_NONBLOCK); -} - - -event_loop::~event_loop() -{ - if (m_fifo_fd != -1) - close(m_fifo_fd); - remove(CTL_FIFO_PATH); - - if (m_sigint_fds[0] != -1) - close(m_sigint_fds[0]); - if (m_sigint_fds[1] != -1) - close(m_sigint_fds[1]); - - if (sigint_fd != -1) - { - sigaction(SIGINT, &old_sigint_action, NULL); - sigint_fd = -1; - } -} - - -void event_loop::run(animation &p_animation) -{ - if (m_fifo_fd == -1) - return; - - animation_position anim_pos; - bool stop_loop_requested = false; - - // Calculate coordinates based on the output width specified - // by the desc.txt file in the animation. - // The coordinates display the frames in the center of the screen. - long dx1 = (m_display.get_width() - p_animation.get_output_width()) / 2; - long dy1 = (m_display.get_height() - p_animation.get_output_height()) / 2; - long dx2 = dx1 + p_animation.get_output_width() - 1; - long dy2 = dy1 + p_animation.get_output_height() - 1; - - // Setup pollfd structure to check for bytes to read from the FIFO fd - struct pollfd fds[2]; - std::memset(fds, 0, sizeof(fds)); - int num_poll_fds = 0; - - fds[0].fd = m_fifo_fd; - fds[0].events = POLLIN; - ++num_poll_fds; - - if (sigint_fd != -1) - { - fds[1].fd = m_sigint_fds[0]; - fds[1].events = POLLIN; - ++num_poll_fds; - } - - // Defines how many milliseconds poll() shall wait for bytes to read. - // Initially, this value is 0, causing poll() to return immediately. - // This is desirable, since then, it will always paint the first frame - // immediately, meaning the first frame will appear as soon as the - // event loop starts. In non-realtime mode, however, set this to -1, - // since then, no timeouts shall occur (animation advances only when - // requested by a FIFO message). - int wait_period = m_non_realtime_mode ? -1 : 0; - - // Define period of one frame - std::chrono::steady_clock::duration const frame_dur = std::chrono::nanoseconds(1000000000) / p_animation.get_fps(); - - // Frame drawing function - auto draw_frame = [&](){ - display::image_handle cur_image_handle = p_animation.get_image_handle_at(anim_pos); - if (cur_image_handle != display::invalid_image_handle) - { - m_display.draw_image(cur_image_handle, dx1, dy1, dx2, dy2); - m_display.swap_buffers(); - } - }; - - unsigned int old_abs_frame_pos = 0; - - while (true) - { - // Check if the loop should stop. Stop if a stop was requested, - // and either if anim_pos' part nr is the last one, or the part's - // m_play_until_complete flag is false, or non-realtime mode is - // requested. - // Reason: part's with m_play_until_complete set to true must be - // played until the end. But for the last part, it would mean - // the animation could never be stopped, since the last part runs - // in an infinite loop. Also, in non-realtime mode, the - // m_play_until_complete flag makes no sense. - if (stop_loop_requested && (m_non_realtime_mode || (anim_pos.m_part_nr >= (p_animation.get_num_parts() - 1)) || !p_animation.get_part(anim_pos.m_part_nr)->m_play_until_complete)) - break; - - int poll_ret = poll(fds, num_poll_fds, wait_period); - - if (poll_ret == 0) - { - // poll() timed out. Draw the next frame. - - // Draw frame & swap buffers to display the frame, - // and measure the time that passed for each frame drawing - std::chrono::steady_clock::time_point first_time_point = std::chrono::steady_clock::now(); - draw_frame(); - std::chrono::steady_clock::time_point second_time_point = std::chrono::steady_clock::now(); - - // Calculate waiting period. It should equal the period for one frame - // stored in frame_dur. Since the time spent drawing the frame is nonzero, - // simply subtract that time from the frame_dur period, thus ensuring that - // the frames are drawn in frame_dur intervals. - std::chrono::steady_clock::duration dur = second_time_point - first_time_point; - wait_period = std::chrono::duration_cast < std::chrono::milliseconds > (frame_dur - dur).count(); - - // Special case - something caused a slowdown while drawing (usually - // happens the first time it is called), so the time spent with - // drawing was *larger* than frame_dur. Simply set wait_period to 0 - // in that case. It is not expected to occur often. - if (wait_period < 0) - wait_period = 0; - - // Move frame position forward by one frame. - update_position(anim_pos, p_animation, 1); - } - else if ((fds[0].revents & POLLIN) != 0) - { - // poll() reported there are bytes to read. Do so. - - std::uint8_t progress; - int read_ret = read(m_fifo_fd, &progress, sizeof(progress)); - if (read_ret == 0) - { - continue; - } - else if (read_ret == -1) - { - if (errno == EAGAIN) - continue; - else - { - LOG_MSG(error, "could not read from FIFO fd: " << std::strerror(errno)); - break; - } - } - - if (progress > 100) // More than 100% are not possible - progress = 100; - - if (m_non_realtime_mode) - { - // In non-realtime mode, move to the frame that corresponds - // to the percentage defined by "progress". Do so by calculating - // by how many frames the animation advances. - - unsigned int abs_frame_pos = progress * (p_animation.get_total_num_frames() - 1) / 100; - if (abs_frame_pos > old_abs_frame_pos) - { - // Only handle forward advances (backwards - // animations are not supported). - update_position(anim_pos, p_animation, abs_frame_pos - old_abs_frame_pos); - old_abs_frame_pos = abs_frame_pos; - draw_frame(); - } - } - - if (progress == 100) - stop_loop_requested = true; - } - else if ((fds[1].revents & POLLIN) != 0) - { - // sigint FD got a message -> quit - LOG_MSG(info, "SIGINT received - stopping event loop"); - break; - } - } -} - - -} // namespace easysplash end diff --git a/src/event_loop.hpp b/src/event_loop.hpp deleted file mode 100644 index d972a77..0000000 --- a/src/event_loop.hpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_EVENT_LOOP_HPP -#define EASYSPLASH_EVENT_LOOP_HPP - -#include "display.hpp" -#include "animation.hpp" - - -namespace easysplash -{ - - -/* event loop: - * - * This runs the main event loop that listens to the control FIFO, - * and updates animations on screen. - */ -class event_loop -{ -public: - event_loop(display &p_display, bool const p_non_realtime_mode); - ~event_loop(); - - void run(animation &p_animation); - -private: - display &m_display; - int m_fifo_fd, m_sigint_fds[2]; - bool m_non_realtime_mode; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/g2d/g2d_display.cpp b/src/g2d/g2d_display.cpp deleted file mode 100644 index 62f3434..0000000 --- a/src/g2d/g2d_display.cpp +++ /dev/null @@ -1,256 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include "g2d_display.hpp" -#include "linux_framebuffer.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -#define ALIGN_VAL_TO(LENGTH, ALIGN_SIZE) ( ((guintptr)((LENGTH) + (ALIGN_SIZE) - 1) / (ALIGN_SIZE)) * (ALIGN_SIZE) ) - - -g2d_display::g2d_image::g2d_image::g2d_image() - : m_g2d_buffer(nullptr) -{ -} - - -g2d_display::g2d_image::g2d_image(g2d_image && p_other) - : m_g2d_surface(p_other.m_g2d_surface) - , m_g2d_buffer(p_other.m_g2d_buffer) -{ - p_other.m_g2d_buffer = nullptr; -} - - -g2d_display::g2d_image::~g2d_image() -{ - if (m_g2d_buffer != nullptr) - { - LOG_MSG(debug, "Shutting down G2D image"); - g2d_free(m_g2d_buffer); - } -} - - -g2d_display::g2d_image& g2d_display::g2d_image::operator = (g2d_image && p_other) -{ - m_g2d_surface = p_other.m_g2d_surface; - m_g2d_buffer = p_other.m_g2d_buffer; - p_other.m_g2d_buffer = nullptr; - return *this; -} - - -bool g2d_display::g2d_image::load_image(image const &p_image) -{ - // Need to align the stride to 16-byte boundaries for G2D - unsigned long stride = ((p_image.m_stride + 15) / 16) * 16; - m_g2d_buffer = g2d_alloc(stride * p_image.m_height, 0); - - if (m_g2d_buffer == nullptr) - { - LOG_MSG(error, "could not allocate buffer for G2D surface"); - return false; - } - - for (unsigned long y = 0; y < p_image.m_height; ++y) - { - std::uint8_t const * srcrow = &(p_image.m_pixels[0]) + y * p_image.m_stride; - std::uint8_t* destrow = reinterpret_cast < std::uint8_t* > (m_g2d_buffer->buf_vaddr) + y * stride; - std::memcpy(destrow, srcrow, p_image.m_width * 4); // *4, since image is always 32-bit RGBA - } - - m_g2d_surface.format = G2D_BGRA8888; - m_g2d_surface.planes[0] = m_g2d_buffer->buf_paddr; - m_g2d_surface.left = 0; - m_g2d_surface.top = 0; - m_g2d_surface.right = p_image.m_width; - m_g2d_surface.bottom = p_image.m_height; - m_g2d_surface.stride = stride / 4; // /4, since for some odd reason, stride is in pixels with G2D surfaces, and not in bytes - m_g2d_surface.width = p_image.m_width; - m_g2d_surface.height = p_image.m_height; - m_g2d_surface.blendfunc = G2D_ONE; - m_g2d_surface.global_alpha = 255; - m_g2d_surface.clrcolor = 0x00000000; - m_g2d_surface.rot = G2D_ROTATION_0; - - return true; -} - - -bool g2d_display::g2d_image::operator < (g2d_image const &p_other) const -{ - // The actual value of the buffer pointer is not relevant. The pointer - // is just used as a key, since it is easy to compare, and uniquely - // identifies the image (because there is exactly one buffer for each - // G2D image). - return m_g2d_buffer < p_other.m_g2d_buffer; -} - - - - -g2d_display::g2d_display() - : m_g2d_handle(nullptr) -{ - m_g2d_fb_surface.planes[0] = 0; - - if (!create_g2d_fb_image()) - return; - - if (g2d_open(&m_g2d_handle) != 0) - { - LOG_MSG(error, "opening g2d device failed"); - return; - } - - if (g2d_make_current(m_g2d_handle, G2D_HARDWARE_2D) != 0) - { - LOG_MSG(error, "g2d_make_current() failed"); - if (g2d_close(m_g2d_handle) != 0) - LOG_MSG(error, "closing g2d device failed"); - return; - } - - enable_framebuffer_cursor(false); -} - - -g2d_display::~g2d_display() -{ - enable_framebuffer_cursor(true); - - if (m_g2d_handle != nullptr) - { - if (g2d_close(m_g2d_handle) != 0) - LOG_MSG(error, "closing g2d device failed"); - } -} - - -bool g2d_display::is_valid() const -{ - return m_fbdev.is_valid() && (m_g2d_handle != nullptr) && (m_g2d_fb_surface.planes[0] != 0); -} - - -long g2d_display::get_width() const -{ - return m_fbdev.get_width(); -} - - -long g2d_display::get_height() const -{ - return m_fbdev.get_height(); -} - - -display::image_handle g2d_display::load_image(image const &p_image) -{ - g2d_image g2dimg; - - if (g2dimg.load_image(p_image) == false) - return display::invalid_image_handle; - - display::image_handle imghandle = display::image_handle(g2dimg.m_g2d_buffer); - - m_g2d_images.insert(std::move(g2dimg)); - - return imghandle; -} - - -void g2d_display::unload_image(image_handle const p_image_handle) -{ - g2d_image g2dimg; - g2dimg.m_g2d_buffer = reinterpret_cast < struct g2d_buf * > (p_image_handle); - auto iter = m_g2d_images.find(g2dimg); - g2dimg.m_g2d_buffer = nullptr; - - if (iter != m_g2d_images.end()) - m_g2d_images.erase(iter); -} - - -void g2d_display::draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2) -{ - m_g2d_fb_surface.left = p_x1; - m_g2d_fb_surface.top = p_y1; - m_g2d_fb_surface.right = p_x2 + 1; - m_g2d_fb_surface.bottom = p_y2 + 1; - - g2d_image g2dimg; - g2dimg.m_g2d_buffer = reinterpret_cast < struct g2d_buf * > (p_image_handle); - auto iter = m_g2d_images.find(g2dimg); - g2dimg.m_g2d_buffer = nullptr; - - if (iter == m_g2d_images.end()) - { - LOG_MSG(error, "could not find image for given handle"); - return; - } - - if (g2d_blit(m_g2d_handle, const_cast < g2d_surface * > (&(iter->m_g2d_surface)), &m_g2d_fb_surface) != 0) - LOG_MSG(error, "blitting failed"); - - if (g2d_finish(m_g2d_handle) != 0) - LOG_MSG(error, "finishing g2d device operations failed"); -} - - -void g2d_display::swap_buffers() -{ -} - - -bool g2d_display::create_g2d_fb_image() -{ - linux_framebuffer::format const & fbfmt = m_fbdev.get_format(); - - if ((fbfmt.m_num_rgba_bits[0] == 5) && (fbfmt.m_num_rgba_bits[1] == 6) && (fbfmt.m_num_rgba_bits[2] == 5)) - m_g2d_fb_surface.format = G2D_BGR565; - else if ((fbfmt.m_num_rgba_bits[0] == 8) && (fbfmt.m_num_rgba_bits[1] == 8) && (fbfmt.m_num_rgba_bits[2] == 8) && (fbfmt.m_channel_order == linux_framebuffer::channel_order_argb)) - { - if (fbfmt.m_num_rgba_bits[3] == 8) - m_g2d_fb_surface.format = G2D_RGBA8888; - else - m_g2d_fb_surface.format = G2D_RGBX8888; - } - else - { - LOG_MSG(error, "framebuffer format is not supported by G2D display"); - return false; - } - - m_g2d_fb_surface.planes[0] = int(m_fbdev.get_physical_address()); - m_g2d_fb_surface.left = 0; - m_g2d_fb_surface.top = 0; - m_g2d_fb_surface.right = m_fbdev.get_width(); - m_g2d_fb_surface.bottom = m_fbdev.get_height(); - // division is necessary, since for some reason, the - // G2D API expects stride in pixels, not bytes - m_g2d_fb_surface.stride = m_fbdev.get_stride() / m_fbdev.get_format().m_bytes_per_pixel; - m_g2d_fb_surface.width = m_fbdev.get_width(); - m_g2d_fb_surface.height = m_fbdev.get_height(); - m_g2d_fb_surface.blendfunc = G2D_ZERO; - m_g2d_fb_surface.global_alpha = 255; - m_g2d_fb_surface.clrcolor = 0xFF000000; - m_g2d_fb_surface.rot = G2D_ROTATION_0; - - return true; -} - - -} // namespace end diff --git a/src/g2d/g2d_display.hpp b/src/g2d/g2d_display.hpp deleted file mode 100644 index 0a66dde..0000000 --- a/src/g2d/g2d_display.hpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_G2D_DISPLAY_HPP -#define EASYSPLASH_G2D_DISPLAY_HPP - -#include -#include -#include -#include "display.hpp" -#include "noncopyable.hpp" -#include "linux_framebuffer.hpp" - - -namespace easysplash -{ - - -class g2d_display - : public display -{ -public: - struct g2d_image - : private noncopyable - { - struct g2d_surface m_g2d_surface; - struct g2d_buf *m_g2d_buffer; - - g2d_image(); - g2d_image(g2d_image && p_other); - ~g2d_image(); - - g2d_image& operator = (g2d_image && p_other); - - bool load_image(image const &p_image); - - bool operator < (g2d_image const &p_other) const; - }; - - - g2d_display(); - ~g2d_display(); - - virtual bool is_valid() const; - - virtual long get_width() const; - virtual long get_height() const; - - virtual image_handle load_image(image const &p_image); - virtual void unload_image(image_handle const p_image_handle); - - virtual void draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2); - - virtual void swap_buffers(); - - -private: - bool create_g2d_fb_image(); - - typedef std::set < g2d_image > g2d_images; - - void* m_g2d_handle; - g2d_images m_g2d_images; - - linux_framebuffer m_fbdev; - struct g2d_surface m_g2d_fb_surface; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/init/CMakeLists.txt b/src/init/CMakeLists.txt deleted file mode 100644 index b0ff9df..0000000 --- a/src/init/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# EasySplash - tool for animated splash screens -# Copyright (C) 2014-2016 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -if(WITH_SYSVINIT) - - configure_file(${CMAKE_SOURCE_DIR}/src/init/easysplash-start.init.cmake ${PROJECT_BINARY_DIR}/src/init/easysplash-start) - - install(PROGRAMS ${PROJECT_BINARY_DIR}/src/init/easysplash-start DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/init.d ) -endif() - -if(WITH_SYSTEMD) - - configure_file(${CMAKE_SOURCE_DIR}/src/init/easysplash-start.service.cmake ${PROJECT_BINARY_DIR}/src/init/easysplash-start.service) - configure_file(${CMAKE_SOURCE_DIR}/src/init/easysplash-quit.service.cmake ${PROJECT_BINARY_DIR}/src/init/easysplash-quit.service) - - install(FILES ${PROJECT_BINARY_DIR}/src/init/easysplash-start.service DESTINATION ${SYSTEMD_SYSTEM_UNIT_DIR} ) - install(FILES ${PROJECT_BINARY_DIR}/src/init/easysplash-quit.service DESTINATION ${SYSTEMD_SYSTEM_UNIT_DIR} ) -endif() diff --git a/src/init/easysplash-quit.service.cmake b/src/init/easysplash-quit.service.cmake deleted file mode 100644 index bebb4d0..0000000 --- a/src/init/easysplash-quit.service.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# EasySplash - tool for animated splash screens -# Copyright (C) 2014, 2015 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -[Unit] -Description=Terminate EasySplash Boot Screen -After=easysplash-start.service -DefaultDependencies=no - -[Service] -Type=oneshot -ExecStart=@CMAKE_INSTALL_PREFIX@@CMAKE_INSTALL_SBINDIR@/easysplashctl 100 --wait-until-finished -TimeoutSec=30 - -[Install] -WantedBy=multi-user.target diff --git a/src/init/easysplash-start.init.cmake b/src/init/easysplash-start.init.cmake deleted file mode 100755 index 9b7f187..0000000 --- a/src/init/easysplash-start.init.cmake +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# EasySplash - tool for animated splash screens -# Copyright (C) 2015 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -### BEGIN INIT INFO -# Provides: easysplash -# Required-Start: -# Required-Stop: -# Default-Start: S -# Default-Stop: -### END INIT INFO - -# Read configuration variable file if it is present -[ -r @CMAKE_INSTALL_SYSCONFDIR@/default/easysplash ] && . @CMAKE_INSTALL_SYSCONFDIR@/default/easysplash - -read CMDLINE < /proc/cmdline -for x in $CMDLINE; do - case $x in - easysplash=false) - echo "Boot splashscreen disabled" - exit 0 - ;; - esac -done - -@CMAKE_INSTALL_SBINDIR@/easysplash & diff --git a/src/init/easysplash-start.service.cmake b/src/init/easysplash-start.service.cmake deleted file mode 100644 index 7ab2d7f..0000000 --- a/src/init/easysplash-start.service.cmake +++ /dev/null @@ -1,20 +0,0 @@ -# EasySplash - tool for animated splash screens -# Copyright (C) 2014, 2015 O.S. Systems Software LTDA. -# -# This file is part of EasySplash. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT - -[Unit] -Description=Starts EasySplash Boot screen -Wants=systemd-vconsole-setup.service -After=systemd-vconsole-setup.service systemd-udev-trigger.service systemd-udevd.service -DefaultDependencies=no - -[Service] -EnvironmentFile=-@CMAKE_INSTALL_SYSCONFDIR@/default/easysplash -Type=notify -ExecStart=@CMAKE_INSTALL_PREFIX@@CMAKE_INSTALL_SBINDIR@/easysplash - -[Install] -WantedBy=sysinit.target diff --git a/src/linux_framebuffer.cpp b/src/linux_framebuffer.cpp deleted file mode 100644 index 84206a1..0000000 --- a/src/linux_framebuffer.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "linux_framebuffer.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -linux_framebuffer::linux_framebuffer(std::string const &p_device_name) - : m_fd(-1) - , m_pixels(NULL) - , m_buffer_length(0) - , m_width(0) - , m_height(0) - , m_stride(0) -{ - struct fb_var_screeninfo vinfo; - struct fb_fix_screeninfo finfo; - - if ((m_fd = open(p_device_name.c_str(), O_RDWR)) < 0) - { - LOG_MSG(error, "could not open " << p_device_name << ": " << std::strerror(errno)); - return; - } - - if (ioctl(m_fd, FBIOGET_FSCREENINFO, &finfo) == -1) - { - LOG_MSG(error, "could not open get fixed screen info: " << std::strerror(errno)); - return; - } - - if (ioctl(m_fd, FBIOGET_VSCREENINFO, &vinfo) == -1) - { - LOG_MSG(error, "could not open get variable screen info: " << std::strerror(errno)); - return; - } - - if (finfo.type != FB_TYPE_PACKED_PIXELS) - { - LOG_MSG(error, "non-packed pixel formats are not supported"); - return; - } - - if ((finfo.visual != FB_VISUAL_TRUECOLOR) || (vinfo.grayscale != 0)) - { - LOG_MSG(error, "non-truecolor pixel formats are not supported"); - return; - } - - if ((vinfo.red.msb_right != 0) || (vinfo.green.msb_right != 0) || (vinfo.blue.msb_right != 0)) - { - LOG_MSG(error, "only formats with MSBs on the left are supported"); - return; - } - - unsigned int rofs = vinfo.red.offset; - unsigned int gofs = vinfo.green.offset; - unsigned int bofs = vinfo.blue.offset; - unsigned int aofs = vinfo.transp.offset; - m_format.m_num_rgba_bits[0] = vinfo.red.length; - m_format.m_num_rgba_bits[1] = vinfo.green.length; - m_format.m_num_rgba_bits[2] = vinfo.blue.length; - m_format.m_num_rgba_bits[3] = vinfo.transp.length; - - m_format.m_bits_per_pixel = vinfo.bits_per_pixel; - - switch (vinfo.bits_per_pixel) - { - case 15: - m_format.m_bytes_per_pixel = 3; // 15 bpp formats use 1 bit as padding - break; - default: - m_format.m_bytes_per_pixel = vinfo.bits_per_pixel / 8; - } - - if (((aofs >= rofs) || (aofs == 0)) && (rofs >= gofs) && (gofs >= bofs)) - m_format.m_channel_order = channel_order_argb; - else if ((rofs >= gofs) && (gofs >= bofs) && (bofs >= aofs)) - m_format.m_channel_order = channel_order_rgba; - else - { - LOG_MSG(error, - "unknown channel configuration:" - " R/G/B/A offsets: " << rofs << "/" << gofs << "/" << bofs << "/" << aofs << - " R/G/B/A sizes: " << m_format.m_num_rgba_bits[0] << "/" << m_format.m_num_rgba_bits[1] << "/" << m_format.m_num_rgba_bits[2] << "/" << m_format.m_num_rgba_bits[3] - ); - } - - m_physical_address = imx_phys_addr_t(finfo.smem_start); - m_buffer_length = finfo.smem_len; - m_width = vinfo.xres; - m_height = vinfo.yres; - m_stride = m_width * m_format.m_bytes_per_pixel; // stride value is in bytes, not pixels - - void *pixels; - if ((pixels = mmap(NULL, m_buffer_length, PROT_WRITE, MAP_SHARED, m_fd, 0)) == MAP_FAILED) - { - LOG_MSG(error, "could not map the framebuffer: " << std::strerror(errno)); - m_pixels = NULL; - return; - } - else - m_pixels = reinterpret_cast < std::uint8_t* > (pixels); - - LOG_MSG(info, - "Linux framebuffer: size: " << m_width << "x" << m_height << " buffer length: " << m_buffer_length << " bytes per pixel: " << m_format.m_bytes_per_pixel << - " R/G/B/A offsets: " << rofs << "/" << gofs << "/" << bofs << "/" << aofs << - " R/G/B/A sizes: " << m_format.m_num_rgba_bits[0] << "/" << m_format.m_num_rgba_bits[1] << "/" << m_format.m_num_rgba_bits[2] << "/" << m_format.m_num_rgba_bits[3] - ); -} - - -linux_framebuffer::~linux_framebuffer() -{ - if (m_fd >= 0) - { - if (munmap(m_pixels, m_buffer_length) < 0) - LOG_MSG(error, "could not unmap the framebuffer: " << std::strerror(errno)); - close(m_fd); - } -} - - -bool linux_framebuffer::is_valid() const -{ - return (m_pixels != NULL) && (m_width != 0) && (m_height != 0) && (m_stride != 0); -} - - -std::uint8_t* linux_framebuffer::get_pixels() -{ - return m_pixels; -} - - -imx_phys_addr_t linux_framebuffer::get_physical_address() const -{ - return m_physical_address; -} - - -unsigned long linux_framebuffer::get_width() const -{ - return m_width; -} - - -unsigned long linux_framebuffer::get_height() const -{ - return m_height; -} - - -unsigned long linux_framebuffer::get_stride() const -{ - return m_stride; -} - - -linux_framebuffer::format const & linux_framebuffer::get_format() const -{ - return m_format; -} - - - -namespace -{ - -char const * fbcon_path = "/sys/class/graphics/fbcon/cursor_blink"; - -} // unnamed namespace end - - -void enable_framebuffer_cursor(bool const p_state) -{ - int fd = open(fbcon_path, O_WRONLY); - write(fd, p_state ? "1" : "0", 1); - close(fd); -} - - -bool is_framebuffer_cursor_enabled() -{ - char state; - int fd = open(fbcon_path, O_RDONLY); - if (fd == -1) - { - LOG_MSG(error, "could not open" << fbcon_path << ": " << std::strerror(errno)); - return false; - } - - read(fd, &state, 1); - close(fd); - - return (state == '1'); -} - - -} // namespace easysplash end diff --git a/src/linux_framebuffer.hpp b/src/linux_framebuffer.hpp deleted file mode 100644 index 47a85c8..0000000 --- a/src/linux_framebuffer.hpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_SWRENDER_LINUX_FRAMEBUFFER_HPP -#define EASYSPLASH_SWRENDER_LINUX_FRAMEBUFFER_HPP - -#include -#include -#include "noncopyable.hpp" -#include "types.hpp" - - -namespace easysplash -{ - - -/* linux_framebuffer class: - * - * Opens the framebuffer device and queries information about it. - * This class is useful for displays which are based on the framebuffer, like G2D. - * Only truecolor framebuffers are supported. If a framebuffer uses an unsupported - * format, is_valid() returns false. - * The framebuffer is also memory mapped. get_pixels() returns the mapped virtual - * address, get_physical_address() the framebuffer's physical address. Note that - * the framebuffer is mapped in write mode, so don't try to read from it. - */ -class linux_framebuffer - : private noncopyable -{ -public: - enum channel_order - { - channel_order_argb, - channel_order_rgba - }; - - struct format - { - channel_order m_channel_order; - unsigned int m_num_rgba_bits[4]; - unsigned int m_bits_per_pixel, m_bytes_per_pixel; - }; - - - explicit linux_framebuffer(std::string const &p_device_name = "/dev/fb0"); - ~linux_framebuffer(); - - bool is_valid() const; - - std::uint8_t* get_pixels(); - imx_phys_addr_t get_physical_address() const; - unsigned long get_width() const; - unsigned long get_height() const; - unsigned long get_stride() const; - format const & get_format() const; - - -private: - int m_fd; - std::uint8_t *m_pixels; - imx_phys_addr_t m_physical_address; - unsigned long m_buffer_length, m_width, m_height, m_stride; - format m_format; -}; - - -void enable_framebuffer_cursor(bool const p_state); -bool is_framebuffer_cursor_enabled(); - - -} // namespace easysplash end - - -#endif diff --git a/src/load_png.cpp b/src/load_png.cpp deleted file mode 100644 index b24fbe3..0000000 --- a/src/load_png.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include "load_png.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -namespace -{ - - -enum { png_signature_length = 8 }; - - -// The custom I/O functions below for libpng are written to read from a datablock instance - -struct data_read_state -{ - datablock const &m_data; - std::size_t m_cur_pos; - - data_read_state(datablock const &p_data, std::size_t p_cur_pos) - : m_data(p_data) - , m_cur_pos(p_cur_pos) - { - } -}; - - -void png_read_proc(png_structp png_ptr, png_bytep out_bytes, png_size_t byte_count_to_read) -{ - png_voidp io_ptr = png_get_io_ptr(png_ptr); - - if (io_ptr == NULL) - { - LOG_MSG(error, "io_ptr is NULL"); - return; - } - - data_read_state *read_state = reinterpret_cast < data_read_state* > (io_ptr); - - // Read either the specified amount of bytes, or whatever remains in the buffer to read - // (pick the smaller value) - std::size_t num_to_read = std::min(std::size_t(byte_count_to_read), std::size_t(read_state->m_data.size() - read_state->m_cur_pos)); - - std::memcpy(out_bytes, &(read_state->m_data[read_state->m_cur_pos]), num_to_read); - read_state->m_cur_pos += num_to_read; -} - - -void png_error_msg_proc(png_structp, png_const_charp error_msg) -{ - LOG_MSG(error, "libpng error: " << error_msg); -} - - -void png_warning_msg_proc(png_structp, png_const_charp warning_msg) -{ - LOG_MSG(warning, "libpng warning: " << warning_msg); -} - - -} // unnamed namespace end - - -image load_png(datablock const &p_png_data) -{ - if (p_png_data.empty()) - { - LOG_MSG(error, "no input PNG data"); - return image(); - } - - if (!png_check_sig((png_bytep)(&(p_png_data[0])), png_signature_length)) - { - LOG_MSG(error, "invalid PNG signature"); - return image(); - } - - png_structp png_ptr = NULL; - png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - - if(png_ptr == NULL) - { - LOG_MSG(error, "could not initialize png struct"); - return image(); - } - - png_infop info_ptr = NULL; - info_ptr = png_create_info_struct(png_ptr); - - if(info_ptr == NULL) - { - LOG_MSG(error, "could not initialize info struct"); - png_destroy_read_struct(&png_ptr, NULL, NULL); - return image(); - } - - data_read_state read_state(p_png_data, png_signature_length); - - png_set_error_fn(png_ptr, NULL, png_error_msg_proc, png_warning_msg_proc); - png_set_read_fn(png_ptr, &read_state, png_read_proc); - - // Inform libpng that we already checked for the signature - png_set_sig_bytes(png_ptr, png_signature_length); - - png_read_info(png_ptr, info_ptr); - - png_uint_32 width = 0; - png_uint_32 height = 0; - int bit_depth = 0; - int color_type = -1; - png_uint_32 retval = png_get_IHDR( - png_ptr, info_ptr, - &width, - &height, - &bit_depth, - &color_type, - NULL, NULL, NULL - ); - - if (retval != 1) - { - LOG_MSG(error, "could not get IHDR"); - return image(); - } - - // Make sure the color data is in RGB format - if ((color_type == PNG_COLOR_TYPE_GRAY) || (color_type == PNG_COLOR_TYPE_GRAY_ALPHA)) - { - png_set_gray_to_rgb(png_ptr); - color_type = PNG_COLOR_TYPE_RGB; - } - else if (color_type == PNG_COLOR_TYPE_PALETTE) - { - png_set_palette_to_rgb(png_ptr); - color_type = PNG_COLOR_TYPE_RGB; - } - - // Make sure the channels use 8 bit - if (bit_depth > 8) - png_set_strip_16(png_ptr); - - // Add an alpha channel if necessary, to ensure the data - // is always of RGBA format - if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) - png_set_tRNS_to_alpha(png_ptr); - else - png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); - - // Allocate a 32 bpp RGBA image - image img; - img.m_width = width; - img.m_height = height; - img.m_stride = width * 4; // stride is specified in bytes, and 32 bpp = 4 bytes per pixel - img.m_pixels.resize(img.m_stride * img.m_height); - - // Read the pixels into the image - std::vector < png_bytep > row_pointers(height); - for (std::size_t y = 0; y < height; ++y) - row_pointers[y] = &(img.m_pixels[img.m_stride * y]); - png_read_image(png_ptr, &(row_pointers[0])); - - // Cleanup - png_destroy_read_struct(&png_ptr, &info_ptr, 0); - - LOG_MSG(trace, "loaded PNG: " << width << "x" << height << " pixels"); - - return img; -} - - -} // namespace easysplash end diff --git a/src/load_png.hpp b/src/load_png.hpp deleted file mode 100644 index 62e2401..0000000 --- a/src/load_png.hpp +++ /dev/null @@ -1,25 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_LOAD_PNG_HPP -#define EASYSPLASH_LOAD_PNG_HPP - -#include "types.hpp" - - -namespace easysplash -{ - - -image load_png(datablock const &p_png_data); - - -} // namespace easysplash end - - -#endif diff --git a/src/log.cpp b/src/log.cpp deleted file mode 100644 index c8d2fa9..0000000 --- a/src/log.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include "log.hpp" - - -namespace easysplash -{ - - -namespace -{ - - -void stderr_logfunc(std::chrono::steady_clock::duration const p_timestamp, log_levels const p_log_level, char const *p_srcfile, int const p_srcline, char const *p_srcfunction, std::string const &p_message) -{ - static std::chrono::steady_clock::duration::rep cur_max = 1000000; - static int cur_num_digits = 6; - - auto ms = std::chrono::duration_cast < std::chrono::milliseconds > (p_timestamp).count(); - auto s = ms / 1000; - if (s >= cur_max) - { - cur_max *= 1000; - cur_num_digits += 3; - } - - std::cerr - << "[" << std::dec << std::setfill (' ') << std::setw(cur_num_digits) << s << "." << std::setfill ('0') << std::setw(3) << (ms % 1000) << std::setfill(' ') << "] " - << "[" << log_level_str(p_log_level) << "] " - << "[" << p_srcfile << ":" << p_srcline << " " << p_srcfunction << "] " - << " " << p_message << "\n"; -} - - -struct logger_internal -{ - logger_internal() - : m_logfunc(stderr_logfunc) - , m_min_log_level(log_level_info) - { - m_time_base = std::chrono::steady_clock::now(); - } - - static logger_internal& instance() - { - static logger_internal logger; - return logger; - } - - log_write_function m_logfunc; - log_levels m_min_log_level; - std::chrono::steady_clock::time_point m_time_base; -}; - - -} - - -char const * log_level_str(log_levels const p_log_level) -{ - switch (p_log_level) - { - case log_level_trace: return "trace"; - case log_level_debug: return "debug"; - case log_level_info: return "info"; - case log_level_warning: return "warning"; - case log_level_error: return "error"; - } - - return ""; -} - - -void set_stderr_output() -{ - logger_internal::instance().m_logfunc = stderr_logfunc; -} - - -void set_log_write_function(log_write_function const &p_function) -{ - logger_internal::instance().m_logfunc = p_function; -} - -void log_message(log_levels const p_log_level, char const *p_srcfile, int const p_srcline, char const *p_srcfunction, std::string const &p_message) -{ - assert(logger_internal::instance().m_logfunc); - logger_internal::instance().m_logfunc( - std::chrono::steady_clock::now() - logger_internal::instance().m_time_base, - p_log_level, - p_srcfile, p_srcline, p_srcfunction, - p_message - ); -} - - -void set_min_log_level(log_levels const p_min_log_level) -{ - logger_internal::instance().m_min_log_level = p_min_log_level; -} - - -log_levels get_min_log_level() -{ - return logger_internal::instance().m_min_log_level; -} - - -} // namespace easysplash end - diff --git a/src/log.hpp b/src/log.hpp deleted file mode 100644 index c54e210..0000000 --- a/src/log.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_LOG_HPP -#define EASYSPLASH_LOG_HPP - -#include -#include -#include -#include - - -namespace easysplash -{ - - -enum log_levels -{ - log_level_trace = 0, - log_level_debug, - log_level_info, - log_level_warning, - log_level_error -}; - - -char const * log_level_str(log_levels const p_log_level); - - -typedef std::function < void(std::chrono::steady_clock::duration const p_timestamp, log_levels const p_log_level, char const *p_srcfile, int const p_srcline, char const *p_srcfunction, std::string const &p_message) > log_write_function; - - -void set_stderr_output(); -void set_log_write_function(log_write_function const &p_function); -void log_message(log_levels const p_log_level, char const *p_srcfile, int const p_srcline, char const *p_srcfunction, std::string const &p_message); - -void set_min_log_level(log_levels const p_min_log_level); -log_levels get_min_log_level(); - - - -#define LOG_MSG(LEVEL, MSG) \ - do \ - { \ - if (( ::easysplash::log_level_##LEVEL) >= ::easysplash::get_min_log_level()) \ - { \ - std::stringstream log_msg_internal_sstr_813585712987; \ - log_msg_internal_sstr_813585712987 << MSG; \ - ::easysplash::log_message(::easysplash::log_level_##LEVEL, __FILE__, __LINE__, __func__, log_msg_internal_sstr_813585712987.str()); \ - } \ - } \ - while (false) - - -} // namespace easysplash end - - -#endif - diff --git a/src/lru_cache.hpp b/src/lru_cache.hpp deleted file mode 100644 index 237536f..0000000 --- a/src/lru_cache.hpp +++ /dev/null @@ -1,102 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASHLRU_CACHE_HPP -#define EASYSPLASHLRU_CACHE_HPP - -#include -#include -#include -#include - - -namespace easysplash -{ - - -template < typename Key, typename T > -class lru_cache -{ -public: - typedef std::function < void(Key const &p_key, T & p_value) > unload_function; - - explicit lru_cache(std::size_t const p_max_entries, unload_function const &p_unload_function = unload_function()) - : m_max_entries(p_max_entries) - , m_unload_function(p_unload_function) - { - assert(p_max_entries > 0); - } - - T* get_entry(Key const &p_key) - { - auto map_iter = m_entry_map.find(p_key); - if (map_iter == m_entry_map.end()) - return nullptr; - - auto &list_iter = map_iter->second; - - if (list_iter != m_entry_list.begin()) - { - m_entry_list.splice(m_entry_list.begin(), m_entry_list, list_iter); - list_iter = m_entry_list.begin(); - map_iter->second = list_iter; - } - - return &(list_iter->second); - } - - T& add_entry(Key const &p_key, T const & p_value) - { - m_entry_list.push_front(entry_list::value_type(p_key, p_value)); - m_entry_map[p_key] = m_entry_list.begin(); - trim_list(); - return m_entry_list.front()->second; - } - - T& add_entry(Key const &p_key, T && p_value) - { - m_entry_list.push_front(typename entry_list::value_type(p_key, std::move(p_value))); - m_entry_map[p_key] = m_entry_list.begin(); - trim_list(); - return m_entry_list.front().second; - } - -private: - void trim_list() - { - while (m_entry_list.size() > m_max_entries) - { - auto list_iter = m_entry_list.end(); - list_iter--; - - if (m_unload_function) - m_unload_function(list_iter->first, list_iter->second); - - auto map_iter = m_entry_map.find(list_iter->first); - if (map_iter != m_entry_map.end()) - m_entry_map.erase(map_iter); - - m_entry_list.erase(list_iter); - } - } - - typedef std::list < std::pair < Key, T > > entry_list; - typedef std::unordered_map < Key, typename entry_list::iterator > entry_map; - - std::size_t m_max_entries; - entry_list m_entry_list; - entry_map m_entry_map; - - unload_function m_unload_function; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index a34a7dd..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014, 2015 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include -#include -#include -#include -#include -#include "event_loop.hpp" -#include "animation.hpp" -#include "zip_archive.hpp" -#include "log.hpp" - -#if defined(WITH_SYSTEMD) -#include -#endif - -#if defined(DISPLAY_TYPE_SWRENDER) -#include "swrender/swrender_display.hpp" -#elif defined(DISPLAY_TYPE_G2D) -#include "g2d/g2d_display.hpp" -#elif defined(DISPLAY_TYPE_GLES) -#include "eglgles/gles_display.hpp" -#else -#error Unknown display type selected -#endif - -#define REAL_INIT "/sbin/init" - -namespace -{ - - -void print_usage(char const *p_program_name) -{ - char const *help_text = - " -h --help display this usage information and exit\n" - " -i --zipfile zipfile with animation to play\n" - " -v --loglevel minimum levels messages must have to be logged\n" - " -n --non-realtime Run in non-realtime mode (animation advances depending\n" - " on the percentage value from the ctl application)\n" - ; - - std::cout << "Usage: " << p_program_name << " [OPTION]...\n"; - std::cout << help_text; -} - - -} // unnamed namespace end - - -int main(int argc, char *argv[]) -{ - bool non_realtime = false; - char const * const short_options = "hi:l:v:n"; - struct option const long_options[] = { - { "help", 0, NULL, 'h' }, - { "zipfile", 1, NULL, 'i' }, - { "loglevel", 1, NULL, 'v' }, - { "non-realtime", 0, NULL, 'n' }, - { NULL, 0, NULL, 0 } - }; - int next_option; - std::string zipfilename, loglevel; - - while (true) - { - next_option = getopt_long(argc, argv, short_options, long_options, NULL); - - if (next_option == -1) - break; - - switch (next_option) - { - case 'h': - print_usage(argv[0]); - return 0; - - case 'i': - zipfilename = optarg; - break; - - case 'v': - loglevel = optarg; - break; - - case 'n': - non_realtime = true; - break; - } - } - - - using namespace easysplash; - - - if (loglevel == "trace") - set_min_log_level(log_level_trace); - else if (loglevel == "debug") - set_min_log_level(log_level_debug); - else if (loglevel == "info") - set_min_log_level(log_level_info); - else if (loglevel == "warning") - set_min_log_level(log_level_trace); - else if (loglevel == "error") - set_min_log_level(log_level_error); - - if (getpid() == 1) - { - if (fork()) - { - char *argv0 = argv[0]; - /* first, change our argv[0], then exec */ - argv[0] = basename((char *)REAL_INIT); - execv(REAL_INIT, argv); - - argv[0] = argv0; - LOG_MSG(error, "failed to exec " << REAL_INIT); - exit(1); - } - } - - int pidfile_fd; - - { - int this_pid = getpid(); - if ((pidfile_fd = open(EASYSPLASH_PID_FILE, O_RDWR | O_CREAT, 0600)) == -1) - { - LOG_MSG(error, "could not open PID file: " << std::strerror(errno)); - return -1; - } - - if (lockf(pidfile_fd, F_TLOCK, 0) == -1) - { - LOG_MSG(error, "could not get a lock on the PID file: " << std::strerror(errno)); - return -1; - } - - std::string pid_str = std::to_string(this_pid); - write(pidfile_fd, pid_str.c_str(), pid_str.length()); - } - - { - -#if defined(DISPLAY_TYPE_SWRENDER) - swrender_display display; -#elif defined(DISPLAY_TYPE_G2D) - g2d_display display; -#elif defined(DISPLAY_TYPE_GLES) - gles_display display; -#endif - - if (!display.is_valid()) - { - LOG_MSG(error, "initializing display failed"); - return -1; - } - - if (zipfilename.empty()) - { - std::queue zip_queue; - - zip_queue.push(std::string("/lib/easysplash/oem/") + std::to_string(display.get_width()) + ".zip"); - zip_queue.push("/lib/easysplash/oem/bootanimation.zip"); - zip_queue.push(std::string("/lib/easysplash/") + std::to_string(display.get_width()) + ".zip"); - zip_queue.push("/lib/easysplash/bootanimation.zip"); - - while (!zip_queue.empty()) - { - zipfilename = zip_queue.front(); - - std::ifstream f(zipfilename.c_str()); - if (f.good()) { - break; - } - - zip_queue.pop(); - } - } - - std::ifstream in_zip_stream(zipfilename.c_str(), std::ios::binary); - if (!in_zip_stream) - { - LOG_MSG(error, "could not load zip archive " << zipfilename); - return -1; - } - - event_loop evloop(display, non_realtime); - -#if defined(WITH_SYSTEMD) - sd_notify(0, "READY=1"); -#endif - - LOG_MSG(info, "loading animation from zip archive " << zipfilename); - zip_archive zip(in_zip_stream); - animation anim(zip, display); - if (!is_valid(anim)) - { - LOG_MSG(error, "could not load animation from zip archivei " << zipfilename); - return -1; - } - - evloop.run(anim); - } - - close(pidfile_fd); - unlink(EASYSPLASH_PID_FILE); - - return 0; -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1be27a4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +// EasySplash - tool for animated splash screens +// Copyright (C) 2020 O.S. Systems Software LTDA. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +fn main() -> Result<(), anyhow::Error> { + println!("Hello, world!"); + + Ok(()) +} diff --git a/src/noncopyable.hpp b/src/noncopyable.hpp deleted file mode 100644 index 470e629..0000000 --- a/src/noncopyable.hpp +++ /dev/null @@ -1,40 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_NONCOPYABLE_HPP -#define EASYSPLASH_NONCOPYABLE_HPP - - -namespace easysplash -{ - - -namespace noncopyable_ -{ - -class noncopyable -{ -protected: - noncopyable() {} - ~noncopyable() {} -private: - noncopyable(noncopyable const &); - noncopyable const & operator = (noncopyable const &); -}; - -} - - -typedef noncopyable_::noncopyable noncopyable; - - -} - - -#endif - diff --git a/src/scope_guard.hpp b/src/scope_guard.hpp deleted file mode 100644 index 054c8f0..0000000 --- a/src/scope_guard.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2017 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_SCOPE_GUARD_HPP -#define EASYSPLASH_SCOPE_GUARD_HPP - -#include - - -namespace easysplash -{ - - -namespace detail -{ - - -class scope_guard_impl -{ -public: - template < typename Func > - explicit scope_guard_impl(Func &&p_func) - : m_func(std::forward < Func > (p_func)) - , m_dismissed(false) - { - } - - ~scope_guard_impl() - { - if (!m_dismissed) - { - try - { - m_func(); - } - catch (...) - { - } - } - } - - scope_guard_impl(scope_guard_impl &&p_other) - : m_func(std::move(p_other.m_func)) - , m_dismissed(p_other.m_dismissed) - { - p_other.m_dismissed = true; - } - - void dismiss() const throw() - { - m_dismissed = true; - } - - -private: - scope_guard_impl(scope_guard_impl const &) = delete; - scope_guard_impl& operator = (scope_guard_impl const &) = delete; - - std::function < void() > m_func; - mutable bool m_dismissed; -}; - - -} // namespace detail end - - -typedef detail::scope_guard_impl scope_guard_type; - - -template < typename Func > -detail::scope_guard_impl make_scope_guard(Func &&p_func) -{ - return detail::scope_guard_impl(std::forward < Func > (p_func)); -} - - -} // namespace easysplash end - - -#endif diff --git a/src/swrender/swrender_display.cpp b/src/swrender/swrender_display.cpp deleted file mode 100644 index d849c16..0000000 --- a/src/swrender/swrender_display.cpp +++ /dev/null @@ -1,234 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include "swrender_display.hpp" -#include "linux_framebuffer.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -swrender_display::sw_image::sw_image() - : m_pixman_image(NULL) -{ -} - - -swrender_display::sw_image::sw_image(sw_image && p_other) - : m_pixman_image(p_other.m_pixman_image) -{ - std::swap(m_image, p_other.m_image); - p_other.m_pixman_image = NULL; -} - - -swrender_display::sw_image::~sw_image() -{ - if (m_pixman_image != NULL) - { - LOG_MSG(debug, "Shutting down Pixman display"); - pixman_image_unref(m_pixman_image); - } -} - - -swrender_display::sw_image& swrender_display::sw_image::operator = (sw_image && p_other) -{ - std::swap(m_image, p_other.m_image); - m_pixman_image = p_other.m_pixman_image; - p_other.m_pixman_image = NULL; - - return *this; -} - - -bool swrender_display::sw_image::load_image(image && p_image) -{ - m_image = std::unique_ptr < image > (new image(std::move(p_image))); - if (!easysplash::is_valid(*m_image)) - { - LOG_MSG(error, "could not create pixman image: input image is invalid"); - return false; - } - - m_pixman_image = pixman_image_create_bits( - PIXMAN_a8b8g8r8, - p_image.m_width, - p_image.m_height, - reinterpret_cast < uint32_t* > (&(m_image->m_pixels[0])), - p_image.m_stride - ); - - if (m_pixman_image == NULL) - { - LOG_MSG(error, "could not create pixman image"); - return false; - } - - pixman_image_set_filter(m_pixman_image, PIXMAN_FILTER_BILINEAR, NULL, 0); - - return true; -} - - -bool swrender_display::sw_image::operator < (sw_image const &p_other) const -{ - // Using pixman image pointers as keys, since they are easy to compare, - // and uniquely identify the image (because there is exactly one pixman - // image for each sw_image). - return m_pixman_image < p_other.m_pixman_image; -} - - - - -swrender_display::swrender_display() - : m_pixman_fb_image(NULL) -{ - enable_framebuffer_cursor(false); - create_pixman_fb_image(); -} - - -swrender_display::~swrender_display() -{ - enable_framebuffer_cursor(true); - if (m_pixman_fb_image != NULL) - pixman_image_unref(m_pixman_fb_image); -} - - -bool swrender_display::is_valid() const -{ - return m_fbdev.is_valid() && (m_pixman_fb_image != NULL); -} - - -long swrender_display::get_width() const -{ - return m_fbdev.get_width(); -} - - -long swrender_display::get_height() const -{ - return m_fbdev.get_height(); -} - - -display::image_handle swrender_display::load_image(image &&p_image) -{ - sw_image swimg; - - if (swimg.load_image(std::move(p_image)) == false) - return display::invalid_image_handle; - - display::image_handle imghandle = display::image_handle(swimg.m_pixman_image); - - m_pixman_images.insert(std::move(swimg)); - - return imghandle; -} - - -display::image_handle swrender_display::load_image(image const &p_image) -{ - image temp_image(p_image); - return load_image(std::move(temp_image)); -} - - -void swrender_display::unload_image(image_handle const p_image_handle) -{ - sw_image swimg; - swimg.m_pixman_image = reinterpret_cast < pixman_image_t* > (p_image_handle); - auto iter = m_pixman_images.find(swimg); - swimg.m_pixman_image = nullptr; - - if (iter != m_pixman_images.end()) - m_pixman_images.erase(iter); -} - - -void swrender_display::draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2) -{ - pixman_image_t *srcimg = reinterpret_cast < pixman_image_t* > (p_image_handle); - - long draw_w = p_x2 - p_x1 + 1; - long draw_h = p_y2 - p_y1 + 1; - - // Blit the image with pixman, with bilinear scaling - // The scaling factors are calculated to make sure the scaled image fits - // with the size of the drawing area - - pixman_fixed_t scale_x = pixman_image_get_width(srcimg) * pixman_int_to_fixed(1) / draw_w; - pixman_fixed_t scale_y = pixman_image_get_height(srcimg) * pixman_int_to_fixed(1) / draw_h; - - pixman_transform_t transform; - pixman_transform_init_scale(&transform, scale_x, scale_y); - pixman_image_set_transform(srcimg, &transform); - - pixman_image_composite( - PIXMAN_OP_SRC, - srcimg, - NULL, - m_pixman_fb_image, - 0, 0, - 0, 0, - p_x1, p_y1, - draw_w, draw_h - ); -} - - -void swrender_display::swap_buffers() -{ -} - - -bool swrender_display::create_pixman_fb_image() -{ - int type; - linux_framebuffer::format const & fbfmt = m_fbdev.get_format(); - - switch (fbfmt.m_channel_order) - { - case linux_framebuffer::channel_order_argb: type = PIXMAN_TYPE_ARGB; break; - case linux_framebuffer::channel_order_rgba: type = PIXMAN_TYPE_RGBA; break; - } - - pixman_format_code_t pxfmt = pixman_format_code_t(PIXMAN_FORMAT( - fbfmt.m_bits_per_pixel, - type, - fbfmt.m_num_rgba_bits[3], - fbfmt.m_num_rgba_bits[0], - fbfmt.m_num_rgba_bits[1], - fbfmt.m_num_rgba_bits[2] - )); - - m_pixman_fb_image = pixman_image_create_bits( - pxfmt, - m_fbdev.get_width(), - m_fbdev.get_height(), - reinterpret_cast < uint32_t* > (m_fbdev.get_pixels()), - m_fbdev.get_stride() - ); - - if (m_pixman_fb_image == NULL) - { - LOG_MSG(error, "could not create pixman framebuffer image"); - return false; - } - else - return true; -} - - -} // namespace easysplash end diff --git a/src/swrender/swrender_display.hpp b/src/swrender/swrender_display.hpp deleted file mode 100644 index c6682c1..0000000 --- a/src/swrender/swrender_display.hpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_SWRENDER_DISPLAY_HPP -#define EASYSPLASH_SWRENDER_DISPLAY_HPP - -#include -#include -#include -#include "display.hpp" -#include "noncopyable.hpp" -#include "linux_framebuffer.hpp" - - -namespace easysplash -{ - - -class swrender_display - : public display -{ -public: - struct sw_image - : private noncopyable - { - pixman_image_t *m_pixman_image; - std::unique_ptr < image > m_image; - - sw_image(); - sw_image(sw_image && p_other); - ~sw_image(); - - sw_image& operator = (sw_image && p_other); - - bool load_image(image && p_image); - - bool operator < (sw_image const &p_other) const; - }; - - - swrender_display(); - ~swrender_display(); - - virtual bool is_valid() const; - - virtual long get_width() const; - virtual long get_height() const; - - virtual image_handle load_image(image &&p_image); - virtual image_handle load_image(image const &p_image); - virtual void unload_image(image_handle const p_image_handle); - - virtual void draw_image(image_handle const p_image_handle, long const p_x1, long const p_y1, long const p_x2, long const p_y2); - - virtual void swap_buffers(); - - -private: - bool create_pixman_fb_image(); - - typedef std::set < sw_image > pixman_images; - pixman_images m_pixman_images; - - linux_framebuffer m_fbdev; - pixman_image_t *m_pixman_fb_image; -}; - - -} // namespace easysplash end - - -#endif diff --git a/src/types.cpp b/src/types.cpp deleted file mode 100644 index 49675fe..0000000 --- a/src/types.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include "types.hpp" - - -namespace easysplash -{ - - -datablock_buf::datablock_buf(datablock const &p_datablock) -{ - char *p = (char *)(&(p_datablock[0])); - setg(p, p, p + p_datablock.size()); -} - - -image::image() - : m_width(0) - , m_height(0) - , m_stride(0) -{ -} - - -image::image(image &&p_other) - : m_pixels(std::move(p_other.m_pixels)) - , m_width(p_other.m_width) - , m_height(p_other.m_height) - , m_stride(p_other.m_stride) -{ -} - - -image::image(image const &p_other) - : m_pixels(p_other.m_pixels) - , m_width(p_other.m_width) - , m_height(p_other.m_height) - , m_stride(p_other.m_stride) -{ -} - - -image& image::operator = (image &&p_other) -{ - std::swap(m_pixels, p_other.m_pixels); - m_width = p_other.m_width; - m_height = p_other.m_height; - m_stride = p_other.m_stride; - return *this; -} - - -image& image::operator = (image const &p_other) -{ - m_pixels = p_other.m_pixels; - m_width = p_other.m_width; - m_height = p_other.m_height; - m_stride = p_other.m_stride; - return *this; -} - - -bool is_valid(image const &p_image) -{ - return (p_image.m_width != 0) && (p_image.m_height != 0) && (p_image.m_stride != 0) && !(p_image.m_pixels.empty()); -} - - -} // namespace easysplash end diff --git a/src/types.hpp b/src/types.hpp deleted file mode 100644 index 97baae9..0000000 --- a/src/types.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_TYPES_HPP -#define EASYSPLASH_TYPES_HPP - -#include -#include -#include - - -namespace easysplash -{ - - -typedef unsigned long imx_phys_addr_t; - - -typedef std::vector < std::uint8_t > datablock; - - -// Helper class to operate with iostreams inside a datablock -class datablock_buf - : public std::streambuf -{ -public: - explicit datablock_buf(datablock const &p_datablock); -}; - - -/* image: - * - * This is a simple structure for holding image data. It consists - * of a datablock with the actual pixels, width & height values, - * and a stride value. The stride value specifies how many bytes each - * image row contains. The pixels are assumed to be 32-bit RGBA - * formatted:. - */ -struct image -{ - datablock m_pixels; - unsigned long m_width, m_height, m_stride; - - image(); - image(image &&p_other); - image(image const &p_other); - - image& operator = (image &&p_other); - image& operator = (image const &p_other); -}; - - -bool is_valid(image const &p_image); - - -} // namespace easysplash end - - -#endif diff --git a/src/zip_archive.cpp b/src/zip_archive.cpp deleted file mode 100644 index c052961..0000000 --- a/src/zip_archive.cpp +++ /dev/null @@ -1,323 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#include -#include -#include -#include -#include -#include "zip_archive.hpp" -#include "log.hpp" - - -namespace easysplash -{ - - -namespace -{ - - -std::string normalize_path(std::string const &p_path) -{ - // If the path starts with "./", remove this prefix - std::string normalized_path = p_path; - if (normalized_path.find("./") == 0) - normalized_path = normalized_path.substr(2); - return normalized_path; -} - - -template < typename T, class = typename std::enable_if < std::is_arithmetic < T > ::value > ::type > -std::istream& read_binary(std::istream &p_input, T &p_value) -{ - p_input.read(reinterpret_cast < char* >(&p_value), sizeof(T)); - return p_input; -} - - -bool inflate_data(datablock &p_compressed_data, datablock &p_uncompressed_data) -{ - z_stream stream; - int ret; - bool ok = true; - - memset(&stream, 0, sizeof(stream)); - stream.next_in = reinterpret_cast < Bytef* > (&(p_compressed_data[0])); - stream.avail_in = p_compressed_data.size(); - stream.next_out = reinterpret_cast < Bytef* > (&(p_uncompressed_data[0])); - stream.avail_out = p_uncompressed_data.size(); - - // -MAX_WBITS is necessary for deflated data in a zip - if (ok && ((ret = inflateInit2(&stream, -MAX_WBITS)) != Z_OK)) - { - LOG_MSG(error, "could not initialize zlib inflate: " << zError(ret)); - ok = false; - } - else - { - if (ok) - { - ret = inflate(&stream, Z_SYNC_FLUSH); - - if ((ret != Z_OK) && (ret != Z_STREAM_END)) - { - LOG_MSG(error, "could not inflate data: " << zError(ret)); - ok = false; - } - } - - // uninitialize even if inflate() failed to properly clean up zlib resources - if ((ret = inflateEnd(&stream) != Z_OK)) - { - LOG_MSG(error, "error while cleaning up zlib inflate: " << zError(ret)); - ok = false; - } - } - - return ok; -} - - -} // unnamed namespace end - - -zip_archive::zip_archive(std::istream &p_input) - : m_input(p_input) -{ - read_central_directory(); -} - - -zip_archive::entries const & zip_archive::get_entries() const -{ - return m_entries; -} - - -void zip_archive::find_entries_with_path(std::string const &p_path, entry_iter_callback const &p_callback) -{ - std::string path = normalize_path(p_path); - for (auto const &pair : m_entries) - { - entry const &p_entry = pair.second; - if (p_entry.m_filename.find(path) == 0) - { - LOG_MSG(trace, "entry filename \"" << p_entry.m_filename << "\" starts with path \"" << path << "\" - invoking callback"); - p_callback(p_entry); - } - else - LOG_MSG(trace, "entry filename \"" << p_entry.m_filename << "\" does not start with path \"" << path << "\" - skipping"); - } -} - - -datablock zip_archive::uncompress_data(entry const &p_entry) -{ - datablock uncompressed_data(p_entry.m_uncompressed_size); - - if (p_entry.m_offset_to_data == 0) - { - uint16_t local_filename_len, local_extra_field_len; - - // go to local header, and skip signature, required version, bit flag, compression - // method, last mod time & date, crc32, compressed & uncompressed size (all of this - // data is already present in the central directory) - m_input.seekg(p_entry.m_offset_to_local_header + 4 + 5*2 + 3*4, std::ios::beg); - - // skip local filename and extra fields - read_binary(m_input, local_filename_len); - read_binary(m_input, local_extra_field_len); - m_input.seekg(local_filename_len + local_extra_field_len, std::ios::cur); - - p_entry.m_offset_to_data = m_input.tellg(); - } - else - m_input.seekg(p_entry.m_offset_to_data, std::ios::beg); - - switch (p_entry.m_compression_method) - { - case 0: // no compression - { - m_input.read(reinterpret_cast < char* > (&uncompressed_data[0]), uncompressed_data.size()); - break; - } - - case 8: // deflate compression - { - datablock compressed_data(p_entry.m_compressed_size); - m_input.read(reinterpret_cast < char* > (&compressed_data[0]), compressed_data.size()); - - if (!inflate_data(compressed_data, uncompressed_data)) - { - LOG_MSG(error, "could not decompress data - returning empty datablock"); - return datablock(); - } - - break; - }; - - default: - // should not happen - compression methods other than 0 or 8 are filtered earlier - assert(0); - } - - return uncompressed_data; -} - - -void zip_archive::read_central_directory() -{ - std::streamoff end_of_central_dir_pos = find_end_of_central_directory(); - - if (end_of_central_dir_pos == -1) - { - LOG_MSG(error, "could not find end of central directory"); - return; - } - - // go to end of central directory, and skip: - // signature (4 byte), disk nr (2 byte), disk nr. with start of central dir (2 byte), - // number of entries on this disk (2 byte) - m_input.seekg(end_of_central_dir_pos + 4 + 3*2, std::ios::beg); - - std::uint16_t num_entries; - std::uint32_t central_dir_size; - std::uint32_t central_dir_pos; - - read_binary(m_input, num_entries); - read_binary(m_input, central_dir_size); - read_binary(m_input, central_dir_pos); - - scan_central_directory(num_entries, central_dir_size, central_dir_pos); -} - - -void zip_archive::scan_central_directory(std::uint16_t const p_num_entries, std::uint32_t const p_central_dir_size, std::uint32_t const p_central_dir_pos) -{ - m_input.seekg(p_central_dir_pos, std::ios::beg); - - for (std::uint16_t i = 0; i < p_num_entries; ++i) - { - entry new_entry; - std::uint16_t fn_length, extra_field_length, file_comment_length; - - m_input.seekg(4 + 3*2, std::ios::cur); // skipping signature, version, required version, bit flag - read_binary(m_input, new_entry.m_compression_method); - m_input.seekg(2*2, std::ios::cur); // skipping last mod file time & date - read_binary(m_input, new_entry.m_CRC32_checksum); - read_binary(m_input, new_entry.m_compressed_size); - read_binary(m_input, new_entry.m_uncompressed_size); - read_binary(m_input, fn_length); - read_binary(m_input, extra_field_length); - read_binary(m_input, file_comment_length); - m_input.seekg(2*2 + 4, std::ios::cur); // skipping disk nr. start, internal & external file attributes - read_binary(m_input, new_entry.m_offset_to_local_header); - - // copy the filename - new_entry.m_filename.resize(fn_length); - m_input.read(&(new_entry.m_filename[0]), new_entry.m_filename.size()); - new_entry.m_filename = normalize_path(new_entry.m_filename); - - // skip extra field & file comment so we are at the next file header in the central directory - m_input.seekg(extra_field_length + file_comment_length, std::ios::cur); - - // check compression method - // checking here, to ensure filename, comments etc have been read/skipped - // and the input position is at the right place for reading the next header - if ((new_entry.m_compression_method != 0) && (new_entry.m_compression_method != 8)) - { - LOG_MSG(warning, "zip entry \"" << new_entry.m_filename << "\" uses unsupported compression method " << new_entry.m_compression_method << " - skipping this entry"); - continue; - } - - // the offset is computed on-demand; 0 means "not computed yet" - new_entry.m_offset_to_data = 0; - - m_entries[new_entry.m_filename] = new_entry; - - // check if we are outside of the bounds of the central directory - assert((std::uint32_t(m_input.tellg()) - p_central_dir_pos) <= p_central_dir_size); - } -} - - -std::streamoff zip_archive::find_end_of_central_directory() -{ - m_input.seekg(0, std::ios::end); - std::streamsize filesize = m_input.tellg(); - m_input.seekg(0, std::ios::beg); - - if (filesize < 4) - { - LOG_MSG(error, "zip file is too small - must be at least 4 byte in size"); - return -1; - } - - std::uint32_t const end_of_central_dir_signature = 0x06054b50; - - std::streamoff max_back_read = 0xffff, backwards_pos = 4, found_pos = -1; - std::streamsize const max_read_size = 0x0400; - - uint8_t tempbuf[max_read_size]; - - if (max_back_read > filesize) - max_back_read = filesize; - - // Search backwards for the end-of-central directory signature. - // If the signature isnt found, go backwards by max_back_read bytes, - // and look again. Do so until the signature is found, or until the - // limit is reached (to prevent it from looking through the entire file). - while (true) - { - std::streamoff read_pos = filesize - backwards_pos; - std::streamsize read_size = std::min(std::streamsize(backwards_pos), max_read_size); - - m_input.seekg(read_pos, std::ios::beg); - m_input.read(reinterpret_cast < char* > (&(tempbuf[0])), read_size); - - for (std::streamoff i = read_size - 4; i >= 0; --i) - { - std::uint32_t test_sig = - (std::uint32_t(tempbuf[i + 3]) << 24) | - (std::uint32_t(tempbuf[i + 2]) << 16) | - (std::uint32_t(tempbuf[i + 1]) << 8) | - (std::uint32_t(tempbuf[i + 0]) << 0); - - if (test_sig == end_of_central_dir_signature) - { - found_pos = read_pos + i; - break; - } - } - - if (found_pos != -1) - break; - - if (backwards_pos == max_back_read) - break; - - backwards_pos += max_read_size; - if (backwards_pos > max_back_read) - backwards_pos = max_back_read; - } - - return found_pos; -} - - - - -zip_archive::entry const * find_entry_by_name(zip_archive const &p_archive, std::string const &p_name) -{ - auto iter = p_archive.get_entries().find(normalize_path(p_name)); - return (iter == p_archive.get_entries().end()) ? NULL : &(iter->second); -} - - -} // namespace easysplash end diff --git a/src/zip_archive.hpp b/src/zip_archive.hpp deleted file mode 100644 index e581549..0000000 --- a/src/zip_archive.hpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * EasySplash - tool for animated splash screens - * Copyright (C) 2014 O.S. Systems Software LTDA. - * - * SPDX-License-Identifier: Apache-2.0 OR MIT - */ - - -#ifndef EASYSPLASH_ZIP_ARCHIVE_HPP -#define EASYSPLASH_ZIP_ARCHIVE_HPP - -#include -#include -#include -#include -#include - -#include "noncopyable.hpp" -#include "types.hpp" - - -namespace easysplash -{ - - -class zip_archive - : private noncopyable -{ -public: - struct entry - { - std::string m_filename; - std::uint16_t m_compression_method; - std::uint32_t m_CRC32_checksum; - std::uint32_t m_compressed_size; - std::uint32_t m_uncompressed_size; - std::uint32_t m_offset_to_local_header; - mutable std::uint32_t m_offset_to_data; - }; - - // By using std::map instead of std::unordered_map for the entries, - // it is guaranteed they are sorted lexicographically by their filenames, - // since map sorts the keys in a strict weak order - typedef std::map < std::string, entry > entries; - typedef std::function < void(entry const &p_entry) > entry_iter_callback; - - - explicit zip_archive(std::istream &p_input); - - entries const & get_entries() const; - void find_entries_with_path(std::string const &p_path, entry_iter_callback const &p_callback); - - // rvalue references and return value optimization - // get rid of unnecessary datablock copies - datablock uncompress_data(entry const &p_entry); - - -private: - void read_central_directory(); - void scan_central_directory(std::uint16_t const p_num_entries, std::uint32_t const central_dir_size, std::uint32_t const central_dir_pos); - std::streamoff find_end_of_central_directory(); - - std::istream &m_input; - entries m_entries; -}; - - -zip_archive::entry const * find_entry_by_name(zip_archive const &p_archive, std::string const &p_name); - - -} // namespace easysplash end - - -#endif From 60c30dc40035eed14ec3199422b531bf19d2dafb Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Mon, 2 Mar 2020 23:49:44 -0300 Subject: [PATCH 2/6] animation: Add parser for the animation manifest A parser for the animation structure is included. It does the bare minimum but is capable of parse the provided manifest. Signed-off-by: Otavio Salvador --- Cargo.lock | 94 ++++++++++++++++++++++ Cargo.toml | 4 + src/animation.rs | 198 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 298 insertions(+) create mode 100644 src/animation.rs diff --git a/Cargo.lock b/Cargo.lock index e438556..78c823c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,9 +6,103 @@ version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "derive_more" +version = "0.99.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc655351f820d774679da6cdc23355a93de496867d8203496675162e17b1d671" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "easysplash" version = "1.90.0" dependencies = [ "anyhow", + "derive_more", + "log", + "serde", + "toml", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", ] + +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" diff --git a/Cargo.toml b/Cargo.toml index d4edb4a..b41906b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,7 @@ edition = "2018" [dependencies] anyhow = "1.0.26" +derive_more = { version = "0.99.5", default-features = false, features = ["display", "from", "error"] } +log = { version = "0.4.8", default-features = false } +serde = { version = "1.0.104", features = ["derive"] } +toml = "0.5.6" diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 0000000..b97dc20 --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,198 @@ +// EasySplash - tool for animated splash screens +// Copyright (C) 2020 O.S. Systems Software LTDA. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use derive_more::{Display, Error, From}; +use log::{debug, error, info, trace}; +use serde::Deserialize; +use std::{ + fs, io, + io::Read, + iter::Iterator, + path::{Path, PathBuf}, +}; + +#[derive(Display, From, Error, Debug)] +pub(crate) enum Error { + #[display(fmt = "Fail to read the manifest file. Cause: {}", _0)] + Io(io::Error), + + #[display(fmt = "Failed to parse the manifest file. Cause: {}", _0)] + TomlParser(toml::de::Error), + + #[display(fmt = "The animation part '{}' is missing.", "_0.display()")] + MissingPart(#[error(not(source))] PathBuf), +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub(crate) struct Animation { + #[serde(rename = "part")] + parts: Vec, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub(crate) struct Part { + file: PathBuf, + #[serde(default)] + mode: Mode, + #[serde(default)] + repeat: usize, +} + +impl Animation { + pub(crate) fn from_path(path: &Path) -> Result { + info!("loading manifest from {:?}", path); + + let mut buf = String::new(); + fs::File::open(path.join("animation.toml"))?.read_to_string(&mut buf)?; + + let mut animation = toml::from_str::(&buf)?; + + // We ensure we have the full path to the animation files, while we also + // ensure all files exists. + for part in &mut animation.parts { + part.file = fs::canonicalize(path)?.join(&part.file); + if !part.file.exists() { + error!("part {:?} is missing", part.file); + return Err(Error::MissingPart(part.file.to_path_buf())); + } + trace!("part {:?} was found", part.file); + } + + Ok(animation) + } +} + +pub(crate) struct AnimationIter<'a> { + inner: &'a Animation, + current_part: usize, + repeat: usize, +} + +impl<'a> IntoIterator for &'a Animation { + type IntoIter = AnimationIter<'a>; + type Item = &'a Part; + + fn into_iter(self) -> Self::IntoIter { + AnimationIter { inner: self, current_part: 0, repeat: 0 } + } +} + +// Provides an iterator which respects the number of times each part must be +// played. +impl<'a> Iterator for AnimationIter<'a> { + type Item = &'a Part; + + fn next(&mut self) -> Option { + // When we iterate the number of times which are required by the + // `current_part`, we move to the next. + let repeat = self.inner.parts.get(self.current_part)?.repeat; + if self.repeat > repeat { + self.current_part += 1; + self.repeat = 0; + } + + // Get the required part for returning it. + let part = self.inner.parts.get(self.current_part)?; + + if repeat > 0 { + debug!( + "iterator: part {:?} (current: {} / number of times: {})", + part.file, + self.repeat + 1, + repeat + 1 + ); + } else { + debug!("iterator: part {:?} (once)", part.file); + } + + // Account for the number of repetitions we need to do. + self.repeat += 1; + + Some(part) + } +} + +impl Part { + pub(crate) fn url(&self) -> String { + format!("file://{}", self.file.to_string_lossy()) + } +} + +#[derive(Debug, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub(crate) enum Mode { + Complete, + Interruptable, +} + +impl Default for Mode { + fn default() -> Self { + Mode::Complete + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn valid_toml() -> Animation { + let manifest_toml = toml::toml! { + [[part]] + file = "part1.mp4" + + [[part]] + file = "part2.mp4" + repeat = 1 + + [[part]] + file = "part3.mp4" + mode = "interruptable" + }; + + manifest_toml.try_into::().expect("Failed to parse TOML") + } + + #[test] + fn manifest_parse() { + assert_eq!( + valid_toml(), + Animation { + parts: vec![ + Part { file: "part1.mp4".into(), mode: Mode::Complete, repeat: 0 }, + Part { file: "part2.mp4".into(), mode: Mode::Complete, repeat: 1 }, + Part { file: "part3.mp4".into(), mode: Mode::Interruptable, repeat: 0 }, + ] + } + ); + } + + #[test] + fn iterator() { + let animation = Animation { parts: valid_toml().parts }; + let mut animation: AnimationIter = animation.into_iter(); + + assert_eq!( + animation.next(), + Some(&Part { file: "part1.mp4".into(), mode: Mode::Complete, repeat: 0 }) + ); + + assert_eq!( + animation.next(), + Some(&Part { file: "part2.mp4".into(), mode: Mode::Complete, repeat: 1 }) + ); + + assert_eq!( + animation.next(), + Some(&Part { file: "part2.mp4".into(), mode: Mode::Complete, repeat: 1 }) + ); + + assert_eq!( + animation.next(), + Some(&Part { file: "part3.mp4".into(), mode: Mode::Interruptable, repeat: 0 }) + ); + } +} diff --git a/src/main.rs b/src/main.rs index 1be27a4..312df62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +mod animation; + fn main() -> Result<(), anyhow::Error> { println!("Hello, world!"); From 8e8d68d7ab6388ea2a3a6fcab5c3006600178ec0 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Mon, 2 Mar 2020 23:54:32 -0300 Subject: [PATCH 3/6] data: Provide an animation example An animation example is now provided, as well as the respective script required to convert it to the valid archive. Signed-off-by: Otavio Salvador --- data/glowing-logo/animation.toml | 10 ++++++++++ data/glowing-logo/part1.mp4 | Bin 0 -> 11392 bytes data/glowing-logo/part2.mp4 | Bin 0 -> 5278 bytes data/ossystems-demo/animation.toml | 8 ++++++++ data/ossystems-demo/beginning.mp4 | Bin 0 -> 17404 bytes data/ossystems-demo/end.mp4 | Bin 0 -> 22992 bytes data/ossystems-demo/intermediate.mp4 | Bin 0 -> 131981 bytes 7 files changed, 18 insertions(+) create mode 100644 data/glowing-logo/animation.toml create mode 100644 data/glowing-logo/part1.mp4 create mode 100644 data/glowing-logo/part2.mp4 create mode 100644 data/ossystems-demo/animation.toml create mode 100644 data/ossystems-demo/beginning.mp4 create mode 100644 data/ossystems-demo/end.mp4 create mode 100644 data/ossystems-demo/intermediate.mp4 diff --git a/data/glowing-logo/animation.toml b/data/glowing-logo/animation.toml new file mode 100644 index 0000000..eb9a055 --- /dev/null +++ b/data/glowing-logo/animation.toml @@ -0,0 +1,10 @@ +[[part]] +file = "part1.mp4" + +[[part]] +file = "part2.mp4" + +[[part]] +file = "part2.mp4" +repeat = 100 +mode = "interruptable" diff --git a/data/glowing-logo/part1.mp4 b/data/glowing-logo/part1.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a3b1524e1097471f2344814fc62aa8f1d6069578 GIT binary patch literal 11392 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 P+X##C2#E9W^YQ&3qC}T` literal 0 HcmV?d00001 diff --git a/data/glowing-logo/part2.mp4 b/data/glowing-logo/part2.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0ce970a381c65b90aef69bcf965ecadc3b28928a GIT binary patch literal 5278 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 Cfd&Tv literal 0 HcmV?d00001 diff --git a/data/ossystems-demo/end.mp4 b/data/ossystems-demo/end.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..598d4b4fdd8cbb70a22dd91f68141fba296c366d GIT binary patch literal 22992 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

D^U-e> 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 z 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

Fr5D5 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 literal 0 HcmV?d00001 From 97a7a509d5f05e1077307ba5401fabcee177f152 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Mon, 9 Mar 2020 21:24:27 -0300 Subject: [PATCH 4/6] gstreamer: Add playback support This is currently supporting the playback but does not yet is capable of interrupting it. Signed-off-by: Otavio Salvador --- .github/workflows/code_style.yml | 2 + .github/workflows/linux.yml | 2 + Cargo.lock | 697 +++++++++++++++++++++++++++++++ Cargo.toml | 10 + src/gstreamer.rs | 92 ++++ src/main.rs | 49 ++- 6 files changed, 850 insertions(+), 2 deletions(-) create mode 100644 src/gstreamer.rs diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 7be226f..84b060e 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -10,6 +10,8 @@ jobs: clippy_check: runs-on: ubuntu-latest steps: + - name: Install Dependencies + run: sudo apt-get update; sudo apt-get install libgstreamer1.0-dev - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a4d89cf..8a57fe8 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -20,6 +20,8 @@ jobs: runs-on: ubuntu-latest steps: + - name: Install Dependencies + run: sudo apt-get update; sudo apt-get install libgstreamer1.0-dev - name: Checkout sources uses: actions/checkout@v2 - name: Install ${{ matrix.version }} diff --git a/Cargo.lock b/Cargo.lock index 78c823c..e57680e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,12 +6,145 @@ version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" +[[package]] +name = "argh" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1877e24cecacd700d469066e0160c4f8497cc5635367163f50c8beec820154" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e742194e0f43fc932bcb801708c2b279d3ec8f527e3acda05a6a9f342c5ef764" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ba68f4276a778591e36a0c348a269888f3a177c8d2054969389e3b59611ff5" + +[[package]] +name = "async-attributes" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-std" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d68a33ebc8b57800847d00787307f84a562224a14db069b0acefe4c2abbf5d" +dependencies = [ + "async-attributes", + "async-task", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-timer", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "smol", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blocking" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d17efb70ce4421e351d61aafd90c16a20fb5bfe339fcdc32a86816280e62ce0" +dependencies = [ + "futures-channel", + "futures-util", + "once_cell", + "parking", + "waker-fn", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "cache-padded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24508e28c677875c380c20f4d28124fab6f8ed4ef929a1397d7b1a31e92f1005" + +[[package]] +name = "cc" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" + [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "concurrent-queue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83c06aff61f2d899eb87c379df3cbf7876f14471dcab474e0b6dc90ab96c080" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + [[package]] name = "derive_more" version = "0.99.8" @@ -28,12 +161,245 @@ name = "easysplash" version = "1.90.0" dependencies = [ "anyhow", + "argh", + "async-std", "derive_more", + "gstreamer", "log", "serde", + "simple-logging", "toml", ] +[[package]] +name = "fastrand" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64b0126b293b050395b37b10489951590ed024c03d7df4f249d219c8ded7cbf" + +[[package]] +name = "futures-channel" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" + +[[package]] +name = "futures-task" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper", +] + +[[package]] +name = "futures-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "glib" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fb573a09841b6386ddf15fd4bc6655b4f5b106ca962f57ecaecde32a0061c0" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-sys", + "gobject-sys", + "lazy_static", + "libc", +] + +[[package]] +name = "glib-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95856f3802f446c05feffa5e24859fe6a183a7cb849c8449afc35c86b1e316e2" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d1a804f62034eccf370006ccaef3708a71c31d561fee88564abe71177553d9" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", +] + +[[package]] +name = "gstreamer" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8664a114cd6ec16bece783d5eee59496919915b1f6884400ba4a953274a163" +dependencies = [ + "bitflags", + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "lazy_static", + "libc", + "muldiv", + "num-rational", + "paste", +] + +[[package]] +name = "gstreamer-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d18da01b97d0ab5896acd5151e4c155acefd0e6c03c3dd24dd133ba054053db" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff57d6d215f7ca7eb35a9a64d656ba4d9d2bef114d741dc08048e75e2f5d418" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" + [[package]] name = "log" version = "0.4.8" @@ -43,6 +409,139 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "muldiv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + +[[package]] +name = "parking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bcaa58ee64f8e4a3d02f5d8e6ed0340eae28fed6fdabd984ad1776e3b43848a" + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "pin-project" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" + +[[package]] +name = "proc-macro-hack" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + [[package]] name = "proc-macro2" version = "1.0.18" @@ -61,6 +560,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "serde" version = "1.0.114" @@ -81,6 +598,56 @@ dependencies = [ "syn", ] +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smol" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620cbb3c6e34da57d3a248cda0cd01cd5848164dc062e764e65d06fe3ea7aed5" +dependencies = [ + "async-task", + "blocking", + "concurrent-queue", + "fastrand", + "futures-io", + "futures-util", + "libc", + "once_cell", + "scoped-tls", + "slab", + "socket2", + "wepoll-sys-stjepang", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "syn" version = "1.0.33" @@ -92,6 +659,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "toml" version = "0.5.6" @@ -101,8 +679,127 @@ dependencies = [ "serde", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "waker-fn" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9571542c2ce85ce642e6b58b3364da2fb53526360dfb7c211add4f5c23105ff7" + +[[package]] +name = "wasm-bindgen" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2dc4aa152834bc334f506c1a06b866416a8b6697d5c9f75b9a689c8486def0" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded84f06e0ed21499f6184df0e0cb3494727b0c5da89534e0fcc55c51d812101" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64487204d863f109eb77e8462189d111f27cb5712cc9fdb3461297a76963a2f6" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "838e423688dac18d73e31edce74ddfac468e37b1506ad163ffaf0a46f703ffe3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ba19973a58daf4db6f352eda73dc0e289493cd29fb2632eb172085b6521acd" + +[[package]] +name = "web-sys" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-sys-stjepang" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd319e971980166b53e17b1026812ad66c6b54063be879eb182342b55284694" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index b41906b..dfd061f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,17 @@ edition = "2018" [dependencies] anyhow = "1.0.26" +argh = "0.1.3" +async-std = { version = "1.5.0", features = ["attributes", "unstable"] } derive_more = { version = "0.99.5", default-features = false, features = ["display", "from", "error"] } +gst = { version = "0.15.3", package = "gstreamer", default-features = false } log = { version = "0.4.8", default-features = false } serde = { version = "1.0.104", features = ["derive"] } +simple-logging = "2.0.2" toml = "0.5.6" + +[profile.release] +opt-level = 'z' # Optimize for size. +lto = true +codegen-units = 1 +panic = 'abort' diff --git a/src/gstreamer.rs b/src/gstreamer.rs new file mode 100644 index 0000000..e04c8a0 --- /dev/null +++ b/src/gstreamer.rs @@ -0,0 +1,92 @@ +// EasySplash - tool for animated splash screens +// Copyright (C) 2020 O.S. Systems Software LTDA. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::animation::Animation; + +use async_std::{io, prelude::*, sync}; +use derive_more::{Display, Error, From}; +use gst::{prelude::*, MessageView}; +use log::{debug, error, trace}; + +#[derive(Display, From, Error, Debug)] +pub(crate) enum Error { + #[display(fmt = "No animation parts to play")] + NoAnimation, + + #[display(transparent)] + Io(io::Error), + + #[display(transparent)] + Bool(gst::glib::error::BoolError), + + #[display(transparent)] + Glib(gst::glib::error::Error), + + #[display(transparent)] + StateChange(gst::StateChangeError), + + #[display(transparent)] + ChannelReceiver(sync::RecvError), +} + +pub(crate) async fn play_animation(animation: Animation) -> Result<(), Error> { + gst::init()?; + debug!("Using {} as player", gst::version_string()); + + let playbin = gst::ElementFactory::make("playbin", None)?; + + // TODO: We are not yet handling the animation height and width properties. + + // The pipeline is feed by the `feed_pipeline` and the control messages are + // handled by the `handle_message` future. + // + // Any future which finishes, allow the flow to continue. + feed_pipeline(playbin.clone(), animation).await?; + + playbin.set_state(gst::State::Null)?; + + Ok(()) +} + +async fn feed_pipeline(playbin: gst::Element, animation: Animation) -> Result<(), Error> { + // Acquire the iterator so we can walk on the animation parts. + let mut parts = animation.into_iter(); + + // Current playing part. + let mut current_part = parts.next().ok_or(Error::NoAnimation)?; + + // Queue first animation part and ask GStreamer to start playing it. + playbin.set_property("uri", ¤t_part.url())?; + playbin.set_state(gst::State::Playing)?; + + // We need to wait for stream to start and then we can queue the next + // part. We do that so we have a gapless playback. + let bus = playbin.get_bus().expect("failed to get pipeline bus"); + let mut messages = bus.stream(); + while let Some(msg) = messages.next().await { + match msg.view() { + MessageView::Error(err) => { + error!("{}", err.get_error()); + break; + } + MessageView::Eos(_) => { + trace!("end of stream message recived, finishing"); + break; + } + MessageView::StreamStart(_) => { + // If we have more animation parts to play, queue the next. + if let Some(part) = parts.next() { + current_part = part; + + trace!("video has started, queuing next part"); + playbin.set_property("uri", ¤t_part.url())?; + } + } + _ => (), + }; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 312df62..473a5a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,54 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT mod animation; +mod gstreamer; -fn main() -> Result<(), anyhow::Error> { - println!("Hello, world!"); +use crate::animation::Animation; + +use argh::FromArgs; +use log::{info, LevelFilter}; +use std::path::PathBuf; + +/// EasySplash offers a convenient boot splash for Embedded Linux devices, +/// focusing of simplicity and easy to use. +#[derive(FromArgs)] +struct CmdLine { + #[argh(subcommand)] + inner: Commands, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +enum Commands { + Open(Open), +} + +/// open the render with the specific animation +#[derive(FromArgs)] +#[argh(subcommand, name = "open")] +struct Open { + /// path to load the animation + #[argh(positional)] + path: PathBuf, + + /// log level to use (default to 'info') + #[argh(option, default = "LevelFilter::Info")] + log: LevelFilter, +} + +#[async_std::main] +async fn main() -> Result<(), anyhow::Error> { + let cmdline: CmdLine = argh::from_env(); + + match cmdline.inner { + Commands::Open(render) => { + simple_logging::log_to_stderr(render.log); + + info!("starting EasySplash animation"); + + gstreamer::play_animation(Animation::from_path(&render.path)?).await?; + } + } Ok(()) } From 0190c88cf26d29f475bcf2951c0d3b841c63b6ed Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Mon, 22 Jun 2020 22:00:37 -0300 Subject: [PATCH 5/6] message: Allow interruption of animation during playback Signed-off-by: Otavio Salvador --- src/animation.rs | 4 ++ src/gstreamer.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 35 ++++++++++++++++- src/message.rs | 64 ++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/message.rs diff --git a/src/animation.rs b/src/animation.rs index b97dc20..fde176c 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -120,6 +120,10 @@ impl Part { pub(crate) fn url(&self) -> String { format!("file://{}", self.file.to_string_lossy()) } + + pub(crate) fn is_interruptable(&self) -> bool { + self.mode == Mode::Interruptable + } } #[derive(Debug, Deserialize, PartialEq, Clone)] diff --git a/src/gstreamer.rs b/src/gstreamer.rs index e04c8a0..0581c07 100644 --- a/src/gstreamer.rs +++ b/src/gstreamer.rs @@ -3,9 +3,15 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::animation::Animation; - -use async_std::{io, prelude::*, sync}; +use crate::{animation::Animation, message::Message}; +use async_std::{ + io, + os::unix::net::UnixListener, + prelude::*, + sync, + sync::{Arc, Mutex}, + task, +}; use derive_more::{Display, Error, From}; use gst::{prelude::*, MessageView}; use log::{debug, error, trace}; @@ -31,10 +37,20 @@ pub(crate) enum Error { ChannelReceiver(sync::RecvError), } -pub(crate) async fn play_animation(animation: Animation) -> Result<(), Error> { +enum PipelineStatus { + Continuous, + Interruptable, +} + +pub(crate) async fn play_animation( + animation: Animation, + socket: UnixListener, +) -> Result<(), Error> { gst::init()?; debug!("Using {} as player", gst::version_string()); + let (status_tx, status_rx) = sync::channel::(1); + let (message_tx, message_rx) = sync::channel::(1); let playbin = gst::ElementFactory::make("playbin", None)?; // TODO: We are not yet handling the animation height and width properties. @@ -43,14 +59,21 @@ pub(crate) async fn play_animation(animation: Animation) -> Result<(), Error> { // handled by the `handle_message` future. // // Any future which finishes, allow the flow to continue. - feed_pipeline(playbin.clone(), animation).await?; + feed_pipeline(status_tx, playbin.clone(), animation) + .race(handle_client_message(message_tx, socket)) + .race(handle_interrupt_message(status_rx, message_rx)) + .await?; playbin.set_state(gst::State::Null)?; Ok(()) } -async fn feed_pipeline(playbin: gst::Element, animation: Animation) -> Result<(), Error> { +async fn feed_pipeline( + tx: sync::Sender, + playbin: gst::Element, + animation: Animation, +) -> Result<(), Error> { // Acquire the iterator so we can walk on the animation parts. let mut parts = animation.into_iter(); @@ -76,6 +99,17 @@ async fn feed_pipeline(playbin: gst::Element, animation: Animation) -> Result<() break; } MessageView::StreamStart(_) => { + // Notify if current part is interruptable. + let status = if current_part.is_interruptable() { + debug!("animation part is interruptable"); + PipelineStatus::Interruptable + } else { + debug!("animation part is intended to be played completely"); + PipelineStatus::Continuous + }; + + tx.send(status).await; + // If we have more animation parts to play, queue the next. if let Some(part) = parts.next() { current_part = part; @@ -90,3 +124,54 @@ async fn feed_pipeline(playbin: gst::Element, animation: Animation) -> Result<() Ok(()) } + +async fn handle_client_message( + tx: sync::Sender, + socket: UnixListener, +) -> Result<(), Error> { + while let Some(stream) = socket.incoming().next().await { + tx.send(Message::from(stream?.bytes().next().await.expect("unexpected EOF")?)).await + } + + Ok(()) +} + +async fn handle_interrupt_message( + status_rx: sync::Receiver, + message_rx: sync::Receiver, +) -> Result<(), Error> { + let interruptable = Arc::new(Mutex::new(false)); + + // This future is responsible to monitor the status changes for the pipeline + // and mark if it is interruptable or not. + let status_fut = { + let interruptable = interruptable.clone(); + async move { + loop { + match status_rx.recv().await? { + PipelineStatus::Continuous => *interruptable.lock().await = false, + PipelineStatus::Interruptable => *interruptable.lock().await = true, + } + } + } + }; + + // The client messages are handled in this future and it takes the + // interruptable status in consideration. + let message_fut = async move { + 'outter: loop { + match message_rx.recv().await? { + Message::Interrupt => loop { + if *interruptable.lock().await { + break 'outter Ok(()); + } + + // The yield is required to avoid the status_fut to starve. + task::yield_now().await; + }, + } + } + }; + + status_fut.race(message_fut).await +} diff --git a/src/main.rs b/src/main.rs index 473a5a9..65675ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,9 @@ mod animation; mod gstreamer; +mod message; -use crate::animation::Animation; +use crate::{animation::Animation, message::Message}; use argh::FromArgs; use log::{info, LevelFilter}; @@ -24,6 +25,7 @@ struct CmdLine { #[argh(subcommand)] enum Commands { Open(Open), + Client(Client), } /// open the render with the specific animation @@ -34,11 +36,32 @@ struct Open { #[argh(positional)] path: PathBuf, + /// runtime directory (default to '/tmp/easysplash') + #[argh(option, default = "PathBuf::from(\"/tmp/easysplash\")")] + runtime_dir: PathBuf, + /// log level to use (default to 'info') #[argh(option, default = "LevelFilter::Info")] log: LevelFilter, } +/// control the render from the user space +#[derive(FromArgs)] +#[argh(subcommand, name = "client")] +struct Client { + /// stop the render as soon as possible + #[argh(switch)] + stop: bool, + + /// runtime directory (default to '/tmp/easysplash') + #[argh(option, default = "PathBuf::from(\"/tmp/easysplash\")")] + runtime_dir: PathBuf, + + /// log level to use (default to info) + #[argh(option, default = "LevelFilter::Info")] + log: LevelFilter, +} + #[async_std::main] async fn main() -> Result<(), anyhow::Error> { let cmdline: CmdLine = argh::from_env(); @@ -49,7 +72,15 @@ async fn main() -> Result<(), anyhow::Error> { info!("starting EasySplash animation"); - gstreamer::play_animation(Animation::from_path(&render.path)?).await?; + let socket = message::bind_socket(render.runtime_dir).await?; + gstreamer::play_animation(Animation::from_path(&render.path)?, socket).await?; + } + Commands::Client(client) => { + simple_logging::log_to_stderr(client.log); + + if client.stop { + message::send(client.runtime_dir, Message::Interrupt).await?; + } } } diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..c8a7a76 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,64 @@ +// EasySplash - tool for animated splash screens +// Copyright (C) 2020 O.S. Systems Software LTDA. +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use async_std::{ + io, + os::unix::net::{UnixListener, UnixStream}, + prelude::*, +}; +use log::trace; +use std::{fs, path::PathBuf}; + +#[derive(Debug)] +pub(crate) enum Message { + /// Send a interrupt request for the animation. + Interrupt, +} + +pub(crate) async fn bind_socket(runtime_dir: PathBuf) -> Result { + let path = unix_socket_file(runtime_dir, true).await?; + if path.exists() { + fs::remove_file(&path)?; + } + + trace!("Binding to {:?} unix socket", &path); + UnixListener::bind(&path).await +} + +pub(crate) async fn send(runtime_dir: PathBuf, msg: Message) -> Result<(), io::Error> { + trace!("Sending stop request"); + UnixStream::connect(unix_socket_file(runtime_dir, false).await?) + .await? + .write_all(&[msg.into()]) + .await +} + +impl From for Message { + fn from(v: u8) -> Self { + match v { + 0x1 => Message::Interrupt, + _ => unreachable!("invalid message code"), + } + } +} + +impl From for u8 { + fn from(msg: Message) -> Self { + match msg { + Message::Interrupt => 0x1, + } + } +} + +async fn unix_socket_file( + runtime_dir: PathBuf, + create_missing: bool, +) -> Result { + if create_missing && !runtime_dir.exists() { + fs::create_dir_all(&runtime_dir)?; + } + + Ok(runtime_dir.join("ipc.socket")) +} From 59730d70d4426c28c92d63630ce4c68f81ff8503 Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Tue, 23 Jun 2020 19:02:16 -0300 Subject: [PATCH 6/6] README: Update to new implementation in Rust and GStreamer Signed-off-by: Otavio Salvador --- README.md | 185 ++++++++++++++---------------------------------------- 1 file changed, 47 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index a3c71c8..dc7e7f2 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,27 @@ EasySplash ========== -This is a simple program for animated splash screens using OpenGL ES for rendering. It consists -of the main splash screen program (easysplash) and a small control program (easysplashctl). +This is a simple program for animated splash screens using GStreamer for rendering. It consists of +an application which is capable of playing the animation and control its flow. -EasySplash takes as input zip archives containing a description and PNG-encoded image frames. -The frames are divided in parts. The archive format is documented [here](doc/Animation-Structure.md) - -It is possible to run EasySplash in X11 and on the PC. This is useful for debugging purposes -and for trying out new animations on the PC before uploading it to embedded devices. - -EasySplash can run in realtime or non-realtime mode. realtime mode means the animaton progresses -in realtime, without user interaction. In non-realtime mode, the animation progresses only by -sending progress updates over easysplashctl. The latter is useful if a progress value can be -determined while something is loading, for example. +It is possible to run EasySplash in all GStreamer supported backends. This is useful for debugging +purposes and for trying out new animations on the desktop before uploading it to embedded devices. For example, the below is the animation that has been in use for O.S. Systems' demo images: ![O.S. Systems demo boot animation](https://github.com/OSSystems/easysplash/raw/master/doc/demo-animation.gif) -There are two examples, which may be used as reference: +There are two animation [examples](https://github.com/OSSystems/EasySplash/tree/master/data), which may be used as reference: -* [O.S. Systems glowing logo](http://bit.ly/3cufarU) -* [O.S. Systems demo boot animation](http://bit.ly/38cwJcD) +* [O.S. Systems glowing logo](https://github.com/OSSystems/EasySplash/tree/master/data/glowing-logo/) +* [O.S. Systems demo boot animation](https://github.com/OSSystems/EasySplash/tree/master/data/ossystems-demo/) Requirements ------------ -* OpenGL ES 2.0 or later -* libpng version 1.2.50 or later -* zlib version 1.2.8 or later -* C++11 capable compiler on the build machine (recommended: gcc 4.8 or newer) -* CMake (used for as build system) - - - -Progress, realtime, and non-realtime mode ------------------------------------------ - -EasySplash defines progress in a 0-100 scale. 0 means the beginning of the animation, -100 the end. At value 100, EasySplash stops. - -As described above, realtime mode means the animation progresses on its own, without -having to use easysplashctl. In realtime mode, value sent by easysplashctl to -EasySplash will not have any effect unless it is the value 100, at which it exits. - -In non-realtime mode, the animation is advanced to the point corresponding to the -progress value sent with easysplashctl. It automatically starts at 0. If for -example 50 is sent to EasySplash, it will advance to the middle of the animation, -and display the frame at this point. - -The non-realtime mode is useful if the splash screen shall cover the time until some -operation is finished, and there are notifications available how far along this -operation has gone. In that case, one can send progress updates to EasySplash with -easysplashctl, and when the operation finishes, the end of animation is reached. - -The realtime mode is useful if no such notifications are available. One example would -be a Linux boot phase. In this situation, a free-running animation is preferable. - - -Building EasySplash -------------------- - -EasySplash uses the [CMake build system](http://www.cmake.org/). -To configure the build, first set the following environment variables to whatever is -necessary for cross compilation for your platform: - -* `CXX` -* `CXXFLAGS` -* `LDFLAGS` -* `PKG_CONFIG_PATH` -* `PKG_CONFIG_SYSROOT_DIR` - -Then, run from EasySplash's root directory: - - mkdir build - cd build - cmake .. -D[display-type]=1 - -(The aforementioned environment variables are only necessary for this configure call.) - -[display-type] specifies which display type to use. At this point, the following types are available: - -* `DISPLAY_TYPE_SWRENDER`: Software rendering to the Linux framebuffer using the - [Pixman library](http://www.pixman.org/) for blitting frames to screen -* `DISPLAY_TYPE_G2D`: Hardware accelerated rendering using the G2D API (i.MX specific) -* `DISPLAY_TYPE_GLES`: Hardware accelerated rendering using the OpenGL ES API - -In the `DISPLAY_TYPE_GLES` case, an additional argument is necessary, `-D[egl-platform]=1`: - -* `EGL_PLATFORM_X11`: Create an EGL surface in an X11 window (useful for debugging animations; also works on the PC) -* `EGL_PLATFORM_VIV_FB`: Create an EGL surface directly on the framebuffer (Vivante GPU specific) -* `EGL_PLATFORM_RPI_DISPMANX`: Create an EGL surface directly on the framebuffer (Raspberry Pi "userland" dispmanx specific) -* `EGL_PLATFORM_GBM`: Create an EGL surface using MESA GBM (supports modern acceleration, such as Etnaviv) - -Example cmake call with an OpenGL ES display on the Framebuffer: - - cmake .. -DDISPLAY_TYPE_GLES=1 -DEGL_PLATFORM_VIV_FB=1 - -Furthermore, support for System V and SystemD are available. Those are enabled using: - -* `ENABLE_SYSTEMD_SUPPORT`: Enable SystemD support -* `ENABLE_SYSVINIT_SUPPORT`: Enable Sys V init support - -Now that the build is configured, simply run: - - make - -Once the build is finished, install using - - make install +* Rust 1.40.0 or newer +* GStreamer (tested with 1.16) Running EasySplash @@ -118,60 +29,58 @@ Running EasySplash This is the help screen of EasySplash when ran with the -h argument: - Usage: build/easysplash [OPTION]... - -h --help display this usage information and exit - -i --zipfile zipfile with animation to play - -v --loglevel minimum levels messages must have to be logged - -n --non-realtime Run in non-realtime mode (animation advances depending - on the percentage value from the ctl application) + Usage: easysplash [] -Valid log levels are: trace, debug, info, warning, error. Log output goes to stderr. + EasySplash offers a convenient boot splash for Embedded Linux devices, + focusing of simplicity and easy to use. -If EasySplash was built with the `DISPLAY_TYPE_GLES` display type and the `EGL_PLATFORM_VIV_FB` -EGL platform, it will automatically enable double buffering and vsync to prevent tearing -artifacts. If this is not desired, start EasySplash with the `EASYSPLASH_NO_FB_MULTI_BUFFER` -environment variable set (the value is unimportant). + Options: + --help display usage information -easysplashctl expects EasySplash to be already running, otherwise it exits with -an error. It requires one argument, the progress indicator, which is a number in the 0-100 -range. For example, to transmit the progress value 55 to EasySplash, run: + Commands: + open open the render with the specific animation + client control the render from the user space - easysplashctl 55 -An optional second argument is `--wait-until-finished`. It is only meaningful if the progress -value is 100. It instructs easysplashctl to wait until the EasySplash process ends. This is -useful to let other applications (which access the screen) wait until EasySplash has finished -displaying the animation. Example: +Animation format +---------------- - easysplashctl 100 --wait-until-finished +EasySplash accepts animations in a directory which layout must be like this: + animation.toml + part1.mp4 + part2.mp4 + part3.mp4 -Mesa3D / GBM specific notes ---------------------------- +The names of the animation files can be chosen freely. Only the animation manifest file, named +`animation.toml`, needs to always be named as shown. -When running EasySplash on a platform with Mesa3D and GBM as OpenGL implementation and -framebuffer management system respectively, there are additional environment variables -that can be used for explicitely specifying several parameters that are otherwise -autodetected. These environment variables are: +The `animation.toml` file uses the [TOML](https://github.com/toml-lang/toml) format and intends to +define the animation. Below is a full example, for reference: -* `EASYSPLASH_GBM_DRM_DEVICE`: Path to a DRI device node. Example: `/dev/dri/card0`. - i.MX6 mainline kernel based platforms may need this variable to be set to - `/dev/dri/card1` in order to select the etnaviv DRI node. -* `EASYSPLASH_GBM_DRM_CONNECTOR`: The DRM connector to use. A connector is for example - a HDMI input, LVDS pins, etc. If EasySplash runs, but nothing shows on screen, then - perhaps the wrong connector is being used. Run EasySplash with the `-v trace` flag - to get verbose log output, then search for lines about DRM connectors to see a list - of available connectors and to check which one is being selected. Then, if necessary, - specify the correct one as the value of this environment variable. + [[part]] + file = "part1.mp4" + mode = "complete" + [[part]] + file = "part2.mp4" + mode = "complete" + repeat = 1 + [[part]] + file = "part3.mp4" + mode = "interruptable" -Implementation notes --------------------- +The animation view port is always rendered on the center of screen. The parts are defined using +`[[part]]`. The parts are inserted in the order encountered. For every part, following options are +available: -The EasySplash code makes extensive use of return value optimization, move semantics, -type deduction, and lambdas. Therefore, it is recommended to use a good C++11 capable -compiler. +- `file`: specifies the file to use for the part (required). +- `mode`: is either `complete` or `interruptable`. (optional) + - `complete` means the part must be played completely, even if somebody requested EasySplash to + stop (default). + - `interruptable` means it can be stopped immediately. +- `repeat`: defines how many times a part is replayed before moving to next. (optional) License