diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fa0342..ae84469 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,120 +1,83 @@ -cmake_minimum_required(VERSION 3.19) -include(FetchContent) -include(ExternalProject) - -project("haio" LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -add_executable(xxd scripts/xxd.cpp) -add_executable(replace scripts/replace.cpp) -find_package(Python REQUIRED COMPONENTS Interpreter) - -file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/shaders") -file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/include/shaders") -file(GLOB_RECURSE HAIO_LIB_SOURCES "${CMAKE_CURRENT_LIST_DIR}/library/*/*.cpp") -#TODO: file(GLOB_RECURSE HAIO_BIN_SOURCES "${CMAKE_CURRENT_LIST_DIR}/source/*/*.cpp") -add_executable(${PROJECT_NAME} "${CMAKE_CURRENT_LIST_DIR}/source/main.cpp;${HAIO_LIB_SOURCES};${HAIO_BIN_SOURCES}") -target_include_directories(${PROJECT_NAME} SYSTEM PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include;${CMAKE_BINARY_DIR}/include") -set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") - -if (WIN32) - file(GLOB_RECURSE HAIO_WINDOWS_SOURCES "${CMAKE_CURRENT_LIST_DIR}/source/platform/microslop/*.cpp") - target_sources(${PROJECT_NAME} PRIVATE "${HAIO_WINDOWS_SOURCES}") -endif() - -set(WUFFS_VERSION "v0.3.4") -set(WUFFS_DIR "${CMAKE_SOURCE_DIR}/vendor/wuffs") -set(WUFFS_DOWNLOAD "https://github.com/google/wuffs-mirror-release-c/archive/refs/tags/${WUFFS_VERSION}.tar.gz") -FetchContent_Populate(wuffs URL "${WUFFS_DOWNLOAD}" SOURCE_DIR "${WUFFS_DIR}") -set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/library/codecs/png.cpp" PROPERTIES INCLUDE_DIRECTORIES "${WUFFS_DIR}/release/c") - -set(VULKAN_VERSION "v1.3.290") -set(VULKAN_DIR "${CMAKE_SOURCE_DIR}/vendor/vulkan/vulkan") -set(VULKAN_DOWNLOAD "https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/${VULKAN_VERSION}.tar.gz") -FetchContent_Populate(vulkan URL ${VULKAN_DOWNLOAD} SOURCE_DIR ${VULKAN_DIR}) - -set(VOLK_VERSION "1.4.350") -set(VOLK_DIR "${CMAKE_SOURCE_DIR}/vendor/vulkan/volk") -set(VOLK_DOWNLOAD "https://github.com/zeux/volk/archive/refs/tags/${VOLK_VERSION}.tar.gz") -FetchContent_Populate(volk URL "${VOLK_DOWNLOAD}" SOURCE_DIR "${VOLK_DIR}") -file(GLOB HAIO_VULKAN_SOURCES "${CMAKE_CURRENT_LIST_DIR}/library/vulkan/*.cpp") -set_source_files_properties(${HAIO_VULKAN_SOURCES} PROPERTIES INCLUDE_DIRECTORIES "${VOLK_DIR};${VULKAN_DIR}/include") -add_library(volk STATIC ${VOLK_DIR}/volk.c) -target_link_libraries(${PROJECT_NAME} PRIVATE volk) -target_include_directories(volk SYSTEM PRIVATE "${VULKAN_DIR}/include") - -set(SHADERC_VERSION "v2026.2") -set(SHADERC_DIR "${CMAKE_SOURCE_DIR}/vendor/vulkan/shaderc") -set(SHADERC_DOWNLOAD "https://github.com/google/shaderc/archive/refs/tags/${SHADERC_VERSION}.tar.gz") -ExternalProject_Add(shaderc - URL ${SHADERC_DOWNLOAD} - PREFIX ${CMAKE_BINARY_DIR}/shaderc - SOURCE_DIR ${SHADERC_DIR} - UPDATE_COMMAND cd ${SHADERC_DIR} && $ utils/git-sync-deps - PATCH_COMMAND $ - "${SHADERC_DIR}/third_party/glslang/CMakeLists.txt" - "if \\(GLSLANG_ENABLE_INSTALL\\)" - "if(FALSE)" - CMAKE_ARGS - -DSHADERC_SKIP_TESTS=ON - -DSHADERC_SKIP_INSTALL=ON - -DSHADERC_SKIP_EXAMPLES=ON - -DCMAKE_BUILD_TYPE=Release - -G "${CMAKE_GENERATOR}" - BUILD_COMMAND $(MAKE) -C glslc_exe - INSTALL_COMMAND "" -) -ExternalProject_Add_StepDependencies(shaderc patch replace) -ExternalProject_Get_Property(shaderc BINARY_DIR) - -add_executable(glslc IMPORTED) -set_target_properties(glslc PROPERTIES IMPORTED_LOCATION "${BINARY_DIR}/glslc/glslc${CMAKE_EXECUTABLE_SUFFIX}") -add_dependencies(glslc shaderc) - -file(GLOB_RECURSE GLSL_SOURCES "${CMAKE_SOURCE_DIR}/shaders/*.glsl") -set(GENERATED_HEADERS "") -foreach(GLSL ${GLSL_SOURCES}) - get_filename_component(GLSL_NAME ${GLSL} NAME_WE) - - set(SPIRV_OUT ${CMAKE_BINARY_DIR}/shaders/${GLSL_NAME}.spirv) - set(HEADER_OUT ${CMAKE_BINARY_DIR}/include/shaders/${GLSL_NAME}.h) - - add_custom_command( - OUTPUT ${SPIRV_OUT} - COMMAND $ -fshader-stage=comp ${GLSL} -o ${SPIRV_OUT} - DEPENDS ${GLSL} - COMMENT "Compiling shader: ${GLSL_NAME}" - ) - - add_custom_command( - OUTPUT ${HEADER_OUT} - COMMAND $ ${SPIRV_OUT} > ${HEADER_OUT} - DEPENDS ${SPIRV_OUT} xxd - COMMENT "Embedding shader: ${GLSL_NAME}" - ) - - list(APPEND GENERATED_HEADERS ${HEADER_OUT}) -endforeach() -add_custom_target(haio_shaders DEPENDS ${GENERATED_HEADERS} shaderc) -add_dependencies(${PROJECT_NAME} haio_shaders) - -option(HAIO_TESTS "Unit Tests" OFF) -if(HAIO_TESTS) - include(CTest) - set(tests - "test_image_convert_png_to_ppm,library/codecs/png.cpp" - ) - foreach(deps IN LISTS tests) - string(REPLACE "," ";${CMAKE_SOURCE_DIR}/" deps "${deps}") - list(GET deps 0 tests_name) - list(REMOVE_AT deps 0) - add_executable("${tests_name}" "${CMAKE_SOURCE_DIR}/tests/${tests_name}.cpp;${deps};${HAIO_LIB_SOURCES}") - target_link_libraries(${tests_name} PRIVATE volk) - add_dependencies(${tests_name} haio_shaders) - target_include_directories(${tests_name} PRIVATE "${CMAKE_SOURCE_DIR}/include;${CMAKE_BINARY_DIR}/include") - add_test(NAME "${tests_name}" COMMAND "${tests_name}") - endforeach() -endif() \ No newline at end of file +cmake_minimum_required(VERSION 3.19) +include(FetchContent) +include(CheckCXXCompilerFlag) + +project("haio" LANGUAGES C CXX) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) +endif() +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(Threads REQUIRED) +find_package(PNG REQUIRED) +find_package(Boost REQUIRED COMPONENTS url) +find_package(PkgConfig REQUIRED) +pkg_check_modules(WEBP REQUIRED IMPORTED_TARGET libwebp) + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +file(GLOB_RECURSE HAIO_LIB_SOURCES "${CMAKE_CURRENT_LIST_DIR}/library/*/*.cpp") + +set(ETCPAK_VERSION "2.1") +set(ETCPAK_DIR "${CMAKE_SOURCE_DIR}/vendor/etcpak") +set(ETCPAK_DOWNLOAD "https://github.com/wolfpld/etcpak/archive/refs/tags/${ETCPAK_VERSION}.tar.gz") +FetchContent_Populate(etcpak URL "${ETCPAK_DOWNLOAD}" SOURCE_DIR "${ETCPAK_DIR}") +set(ETCPAK_SOURCES + "${ETCPAK_DIR}/bcdec.c" + "${ETCPAK_DIR}/Decode.cpp" + "${ETCPAK_DIR}/Dither.cpp" + "${ETCPAK_DIR}/ProcessRGB.cpp" + "${ETCPAK_DIR}/Tables.cpp" +) + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64|i[3-6]86)$") + check_cxx_compiler_flag("-msse4.1" HAIO_HAS_SSE41) + if(HAIO_HAS_SSE41) + set_source_files_properties(${ETCPAK_SOURCES} PROPERTIES COMPILE_OPTIONS "-msse4.1") + endif() +endif() + +set(WUFFS_VERSION "v0.3.4") +set(WUFFS_DIR "${CMAKE_SOURCE_DIR}/vendor/wuffs") +set(WUFFS_DOWNLOAD "https://github.com/google/wuffs-mirror-release-c/archive/refs/tags/${WUFFS_VERSION}.tar.gz") +FetchContent_Populate(wuffs URL "${WUFFS_DOWNLOAD}" SOURCE_DIR "${WUFFS_DIR}") + +add_library(haio_core STATIC ${HAIO_LIB_SOURCES} ${ETCPAK_SOURCES}) +target_include_directories(haio_core + PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include" + PRIVATE "${ETCPAK_DIR}" "${WUFFS_DIR}/release/c" +) +target_link_libraries(haio_core PUBLIC PNG::PNG PkgConfig::WEBP Boost::headers Boost::url Threads::Threads) + +if(WIN32) + file(GLOB_RECURSE HAIO_WINDOWS_SOURCES "${CMAKE_CURRENT_LIST_DIR}/source/platform/microslop/*.cpp") + target_sources(haio_core PRIVATE ${HAIO_WINDOWS_SOURCES}) +endif() + +add_executable(${PROJECT_NAME} "${CMAKE_CURRENT_LIST_DIR}/source/main.cpp") +target_link_libraries(${PROJECT_NAME} PRIVATE haio_core) +set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +option(HAIO_TESTS "Unit Tests" OFF) +if(HAIO_TESTS) + include(CTest) + set(tests + "test_image_convert_png_to_ppm" + "test_image_etc1_roundtrip" + "test_image_convert_png_to_etc1_ppm" + "test_gpu_formats_roundtrip" + "test_url_parsing" + ) + foreach(test_name IN LISTS tests) + add_executable("${test_name}" "${CMAKE_SOURCE_DIR}/tests/${test_name}.cpp") + target_link_libraries("${test_name}" PRIVATE haio_core) + if(test_name STREQUAL "test_image_convert_png_to_ppm") + add_test(NAME "${test_name}" COMMAND "${test_name}" "${CMAKE_SOURCE_DIR}/assets/logo8x8.png" "${CMAKE_BINARY_DIR}/logo8x8_direct.ppm") + elseif(test_name STREQUAL "test_image_convert_png_to_etc1_ppm") + add_test(NAME "${test_name}" COMMAND "${test_name}" "${CMAKE_SOURCE_DIR}/assets/logo8x8.png" "${CMAKE_BINARY_DIR}/logo8x8_etc1.ppm") + else() + add_test(NAME "${test_name}" COMMAND "${test_name}") + endif() + endforeach() +endif() diff --git a/include/haio.hpp b/include/haio.hpp index 2d4b0da..d77674a 100644 --- a/include/haio.hpp +++ b/include/haio.hpp @@ -1,45 +1,46 @@ -#include "haio_common.hpp" -#include "haio_formats.hpp" -#include "haio_iwindow.hpp" -#include "haio_vulkan.hpp" - -namespace Haio { - -struct Image { - Format type; - int width; - int height; - std::vector data; -}; - -using Stage = std::function; - -template -Stage Encode(); - -template -Stage Decode(); - -inline Stage operator|(Stage a, Stage b) { - return [=](const Image& img) { - return b(a(img)); - }; -} - -inline Image operator>>(std::istream& in, Stage decode) { - std::vector buffer( - (std::istreambuf_iterator(in)), - std::istreambuf_iterator() - ); - Image raw{Format::RAW, 0, 0, std::move(buffer)}; - return decode(raw); -} - -inline std::ostream& operator>>(const Image& img, std::ostream& out) { - out.write(reinterpret_cast(img.data.data()), img.data.size()); - return out; -} - -std::unique_ptr CreateWindow(const char* title, int width, int height); - -} +#include "haio_common.hpp" +#include "haio_formats.hpp" +#include "haio_buffer.hpp" +#include "haio_iwindow.hpp" +#include "haio_pipeline.hpp" + +namespace Haio { + +struct Image { + Format type; + int width; + int height; + std::vector data; +}; + +using Stage = std::function; + +template +Stage Encode(); + +template +Stage Decode(); + +inline Stage operator|(Stage a, Stage b) { + return [=](const Image& img) { + return b(a(img)); + }; +} + +inline Image operator>>(std::istream& in, Stage decode) { + std::vector buffer( + (std::istreambuf_iterator(in)), + std::istreambuf_iterator() + ); + Image raw{Format::RAW, 0, 0, std::move(buffer)}; + return decode(raw); +} + +inline std::ostream& operator>>(const Image& img, std::ostream& out) { + out.write(reinterpret_cast(img.data.data()), img.data.size()); + return out; +} + +std::unique_ptr CreateWindow(const char* title, int width, int height); + +} diff --git a/include/haio_buffer.hpp b/include/haio_buffer.hpp new file mode 100644 index 0000000..920a8d7 --- /dev/null +++ b/include/haio_buffer.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "haio_common.hpp" +#include "haio_formats.hpp" + +namespace Haio::Buffer { + +template +void Copy(const std::vector& input, std::vector& output); + +} diff --git a/include/haio_cdn.hpp b/include/haio_cdn.hpp new file mode 100644 index 0000000..76c59e5 --- /dev/null +++ b/include/haio_cdn.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "haio.hpp" + +#include + +#include +#include + +namespace Haio::Cdn { + +struct BucketConfig { + std::string name; + std::string type = "file"; + std::filesystem::path root = "."; + std::string host; + std::string prefix; + std::string endpoint; + std::map values; +}; + +struct Config { + std::string host = "0.0.0.0"; + unsigned short port = 8080; + std::map buckets; +}; + +Config loadConfig(const std::filesystem::path& path, std::filesystem::path defaultRoot = "."); +boost::asio::awaitable fetchBucket(const Config& config, std::string bucket, std::string path); +boost::asio::awaitable runServer(Config config); + +} diff --git a/include/haio_formats.hpp b/include/haio_formats.hpp index 77fda6e..55c9625 100644 --- a/include/haio_formats.hpp +++ b/include/haio_formats.hpp @@ -1,14 +1,21 @@ -#pragma once - -namespace Haio { - -enum class Format { - RAW, - PNG, - PPM, - RGBA8888, - RGB888, - YUV420 -}; - -} \ No newline at end of file +#pragma once + +namespace Haio { + +enum class Format { + RAW, + PNG, + PPM, + WEBP, + RGBA8888, + RGB888, + YUV420, + ETC1, + RGB565, + PVR, + DDS, + KTX, + KTX2 +}; + +} diff --git a/include/haio_iwindow.hpp b/include/haio_iwindow.hpp index 0d9e84c..27eb60d 100644 --- a/include/haio_iwindow.hpp +++ b/include/haio_iwindow.hpp @@ -1,7 +1,6 @@ -#pragma once -#include "haio_iwindow.hpp" - -namespace Haio { +#pragma once + +namespace Haio { class IWindow { public: @@ -13,4 +12,4 @@ class IWindow { virtual bool shouldClose() const = 0; }; -} \ No newline at end of file +} diff --git a/include/haio_pipeline.hpp b/include/haio_pipeline.hpp new file mode 100644 index 0000000..7d99263 --- /dev/null +++ b/include/haio_pipeline.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include "haio_common.hpp" +#include "haio_formats.hpp" + +#include +#include + +namespace Haio { + +struct Image; + +struct Blob { + Format format = Format::RAW; + std::string contentType = "application/octet-stream"; + std::string path; + std::vector data; +}; + +struct Rect { + int x = 0; + int y = 0; + int width = 0; + int height = 0; +}; + +struct Size { + int width = 0; + int height = 0; +}; + +enum class TokenKind { + Source, + DecodeAuto, + Decode, + Crop, + Resize, + Radius, + Encode +}; + +struct Token { + TokenKind kind = TokenKind::DecodeAuto; + std::string bucket; + std::string path; + Format format = Format::RAW; + Rect rect; + Size size; + int radius = 0; +}; + +class Pipeline { +public: + Pipeline& operator|=(Token token); + const std::vector& tokens() const; + +private: + std::vector tokens_; +}; + +namespace Tokens { +Token Source(std::string bucket, std::string path); +Token DecodeAuto(); +Token Decode(Format format); +Token Crop(Rect rect); +Token Resize(Size size); +Token Radius(int radius); +Token Encode(Format format); +} + +Format formatFromName(std::string_view name); +Format formatFromExtension(std::string_view path); +std::string_view formatName(Format format); +std::string_view extensionFor(Format format); +std::string_view contentTypeFor(Format format); +bool isEncodedImageFormat(Format format); +bool isTransformFormat(Format format); + +Image decodeBlob(const Blob& blob, Format requested = Format::RAW); +Blob encodeImage(const Image& image, Format format, std::string path = {}); +Blob runPipeline(Blob input, const Pipeline& pipeline); + +Image cropImage(const Image& image, Rect rect); +Image resizeImage(const Image& image, Size size); +Image roundImageCorners(const Image& image, int radius); + +std::vector parseQueryTokens(std::string_view query); +std::unordered_map parseQueryMap(std::string_view query); + +} diff --git a/include/haio_vulkan.hpp b/include/haio_vulkan.hpp deleted file mode 100644 index d757cfd..0000000 --- a/include/haio_vulkan.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include - -namespace Haio { - class Vulkan { - private: - struct Vm; - Vm* vm; - Vulkan(); - ~Vulkan(); - Vulkan(const Vulkan&) = delete; - Vulkan& operator=(const Vulkan&) = delete; - - public: - static Vulkan &getInstance(); - void transformRGBAtoRGB(const std::vector& input, std::vector& output); - }; -} diff --git a/library/buffer/rgb565_to_rgba8888.cpp b/library/buffer/rgb565_to_rgba8888.cpp new file mode 100644 index 0000000..9caad0c --- /dev/null +++ b/library/buffer/rgb565_to_rgba8888.cpp @@ -0,0 +1,31 @@ +#include + +#include + +namespace { + +static inline uint8_t expand5(uint16_t x) { + return static_cast((x * 255 + 15) / 31); +} + +static inline uint8_t expand6(uint16_t x) { + return static_cast((x * 255 + 31) / 63); +} + +} + +namespace Haio::Buffer { + +template <> +void Copy(const std::vector& input, std::vector& output) { + output.resize((input.size() / 2) * 4); + for (size_t src = 0, dst = 0; src < input.size(); src += 2, dst += 4) { + const auto px = static_cast(input[src + 0] | (input[src + 1] << 8)); + output[dst + 0] = expand5((px >> 11) & 0x1f); + output[dst + 1] = expand6((px >> 5) & 0x3f); + output[dst + 2] = expand5(px & 0x1f); + output[dst + 3] = 255; + } +} + +} diff --git a/library/buffer/rgba8888_to_rgb565.cpp b/library/buffer/rgba8888_to_rgb565.cpp new file mode 100644 index 0000000..f444865 --- /dev/null +++ b/library/buffer/rgba8888_to_rgb565.cpp @@ -0,0 +1,20 @@ +#include + +#include + +namespace Haio::Buffer { + +template <> +void Copy(const std::vector& input, std::vector& output) { + output.resize((input.size() / 4) * 2); + for (size_t src = 0, dst = 0; src < input.size(); src += 4, dst += 2) { + const auto r = static_cast((input[src + 0] * 31 + 127) / 255); + const auto g = static_cast((input[src + 1] * 63 + 127) / 255); + const auto b = static_cast((input[src + 2] * 31 + 127) / 255); + const auto px = static_cast((r << 11) | (g << 5) | b); + output[dst + 0] = static_cast(px); + output[dst + 1] = static_cast(px >> 8); + } +} + +} diff --git a/library/buffer/rgba8888_to_rgb888.cpp b/library/buffer/rgba8888_to_rgb888.cpp new file mode 100644 index 0000000..3a83292 --- /dev/null +++ b/library/buffer/rgba8888_to_rgb888.cpp @@ -0,0 +1,18 @@ +#include + +namespace Haio::Buffer { + +template <> +void Copy(const std::vector& input, std::vector& output) { + const auto outOffset = output.size(); + const auto pixelCount = input.size() / 4; + output.resize(outOffset + pixelCount * 3); + + for (size_t src = 0, dst = outOffset; src < pixelCount * 4; src += 4, dst += 3) { + output[dst + 0] = input[src + 0]; + output[dst + 1] = input[src + 1]; + output[dst + 2] = input[src + 2]; + } +} + +} diff --git a/library/cdn/buckets.cpp b/library/cdn/buckets.cpp new file mode 100644 index 0000000..ff74c8b --- /dev/null +++ b/library/cdn/buckets.cpp @@ -0,0 +1,181 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = beast::http; +using tcp = asio::ip::tcp; + +namespace { + +std::string ensureSlash(std::string value) { + if (value.empty() || value.front() != '/') value.insert(value.begin(), '/'); + return value; +} + +std::filesystem::path safeJoin(const std::filesystem::path& root, std::string_view rawPath) { + std::filesystem::path rel(rawPath); + if (rel.is_absolute()) rel = rel.relative_path(); + + std::filesystem::path clean; + for (const auto& part : rel) { + if (part == "." || part.empty()) continue; + if (part == "..") throw std::runtime_error("path traversal is not allowed"); + clean /= part; + } + return root / clean; +} + +Haio::Blob readFileBlob(const Haio::Cdn::BucketConfig& bucket, std::string path) { + const auto fullPath = safeJoin(bucket.root, path); + std::ifstream in(fullPath, std::ios::binary); + if (!in) throw std::runtime_error("file not found: " + fullPath.string()); + + std::vector data((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + const auto format = Haio::formatFromExtension(fullPath.string()); + return Haio::Blob{format, std::string(Haio::contentTypeFor(format)), fullPath.string(), std::move(data)}; +} + +struct HttpTarget { + std::string host; + std::string port = "80"; + std::string authority; + std::string target = "/"; +}; + +std::string toString(std::string_view value) { + return {value.begin(), value.end()}; +} + +std::string joinUrlPath(std::string_view prefix, std::string_view path) { + if (prefix.empty() || prefix == "/") return ensureSlash(std::string(path)); + if (path.empty()) return ensureSlash(std::string(prefix)); + + std::string out(prefix); + if (!out.ends_with('/')) out.push_back('/'); + if (path.starts_with('/')) path.remove_prefix(1); + out.append(path); + return ensureSlash(std::move(out)); +} + +std::string requestTarget(const boost::urls::url& url) { + auto target = toString(url.encoded_path()); + if (target.empty()) target = "/"; + if (url.has_query()) { + target.push_back('?'); + target += toString(url.encoded_query()); + } + return target; +} + +std::string withDefaultHttpScheme(std::string endpoint) { + if (!endpoint.starts_with("http://") && !endpoint.starts_with("https://")) { + endpoint.insert(0, "http://"); + } + return endpoint; +} + +HttpTarget parseHttpTarget(std::string endpoint, std::string path) { + auto normalized = withDefaultHttpScheme(std::move(endpoint)); + auto parsed = boost::urls::parse_uri(normalized); + if (!parsed) throw std::runtime_error("invalid http bucket endpoint: " + parsed.error().message()); + + boost::urls::url url(*parsed); + if (url.scheme() == "https") throw std::runtime_error("https buckets are not supported yet"); + if (url.scheme() != "http") throw std::runtime_error("unsupported http bucket scheme: " + toString(url.scheme())); + + url.set_path(joinUrlPath(url.path(), path)); + + return HttpTarget{ + .host = toString(url.host()), + .port = url.has_port() ? toString(url.port()) : "80", + .authority = toString(url.encoded_host_and_port()), + .target = requestTarget(url), + }; +} + +asio::awaitable fetchHttp(std::string host, std::string port, std::string authority, std::string target, std::string pathForFormat) { + auto executor = co_await asio::this_coro::executor; + tcp::resolver resolver(executor); + beast::tcp_stream stream(executor); + + auto results = co_await resolver.async_resolve(host, port, asio::use_awaitable); + co_await stream.async_connect(results, asio::use_awaitable); + + http::request req{http::verb::get, target, 11}; + req.set(http::field::host, authority.empty() ? host : authority); + req.set(http::field::user_agent, "haio-cdn"); + + co_await http::async_write(stream, req, asio::use_awaitable); + + beast::flat_buffer buffer; + http::response> res; + co_await http::async_read(stream, buffer, res, asio::use_awaitable); + + beast::error_code ec; + stream.socket().shutdown(tcp::socket::shutdown_both, ec); + + if (res.result_int() < 200 || res.result_int() >= 300) { + throw std::runtime_error("http bucket returned status " + std::to_string(res.result_int())); + } + + const auto format = Haio::formatFromExtension(pathForFormat.empty() ? target : pathForFormat); + co_return Haio::Blob{format, std::string(res[http::field::content_type]), pathForFormat, std::move(res.body())}; +} + +} + +namespace Haio::Cdn { + +asio::awaitable fetchBucket(const Config& config, std::string bucketName, std::string path) { + if (bucketName == "http") { + const auto slash = path.find('/'); + if (slash == std::string::npos) throw std::runtime_error("inline http bucket expects /cdn/http/host/path"); + auto target = parseHttpTarget(path, ""); + co_return co_await fetchHttp(std::move(target.host), std::move(target.port), std::move(target.authority), std::move(target.target), target.target); + } + + auto it = config.buckets.find(bucketName); + if (it == config.buckets.end()) { + if (bucketName == "file") it = config.buckets.find("file"); + if (it == config.buckets.end()) throw std::runtime_error("unknown bucket: " + bucketName); + } + + const auto& bucket = it->second; + if (bucket.type == "file" || bucket.type.empty()) { + co_return readFileBlob(bucket, std::move(path)); + } + + if (bucket.type == "http") { + HttpTarget target; + if (!bucket.endpoint.empty()) { + target = parseHttpTarget(bucket.endpoint, path); + } else { + target.host = bucket.host; + target.target = ensureSlash(bucket.prefix + "/" + path); + } + if (target.host.empty()) throw std::runtime_error("http bucket has no host"); + co_return co_await fetchHttp(std::move(target.host), std::move(target.port), std::move(target.authority), std::move(target.target), path); + } + + if (bucket.type == "s3") { + if (bucket.endpoint.empty()) { + throw std::runtime_error("s3 bucket needs endpoint/base_url for now"); + } + auto target = parseHttpTarget(bucket.endpoint, path); + co_return co_await fetchHttp(std::move(target.host), std::move(target.port), std::move(target.authority), std::move(target.target), path); + } + + throw std::runtime_error("unsupported bucket type: " + bucket.type); +} + +} diff --git a/library/cdn/config.cpp b/library/cdn/config.cpp new file mode 100644 index 0000000..e5ff7e4 --- /dev/null +++ b/library/cdn/config.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include +#include + +namespace { + +std::string trim(std::string value) { + auto is_space = [](unsigned char c) { return std::isspace(c); }; + value.erase(value.begin(), std::find_if_not(value.begin(), value.end(), is_space)); + value.erase(std::find_if_not(value.rbegin(), value.rend(), is_space).base(), value.end()); + return value; +} + +std::string unquote(std::string value) { + value = trim(std::move(value)); + if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) { + return value.substr(1, value.size() - 2); + } + return value; +} + +} + +namespace Haio::Cdn { + +Config loadConfig(const std::filesystem::path& path, std::filesystem::path defaultRoot) { + Config config; + config.buckets["file"] = BucketConfig{.name = "file", .type = "file", .root = std::move(defaultRoot)}; + + if (path.empty() || !std::filesystem::exists(path)) return config; + + std::ifstream in(path); + if (!in) throw std::runtime_error("cannot open config: " + path.string()); + + BucketConfig* current = nullptr; + std::string line; + while (std::getline(in, line)) { + if (const auto comment = line.find('#'); comment != std::string::npos) line.resize(comment); + line = trim(std::move(line)); + if (line.empty()) continue; + + if (line.front() == '[' && line.back() == ']') { + const auto section = line.substr(1, line.size() - 2); + constexpr std::string_view prefix = "bucket."; + if (section.starts_with(prefix)) { + const auto name = section.substr(prefix.size()); + auto& bucket = config.buckets[name]; + bucket.name = name; + current = &bucket; + } else { + current = nullptr; + } + continue; + } + + const auto eq = line.find('='); + if (eq == std::string::npos || !current) continue; + + const auto key = trim(line.substr(0, eq)); + const auto value = unquote(line.substr(eq + 1)); + current->values[key] = value; + + if (key == "type") current->type = value; + else if (key == "root" || key == "path") current->root = value; + else if (key == "host") current->host = value; + else if (key == "prefix") current->prefix = value; + else if (key == "endpoint" || key == "base_url" || key == "url") current->endpoint = value; + } + + return config; +} + +} diff --git a/library/cdn/server.cpp b/library/cdn/server.cpp new file mode 100644 index 0000000..0116198 --- /dev/null +++ b/library/cdn/server.cpp @@ -0,0 +1,163 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = beast::http; +using tcp = asio::ip::tcp; + +namespace { + +std::string toString(std::string_view value) { + return {value.begin(), value.end()}; +} + +std::string joinSegments(const std::vector& segments, size_t first) { + return std::accumulate( + segments.begin() + static_cast(first), + segments.end(), + std::string{}, + [](std::string out, const std::string& segment) { + if (!out.empty()) out.push_back('/'); + out += segment; + return out; + } + ); +} + +struct Route { + std::string bucket; + std::string path; + std::string query; +}; + +Route parseRoute(std::string_view target) { + auto parsed = boost::urls::parse_origin_form(target); + if (!parsed) throw std::runtime_error("invalid request target: " + parsed.error().message()); + + const auto encodedPath = toString(parsed->encoded_path()); + constexpr std::string_view prefix = "/cdn/"; + if (!encodedPath.starts_with(prefix)) throw std::runtime_error("expected /cdn//"); + + std::vector segments; + for (auto segment : parsed->segments()) segments.emplace_back(segment); + if (segments.empty() || segments.front() != "cdn") throw std::runtime_error("expected /cdn//"); + + const auto tail = std::string_view(encodedPath).substr(prefix.size()); + const auto query = parsed->has_query() ? toString(parsed->encoded_query()) : std::string{}; + if (tail.find('/') == std::string_view::npos) { + return Route{"file", segments.size() > 1 ? segments[1] : std::string{}, query}; + } + + auto bucket = segments.size() > 1 ? segments[1] : std::string{}; + auto rest = segments.size() > 2 ? joinSegments(segments, 2) : std::string{}; + if (bucket.empty()) bucket = "file"; + return Route{std::move(bucket), std::move(rest), query}; +} + +bool hasTokenKind(const std::vector& tokens, Haio::TokenKind kind) { + return std::ranges::any_of(tokens, [kind](const Haio::Token& token) { return token.kind == kind; }); +} + +bool hasImageTransform(const std::vector& tokens) { + return std::ranges::any_of(tokens, [](const Haio::Token& token) { + return token.kind == Haio::TokenKind::Crop || token.kind == Haio::TokenKind::Resize || token.kind == Haio::TokenKind::Radius; + }); +} + +http::response> makeResponse(http::status status, std::string_view contentType, std::vector body) { + http::response> res{status, 11}; + res.set(http::field::server, "haio-cdn"); + res.set(http::field::content_type, contentType); + res.body() = std::move(body); + res.prepare_payload(); + return res; +} + +http::response> makeText(http::status status, std::string text) { + return makeResponse(status, "text/plain; charset=utf-8", std::vector(text.begin(), text.end())); +} + +asio::awaitable>> handleRequest(const Haio::Cdn::Config& config, http::request req) { + if (req.method() != http::verb::get) { + co_return makeText(http::status::method_not_allowed, "method not allowed\n"); + } + + try { + const auto route = parseRoute(req.target()); + auto blob = co_await Haio::Cdn::fetchBucket(config, route.bucket, route.path); + auto tokens = Haio::parseQueryTokens(route.query); + + if (!tokens.empty()) { + Haio::Pipeline pipeline; + pipeline |= Haio::Tokens::Source(route.bucket, route.path); + for (auto token : tokens) pipeline |= std::move(token); + + if (hasImageTransform(pipeline.tokens()) && !hasTokenKind(pipeline.tokens(), Haio::TokenKind::Encode)) { + const auto fallback = blob.format == Haio::Format::RAW ? Haio::Format::PNG : blob.format; + pipeline |= Haio::Tokens::Encode(fallback == Haio::Format::PPM ? Haio::Format::PNG : fallback); + } + + blob = Haio::runPipeline(std::move(blob), pipeline); + } + + co_return makeResponse(http::status::ok, blob.contentType.empty() ? Haio::contentTypeFor(blob.format) : blob.contentType, std::move(blob.data)); + } catch (const std::exception& err) { + co_return makeText(http::status::bad_request, std::string(err.what()) + "\n"); + } +} + +asio::awaitable session(tcp::socket socket, Haio::Cdn::Config config) { + beast::flat_buffer buffer; + try { + for (;;) { + http::request req; + co_await http::async_read(socket, buffer, req, asio::use_awaitable); + const bool close = req.need_eof(); + auto res = co_await handleRequest(config, std::move(req)); + res.keep_alive(!close); + co_await http::async_write(socket, res, asio::use_awaitable); + if (close) break; + } + } catch (const std::exception&) { + } + + beast::error_code ec; + socket.shutdown(tcp::socket::shutdown_send, ec); +} + +asio::awaitable listener(Haio::Cdn::Config config) { + auto executor = co_await asio::this_coro::executor; + tcp::resolver resolver(executor); + const auto resolved = co_await resolver.async_resolve(config.host, std::to_string(config.port), asio::use_awaitable); + tcp::acceptor acceptor(executor, *resolved.begin()); + std::cout << "haio cdn listening on http://" << config.host << ':' << config.port << "\n"; + + for (;;) { + auto socket = co_await acceptor.async_accept(asio::use_awaitable); + asio::co_spawn(executor, session(std::move(socket), config), asio::detached); + } +} + +} + +namespace Haio::Cdn { + +asio::awaitable runServer(Config config) { + co_await listener(std::move(config)); +} + +} diff --git a/library/codecs/dds.cpp b/library/codecs/dds.cpp new file mode 100644 index 0000000..4c43ec5 --- /dev/null +++ b/library/codecs/dds.cpp @@ -0,0 +1,53 @@ +#include "gpu_container_common.hpp" + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + GpuContainer::validatePayload(img, false); + const auto pitch = static_cast(img.width * (img.type == Format::RGB565 ? 2 : 4)); + const auto flags = img.type == Format::RGB565 ? 0x40u : 0x41u; + const auto bits = img.type == Format::RGB565 ? 16u : 32u; + const auto r = img.type == Format::RGB565 ? 0xf800u : 0x000000ffu; + const auto g = img.type == Format::RGB565 ? 0x07e0u : 0x0000ff00u; + const auto b = img.type == Format::RGB565 ? 0x001fu : 0x00ff0000u; + const auto a = img.type == Format::RGB565 ? 0u : 0xff000000u; + + std::vector out; + for (auto v : {GpuContainer::fourcc('D', 'D', 'S', ' '), 124u, 0x100fu, static_cast(img.height), static_cast(img.width), + pitch, 0u, 0u}) GpuContainer::appendU32(out, v); + for (int i = 0; i < 11; i++) GpuContainer::appendU32(out, 0); + for (auto v : {32u, flags, 0u, bits, r, g, b, a, 0x1000u, 0u, 0u, 0u, 0u}) GpuContainer::appendU32(out, v); + out.insert(out.end(), img.data.begin(), img.data.end()); + return GpuContainer::wrapped(Format::DDS, img.width, img.height, std::move(out)); + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + if (img.data.size() < 128 || GpuContainer::readU32(img.data, 0) != GpuContainer::fourcc('D', 'D', 'S', ' ') || GpuContainer::readU32(img.data, 4) != 124 || GpuContainer::readU32(img.data, 76) != 32) { + throw std::runtime_error("invalid dds header"); + } + const auto height = static_cast(GpuContainer::readU32(img.data, 12)); + const auto width = static_cast(GpuContainer::readU32(img.data, 16)); + const auto bits = GpuContainer::readU32(img.data, 88); + const auto r = GpuContainer::readU32(img.data, 92); + const auto g = GpuContainer::readU32(img.data, 96); + const auto b = GpuContainer::readU32(img.data, 100); + const auto a = GpuContainer::readU32(img.data, 104); + Format format; + if (bits == 16 && r == 0xf800 && g == 0x07e0 && b == 0x001f && a == 0) { + format = Format::RGB565; + } else if (bits == 32 && r == 0x000000ff && g == 0x0000ff00 && b == 0x00ff0000 && a == 0xff000000) { + format = Format::RGBA8888; + } else { + throw std::runtime_error("unsupported dds payload format"); + } + auto data = GpuContainer::slice(img.data, 128, GpuContainer::payloadSize(format, width, height)); + return GpuContainer::wrapped(format, width, height, std::move(data)); + }; +} + +} diff --git a/library/codecs/etc1.cpp b/library/codecs/etc1.cpp new file mode 100644 index 0000000..9b95de9 --- /dev/null +++ b/library/codecs/etc1.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +static int paddedSize(int size) { + return (size + 3) & ~3; +} + +static size_t blockCount(int width, int height) { + return static_cast(paddedSize(width) / 4) * static_cast(paddedSize(height) / 4); +} + +static void validateDimensions(int width, int height) { + if (width <= 0 || height <= 0) { + throw std::runtime_error("etc1 images require positive width and height"); + } +} + +static std::vector makePaddedBGRA(const Haio::Image& img, int paddedWidth, int paddedHeight) { + const auto expectedSize = static_cast(img.width) * static_cast(img.height) * 4; + if (img.data.size() != expectedSize) { + throw std::runtime_error("invalid rgba8888 data size for etc1 encode"); + } + + std::vector pixels(static_cast(paddedWidth) * static_cast(paddedHeight)); + for (int y = 0; y < paddedHeight; y++) { + const int srcY = std::min(y, img.height - 1); + for (int x = 0; x < paddedWidth; x++) { + const int srcX = std::min(x, img.width - 1); + const auto srcOffset = (static_cast(srcY) * static_cast(img.width) + static_cast(srcX)) * 4; + const uint8_t bgra[4] = { + img.data[srcOffset + 2], + img.data[srcOffset + 1], + img.data[srcOffset + 0], + img.data[srcOffset + 3] + }; + std::memcpy(&pixels[static_cast(y) * static_cast(paddedWidth) + static_cast(x)], bgra, 4); + } + } + return pixels; +} + + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + if (img.type != Format::RGBA8888) { + throw std::runtime_error("etc1 encode only accepts rgba8888 images"); + } + validateDimensions(img.width, img.height); + + const int paddedWidth = paddedSize(img.width); + const int paddedHeight = paddedSize(img.height); + const size_t blocks = blockCount(img.width, img.height); + if (blocks > std::numeric_limits::max()) { + throw std::runtime_error("etc1 image is too large"); + } + + auto pixels = makePaddedBGRA(img, paddedWidth, paddedHeight); + std::vector compressed(blocks); + + CompressEtc1RgbDither( + pixels.data(), + compressed.data(), + static_cast(blocks), + static_cast(paddedWidth) + ); + + std::vector buffer(blocks * 8); + std::memcpy(buffer.data(), compressed.data(), buffer.size()); + + return Image{Format::ETC1, img.width, img.height, std::move(buffer)}; + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + if (img.type != Format::ETC1) { + throw std::runtime_error("etc1 decode only accepts etc1 images"); + } + validateDimensions(img.width, img.height); + + const size_t blocks = blockCount(img.width, img.height); + const size_t expectedSize = blocks * 8; + if (img.data.size() != expectedSize) { + throw std::runtime_error("invalid etc1 data size"); + } + + const int paddedWidth = paddedSize(img.width); + const int paddedHeight = paddedSize(img.height); + std::vector compressed(blocks); + std::memcpy(compressed.data(), img.data.data(), expectedSize); + + std::vector paddedRGBA(static_cast(paddedWidth) * static_cast(paddedHeight)); + DecodeRGB(compressed.data(), paddedRGBA.data(), paddedWidth, paddedHeight); + + std::vector buffer(static_cast(img.width) * static_cast(img.height) * 4); + for (int y = 0; y < img.height; y++) { + for (int x = 0; x < img.width; x++) { + const auto srcOffset = static_cast(y) * static_cast(paddedWidth) + static_cast(x); + const auto dstOffset = (static_cast(y) * static_cast(img.width) + static_cast(x)) * 4; + std::memcpy(buffer.data() + dstOffset, &paddedRGBA[srcOffset], 4); + buffer[dstOffset + 3] = 255; + } + } + + return Image{Format::RGBA8888, img.width, img.height, std::move(buffer)}; + }; +} + +} // namespace diff --git a/library/codecs/gpu_container_common.hpp b/library/codecs/gpu_container_common.hpp new file mode 100644 index 0000000..a7c53b6 --- /dev/null +++ b/library/codecs/gpu_container_common.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace Haio::GpuContainer { + +static constexpr uint32_t GL_UNSIGNED_BYTE = 0x1401; +static constexpr uint32_t GL_UNSIGNED_SHORT_5_6_5 = 0x8363; +static constexpr uint32_t GL_RGB = 0x1907; +static constexpr uint32_t GL_RGBA = 0x1908; +static constexpr uint32_t GL_RGBA8 = 0x8058; +static constexpr uint32_t GL_RGB565 = 0x8d62; +static constexpr uint32_t GL_ETC1_RGB8_OES = 0x8d64; +static constexpr uint32_t VK_FORMAT_R5G6B5_UNORM_PACK16 = 4; +static constexpr uint32_t VK_FORMAT_R8G8B8A8_UNORM = 37; +static constexpr uint32_t VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK = 147; + +struct Profile { + uint32_t glType; + uint32_t glFormat; + uint32_t glInternal; + uint32_t glBase; + uint32_t vkFormat; + uint64_t pvrFormat; + size_t size; +}; + +static inline uint32_t fourcc(char a, char b, char c, char d) { + return static_cast(a) | (static_cast(b) << 8) | (static_cast(c) << 16) | (static_cast(d) << 24); +} + +static inline uint64_t pvrRaw(char a, char b, char c, char d, uint8_t ba, uint8_t bb, uint8_t bc, uint8_t bd) { + return static_cast(a) | (static_cast(b) << 8) | (static_cast(c) << 16) | (static_cast(d) << 24) + | (static_cast(ba) << 32) | (static_cast(bb) << 40) | (static_cast(bc) << 48) | (static_cast(bd) << 56); +} + +static inline size_t etc1Size(int width, int height) { + return static_cast((width + 3) / 4) * static_cast((height + 3) / 4) * 8; +} + +static inline size_t payloadSize(Format format, int width, int height) { + if (width <= 0 || height <= 0) throw std::runtime_error("gpu container requires positive dimensions"); + if (format == Format::ETC1) return etc1Size(width, height); + if (format == Format::RGB565) return static_cast(width) * static_cast(height) * 2; + if (format == Format::RGBA8888) return static_cast(width) * static_cast(height) * 4; + throw std::runtime_error("unsupported gpu payload format"); +} + +static inline Profile profileFor(Format format, int width, int height) { + const auto size = payloadSize(format, width, height); + if (format == Format::ETC1) return {0, 0, GL_ETC1_RGB8_OES, GL_RGB, VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK, 6, size}; + if (format == Format::RGB565) return {GL_UNSIGNED_SHORT_5_6_5, GL_RGB, GL_RGB565, GL_RGB, VK_FORMAT_R5G6B5_UNORM_PACK16, pvrRaw('r', 'g', 'b', 0, 5, 6, 5, 0), size}; + return {GL_UNSIGNED_BYTE, GL_RGBA, GL_RGBA8, GL_RGBA, VK_FORMAT_R8G8B8A8_UNORM, pvrRaw('r', 'g', 'b', 'a', 8, 8, 8, 8), size}; +} + +static inline uint32_t typeSize(const Profile& p) { + return p.glType == GL_UNSIGNED_SHORT_5_6_5 ? 2u : 1u; +} + +static inline void validatePayload(const Image& img, bool allowEtc1 = true) { + if (!allowEtc1 && img.type == Format::ETC1) { + throw std::runtime_error("dds does not support etc1 payloads"); + } + const auto p = profileFor(img.type, img.width, img.height); + if (img.data.size() != p.size) throw std::runtime_error("invalid gpu payload size"); +} + +static inline void appendU32(std::vector& out, uint32_t v) { + out.push_back(static_cast(v)); + out.push_back(static_cast(v >> 8)); + out.push_back(static_cast(v >> 16)); + out.push_back(static_cast(v >> 24)); +} + +static inline void appendU64(std::vector& out, uint64_t v) { + appendU32(out, static_cast(v)); + appendU32(out, static_cast(v >> 32)); +} + +static inline uint32_t readU32(const std::vector& data, size_t off) { + if (off + 4 > data.size()) throw std::runtime_error("truncated gpu container"); + return static_cast(data[off + 0]) | (static_cast(data[off + 1]) << 8) + | (static_cast(data[off + 2]) << 16) | (static_cast(data[off + 3]) << 24); +} + +static inline uint64_t readU64(const std::vector& data, size_t off) { + return static_cast(readU32(data, off)) | (static_cast(readU32(data, off + 4)) << 32); +} + +static inline std::vector slice(const std::vector& data, size_t off, size_t len) { + if (off > data.size() || len > data.size() - off) throw std::runtime_error("truncated gpu payload"); + return {data.begin() + static_cast(off), data.begin() + static_cast(off + len)}; +} + +static inline Image wrapped(Format type, int width, int height, std::vector data) { + return Image{type, width, height, std::move(data)}; +} + +static inline Format ktxFormat(uint32_t glType, uint32_t glFormat, uint32_t glInternal) { + if (glType == 0 && glFormat == 0 && glInternal == GL_ETC1_RGB8_OES) return Format::ETC1; + if (glType == GL_UNSIGNED_SHORT_5_6_5 && glFormat == GL_RGB && glInternal == GL_RGB565) return Format::RGB565; + if (glType == GL_UNSIGNED_BYTE && glFormat == GL_RGBA && glInternal == GL_RGBA8) return Format::RGBA8888; + throw std::runtime_error("unsupported ktx payload format"); +} + +static inline Format vkFormat(uint32_t format) { + if (format == VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK) return Format::ETC1; + if (format == VK_FORMAT_R5G6B5_UNORM_PACK16) return Format::RGB565; + if (format == VK_FORMAT_R8G8B8A8_UNORM) return Format::RGBA8888; + throw std::runtime_error("unsupported ktx2 payload format"); +} + +static inline Format pvrFormat(uint64_t format) { + if (format == 6) return Format::ETC1; + if (format == pvrRaw('r', 'g', 'b', 0, 5, 6, 5, 0)) return Format::RGB565; + if (format == pvrRaw('r', 'g', 'b', 'a', 8, 8, 8, 8)) return Format::RGBA8888; + throw std::runtime_error("unsupported pvr payload format"); +} + +} diff --git a/library/codecs/ktx.cpp b/library/codecs/ktx.cpp new file mode 100644 index 0000000..df07fd6 --- /dev/null +++ b/library/codecs/ktx.cpp @@ -0,0 +1,42 @@ +#include "gpu_container_common.hpp" + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + GpuContainer::validatePayload(img); + const auto p = GpuContainer::profileFor(img.type, img.width, img.height); + std::vector out = {0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a}; + for (auto v : {0x04030201u, p.glType, GpuContainer::typeSize(p), p.glFormat, p.glInternal, p.glBase, static_cast(img.width), + static_cast(img.height), 0u, 0u, 1u, 1u, 0u}) { + GpuContainer::appendU32(out, v); + } + GpuContainer::appendU32(out, static_cast(img.data.size())); + out.insert(out.end(), img.data.begin(), img.data.end()); + while (out.size() % 4) out.push_back(0); + return GpuContainer::wrapped(Format::KTX, img.width, img.height, std::move(out)); + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + static constexpr std::array id = {0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a}; + if (img.data.size() < 68 || !std::equal(id.begin(), id.end(), img.data.begin())) throw std::runtime_error("invalid ktx header"); + if (GpuContainer::readU32(img.data, 12) != 0x04030201) throw std::runtime_error("unsupported ktx endianness"); + const auto format = GpuContainer::ktxFormat(GpuContainer::readU32(img.data, 16), GpuContainer::readU32(img.data, 24), GpuContainer::readU32(img.data, 28)); + const auto width = static_cast(GpuContainer::readU32(img.data, 36)); + const auto height = static_cast(GpuContainer::readU32(img.data, 40)); + if (GpuContainer::readU32(img.data, 44) || GpuContainer::readU32(img.data, 48) || GpuContainer::readU32(img.data, 52) != 1 || GpuContainer::readU32(img.data, 56) > 1) { + throw std::runtime_error("unsupported ktx texture shape"); + } + const auto dataOff = static_cast(64) + GpuContainer::readU32(img.data, 60); + const auto imageSize = GpuContainer::readU32(img.data, dataOff); + auto data = GpuContainer::slice(img.data, dataOff + 4, imageSize); + if (data.size() != GpuContainer::payloadSize(format, width, height)) throw std::runtime_error("invalid ktx payload size"); + return GpuContainer::wrapped(format, width, height, std::move(data)); + }; +} + +} diff --git a/library/codecs/ktx2.cpp b/library/codecs/ktx2.cpp new file mode 100644 index 0000000..03c2ec5 --- /dev/null +++ b/library/codecs/ktx2.cpp @@ -0,0 +1,42 @@ +#include "gpu_container_common.hpp" + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + GpuContainer::validatePayload(img); + const auto p = GpuContainer::profileFor(img.type, img.width, img.height); + std::vector out = {0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a}; + for (auto v : {p.vkFormat, GpuContainer::typeSize(p), static_cast(img.width), + static_cast(img.height), 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u}) GpuContainer::appendU32(out, v); + GpuContainer::appendU64(out, 0); + GpuContainer::appendU64(out, 0); + GpuContainer::appendU64(out, 104); + GpuContainer::appendU64(out, img.data.size()); + GpuContainer::appendU64(out, img.data.size()); + out.insert(out.end(), img.data.begin(), img.data.end()); + return GpuContainer::wrapped(Format::KTX2, img.width, img.height, std::move(out)); + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + static constexpr std::array id = {0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a}; + if (img.data.size() < 104 || !std::equal(id.begin(), id.end(), img.data.begin())) throw std::runtime_error("invalid ktx2 header"); + const auto format = GpuContainer::vkFormat(GpuContainer::readU32(img.data, 12)); + const auto width = static_cast(GpuContainer::readU32(img.data, 20)); + const auto height = static_cast(GpuContainer::readU32(img.data, 24)); + if (GpuContainer::readU32(img.data, 28) || GpuContainer::readU32(img.data, 32) || GpuContainer::readU32(img.data, 36) != 1 || GpuContainer::readU32(img.data, 40) != 1 || GpuContainer::readU32(img.data, 44)) { + throw std::runtime_error("unsupported ktx2 texture shape"); + } + const auto off = static_cast(GpuContainer::readU64(img.data, 80)); + const auto len = static_cast(GpuContainer::readU64(img.data, 88)); + auto data = GpuContainer::slice(img.data, off, len); + if (data.size() != GpuContainer::payloadSize(format, width, height)) throw std::runtime_error("invalid ktx2 payload size"); + return GpuContainer::wrapped(format, width, height, std::move(data)); + }; +} + +} diff --git a/library/codecs/png.cpp b/library/codecs/png.cpp index bc6a470..c447d02 100644 --- a/library/codecs/png.cpp +++ b/library/codecs/png.cpp @@ -1,72 +1,132 @@ -#include - -#define WUFFS_IMPLEMENTATION -#define WUFFS_CONFIG__MODULES -#define WUFFS_CONFIG__MODULE__BASE -#define WUFFS_CONFIG__MODULE__PNG -#define WUFFS_CONFIG__MODULE__ZLIB -#define WUFFS_CONFIG__MODULE__DEFLATE -#define WUFFS_CONFIG__MODULE__ADLER32 -#define WUFFS_CONFIG__MODULE__CRC32 -#include - -namespace Haio { - template <> - Stage Decode() { - return [](const Image &img) { - wuffs_png__decoder dec; - wuffs_base__status st = wuffs_png__decoder__initialize(&dec, sizeof dec, WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS); - - if (!wuffs_base__status__is_ok(&st)) { - throw std::runtime_error("wuffs is not ok!"); - } - - wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader((uint8_t *)img.data.data(), img.data.size(), true); - - wuffs_base__image_config cfg; - memset(&cfg, 0, sizeof cfg); - st = wuffs_base__image_decoder__decode_image_config(wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec), &cfg, &src); - if (!wuffs_base__status__is_ok(&st)) { - throw std::runtime_error("invalid png config"); - } - - uint32_t w = wuffs_base__pixel_config__width(&cfg.pixcfg); - uint32_t h = wuffs_base__pixel_config__height(&cfg.pixcfg); - if (!w || !h) { - throw std::runtime_error("invalid width ou height"); - } - - wuffs_base__pixel_config__set(&cfg.pixcfg, WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h); - - size_t img_size = (size_t)w * (size_t)h * 4; - std::vector buffer(img_size); - - wuffs_base__pixel_buffer pb; - st = wuffs_base__pixel_buffer__set_from_slice(&pb, &cfg.pixcfg, wuffs_base__make_slice_u8(buffer.data(), buffer.size())); - - if (!wuffs_base__status__is_ok(&st)) { - throw std::runtime_error("error 2"); - } - - wuffs_base__range_ii_u64 wb = wuffs_base__image_decoder__workbuf_len(wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec)); - std::vector work(wb.max_incl ? (size_t)wb.max_incl : 0); - - st = wuffs_base__image_decoder__decode_frame(wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec), &pb, &src, - WUFFS_BASE__PIXEL_BLEND__SRC, wuffs_base__make_slice_u8(work.data(), work.size()), NULL); - if (!wuffs_base__status__is_ok(&st)) { - throw std::runtime_error("error 3"); - } - - return Image{Format::RGBA8888, (int)w, (int)h, std::move(buffer)}; - }; - } - - template <> - Stage Encode() { - return [](const Image &img) { - std::vector buffer = {0, 1, 2, 3, 4}; - Image out{Format::PNG, 6, 5, buffer}; - return out; - }; - } -} +#include + +#include + +#include +#include + +#define WUFFS_IMPLEMENTATION +#define WUFFS_CONFIG__MODULES +#define WUFFS_CONFIG__MODULE__BASE +#define WUFFS_CONFIG__MODULE__PNG +#define WUFFS_CONFIG__MODULE__ZLIB +#define WUFFS_CONFIG__MODULE__DEFLATE +#define WUFFS_CONFIG__MODULE__ADLER32 +#define WUFFS_CONFIG__MODULE__CRC32 +#include + +namespace { + +struct PngWriteTarget { + std::vector* data; +}; + +void writePngBytes(png_structp png, png_bytep bytes, png_size_t len) { + auto* target = static_cast(png_get_io_ptr(png)); + target->data->insert(target->data->end(), bytes, bytes + len); +} + +void flushPngBytes(png_structp) {} + +} + +namespace Haio { + +template <> +Stage Decode() { + return [](const Image& img) { + wuffs_png__decoder dec; + wuffs_base__status st = wuffs_png__decoder__initialize(&dec, sizeof dec, WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS); + + if (!wuffs_base__status__is_ok(&st)) { + throw std::runtime_error("wuffs is not ok"); + } + + wuffs_base__io_buffer src = wuffs_base__ptr_u8__reader((uint8_t*)img.data.data(), img.data.size(), true); + + wuffs_base__image_config cfg; + memset(&cfg, 0, sizeof cfg); + st = wuffs_base__image_decoder__decode_image_config(wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec), &cfg, &src); + if (!wuffs_base__status__is_ok(&st)) { + throw std::runtime_error("invalid png config"); + } + + uint32_t w = wuffs_base__pixel_config__width(&cfg.pixcfg); + uint32_t h = wuffs_base__pixel_config__height(&cfg.pixcfg); + if (!w || !h) { + throw std::runtime_error("invalid png dimensions"); + } + + wuffs_base__pixel_config__set(&cfg.pixcfg, WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h); + + std::vector buffer(static_cast(w) * static_cast(h) * 4); + + wuffs_base__pixel_buffer pb; + st = wuffs_base__pixel_buffer__set_from_slice(&pb, &cfg.pixcfg, wuffs_base__make_slice_u8(buffer.data(), buffer.size())); + if (!wuffs_base__status__is_ok(&st)) { + throw std::runtime_error("failed to create png pixel buffer"); + } + + wuffs_base__range_ii_u64 wb = wuffs_base__image_decoder__workbuf_len(wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec)); + std::vector work(wb.max_incl ? static_cast(wb.max_incl) : 0); + + st = wuffs_base__image_decoder__decode_frame( + wuffs_png__decoder__upcast_as__wuffs_base__image_decoder(&dec), + &pb, + &src, + WUFFS_BASE__PIXEL_BLEND__SRC, + wuffs_base__make_slice_u8(work.data(), work.size()), + nullptr + ); + if (!wuffs_base__status__is_ok(&st)) { + throw std::runtime_error("failed to decode png frame"); + } + + return Image{Format::RGBA8888, static_cast(w), static_cast(h), std::move(buffer)}; + }; +} + +template <> +Stage Encode() { + return [](const Image& img) { + if (img.type != Format::RGBA8888) { + throw std::runtime_error("png encode only accepts rgba8888 images"); + } + if (img.width <= 0 || img.height <= 0 || img.data.size() != static_cast(img.width) * static_cast(img.height) * 4) { + throw std::runtime_error("invalid rgba8888 image for png encode"); + } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png) throw std::runtime_error("failed to create png writer"); + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_write_struct(&png, nullptr); + throw std::runtime_error("failed to create png info"); + } + + std::vector buffer; + PngWriteTarget target{&buffer}; + + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &info); + throw std::runtime_error("failed to encode png"); + } + + png_set_write_fn(png, &target, writePngBytes, flushPngBytes); + png_set_IHDR(png, info, img.width, img.height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + std::vector rows(static_cast(img.height)); + for (int y = 0; y < img.height; y++) { + rows[static_cast(y)] = const_cast(img.data.data() + static_cast(y) * static_cast(img.width) * 4); + } + png_write_image(png, rows.data()); + png_write_end(png, nullptr); + png_destroy_write_struct(&png, &info); + + return Image{Format::PNG, img.width, img.height, std::move(buffer)}; + }; +} + +} diff --git a/library/codecs/ppm.cpp b/library/codecs/ppm.cpp index 369f948..95697c2 100644 --- a/library/codecs/ppm.cpp +++ b/library/codecs/ppm.cpp @@ -11,7 +11,7 @@ Stage Encode() { if (img.type == Format::RGBA8888) { buffer.reserve(buffer.size() + img.width * img.height * 3); - Vulkan::getInstance().transformRGBAtoRGB(img.data, buffer); + Buffer::Copy(img.data, buffer); } else { buffer.insert(buffer.end(), img.data.begin(), img.data.end()); } diff --git a/library/codecs/pvr.cpp b/library/codecs/pvr.cpp new file mode 100644 index 0000000..d64b83a --- /dev/null +++ b/library/codecs/pvr.cpp @@ -0,0 +1,35 @@ +#include "gpu_container_common.hpp" + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + GpuContainer::validatePayload(img); + const auto p = GpuContainer::profileFor(img.type, img.width, img.height); + std::vector out; + for (auto v : {0x03525650u, 0u}) GpuContainer::appendU32(out, v); + GpuContainer::appendU64(out, p.pvrFormat); + for (auto v : {0u, 0u, static_cast(img.height), static_cast(img.width), 1u, 1u, 1u, 1u, 0u}) GpuContainer::appendU32(out, v); + out.insert(out.end(), img.data.begin(), img.data.end()); + return GpuContainer::wrapped(Format::PVR, img.width, img.height, std::move(out)); + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + if (img.data.size() < 52 || GpuContainer::readU32(img.data, 0) != 0x03525650) throw std::runtime_error("invalid pvr header"); + const auto format = GpuContainer::pvrFormat(GpuContainer::readU64(img.data, 8)); + const auto height = static_cast(GpuContainer::readU32(img.data, 24)); + const auto width = static_cast(GpuContainer::readU32(img.data, 28)); + if (GpuContainer::readU32(img.data, 32) != 1 || GpuContainer::readU32(img.data, 36) != 1 || GpuContainer::readU32(img.data, 40) != 1 || GpuContainer::readU32(img.data, 44) != 1) { + throw std::runtime_error("unsupported pvr texture shape"); + } + const auto off = static_cast(52) + GpuContainer::readU32(img.data, 48); + auto data = GpuContainer::slice(img.data, off, GpuContainer::payloadSize(format, width, height)); + return GpuContainer::wrapped(format, width, height, std::move(data)); + }; +} + +} diff --git a/library/codecs/rgb565.cpp b/library/codecs/rgb565.cpp new file mode 100644 index 0000000..560edda --- /dev/null +++ b/library/codecs/rgb565.cpp @@ -0,0 +1,51 @@ +#include + +#include + +namespace { + +/** @todo remove in future, validation should move to the compile-time graph when it exists */ +static void validateRGBA(const Haio::Image& img) { + if (img.type != Haio::Format::RGBA8888) { + throw std::runtime_error("rgb565 encode only accepts rgba8888 images"); + } + if (img.width <= 0 || img.height <= 0 || img.data.size() != static_cast(img.width) * static_cast(img.height) * 4) { + throw std::runtime_error("invalid rgba8888 image for rgb565 encode"); + } +} + +/** @todo remove in future, validation should move to the compile-time graph when it exists */ +static void validateRGB565(const Haio::Image& img) { + if (img.type != Haio::Format::RGB565) { + throw std::runtime_error("rgb565 decode only accepts rgb565 images"); + } + if (img.width <= 0 || img.height <= 0 || img.data.size() != static_cast(img.width) * static_cast(img.height) * 2) { + throw std::runtime_error("invalid rgb565 image"); + } +} + +} + +namespace Haio { + +template <> +Stage Encode() { + return [](const Image& img) { + validateRGBA(img); + std::vector buffer; + Buffer::Copy(img.data, buffer); + return Image{Format::RGB565, img.width, img.height, std::move(buffer)}; + }; +} + +template <> +Stage Decode() { + return [](const Image& img) { + validateRGB565(img); + std::vector buffer; + Buffer::Copy(img.data, buffer); + return Image{Format::RGBA8888, img.width, img.height, std::move(buffer)}; + }; +} + +} diff --git a/library/codecs/webp.cpp b/library/codecs/webp.cpp new file mode 100644 index 0000000..301143a --- /dev/null +++ b/library/codecs/webp.cpp @@ -0,0 +1,48 @@ +#include + +#include +#include + +#include + +namespace Haio { + +template <> +Stage Decode() { + return [](const Image& img) { + int width = 0; + int height = 0; + uint8_t* decoded = WebPDecodeRGBA(img.data.data(), img.data.size(), &width, &height); + if (!decoded || width <= 0 || height <= 0) { + throw std::runtime_error("failed to decode webp"); + } + + std::vector buffer(decoded, decoded + static_cast(width) * static_cast(height) * 4); + WebPFree(decoded); + return Image{Format::RGBA8888, width, height, std::move(buffer)}; + }; +} + +template <> +Stage Encode() { + return [](const Image& img) { + if (img.type != Format::RGBA8888) { + throw std::runtime_error("webp encode only accepts rgba8888 images"); + } + if (img.width <= 0 || img.height <= 0 || img.data.size() != static_cast(img.width) * static_cast(img.height) * 4) { + throw std::runtime_error("invalid rgba8888 image for webp encode"); + } + + uint8_t* out = nullptr; + const auto len = WebPEncodeLosslessRGBA(img.data.data(), img.width, img.height, img.width * 4, &out); + if (!out || len == 0) { + throw std::runtime_error("failed to encode webp"); + } + + std::vector buffer(out, out + len); + WebPFree(out); + return Image{Format::WEBP, img.width, img.height, std::move(buffer)}; + }; +} + +} diff --git a/library/core/format.cpp b/library/core/format.cpp new file mode 100644 index 0000000..57f4386 --- /dev/null +++ b/library/core/format.cpp @@ -0,0 +1,106 @@ +#include + +#include +#include +#include + +namespace { + +std::string lower(std::string_view value) { + std::string out(value); + std::ranges::transform(out, out.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return out; +} + +std::string_view lastExtension(std::string_view path) { + const auto slash = path.find_last_of("/\\"); + const auto dot = path.find_last_of('.'); + if (dot == std::string_view::npos || (slash != std::string_view::npos && dot < slash)) return {}; + return path.substr(dot + 1); +} + +} + +namespace Haio { + +Format formatFromName(std::string_view name) { + const auto key = lower(name); + if (key == "png") return Format::PNG; + if (key == "ppm") return Format::PPM; + if (key == "webp") return Format::WEBP; + if (key == "rgba" || key == "rgba8888") return Format::RGBA8888; + if (key == "rgb" || key == "rgb888") return Format::RGB888; + if (key == "etc1") return Format::ETC1; + if (key == "rgb565") return Format::RGB565; + if (key == "pvr") return Format::PVR; + if (key == "dds") return Format::DDS; + if (key == "ktx") return Format::KTX; + if (key == "ktx2") return Format::KTX2; + if (key == "raw" || key.empty()) return Format::RAW; + throw std::runtime_error("unknown format: " + std::string(name)); +} + +Format formatFromExtension(std::string_view path) { + return formatFromName(lastExtension(path)); +} + +std::string_view formatName(Format format) { + switch (format) { + case Format::RAW: return "raw"; + case Format::PNG: return "png"; + case Format::PPM: return "ppm"; + case Format::WEBP: return "webp"; + case Format::RGBA8888: return "rgba8888"; + case Format::RGB888: return "rgb888"; + case Format::YUV420: return "yuv420"; + case Format::ETC1: return "etc1"; + case Format::RGB565: return "rgb565"; + case Format::PVR: return "pvr"; + case Format::DDS: return "dds"; + case Format::KTX: return "ktx"; + case Format::KTX2: return "ktx2"; + } + return "raw"; +} + +std::string_view extensionFor(Format format) { + return formatName(format); +} + +std::string_view contentTypeFor(Format format) { + switch (format) { + case Format::PNG: return "image/png"; + case Format::PPM: return "image/x-portable-pixmap"; + case Format::WEBP: return "image/webp"; + case Format::KTX: return "image/ktx"; + case Format::KTX2: return "image/ktx2"; + case Format::DDS: return "image/vnd-ms.dds"; + case Format::PVR: return "image/x-pvr"; + default: return "application/octet-stream"; + } +} + +bool isEncodedImageFormat(Format format) { + switch (format) { + case Format::PNG: + case Format::PPM: + case Format::WEBP: + case Format::ETC1: + case Format::RGB565: + case Format::PVR: + case Format::DDS: + case Format::KTX: + case Format::KTX2: + return true; + default: + return false; + } +} + +bool isTransformFormat(Format format) { + return format == Format::RGBA8888 || format == Format::RGB888 || format == Format::RGB565 || format == Format::ETC1; +} + +} diff --git a/library/pipeline/convert.cpp b/library/pipeline/convert.cpp new file mode 100644 index 0000000..cf133f4 --- /dev/null +++ b/library/pipeline/convert.cpp @@ -0,0 +1,106 @@ +#include + +#include + +namespace { + +Haio::Format inferInputFormat(const Haio::Blob& blob, Haio::Format requested) { + if (requested != Haio::Format::RAW) return requested; + if (blob.format != Haio::Format::RAW) return blob.format; + return Haio::formatFromExtension(blob.path); +} + +Haio::Image decodeByFormat(const Haio::Blob& blob, Haio::Format format) { + Haio::Image raw{Haio::Format::RAW, 0, 0, blob.data}; + switch (format) { + case Haio::Format::PNG: return Haio::Decode()(raw); + case Haio::Format::WEBP: return Haio::Decode()(raw); + case Haio::Format::KTX: return Haio::Decode()(raw); + case Haio::Format::KTX2: return Haio::Decode()(raw); + case Haio::Format::PVR: return Haio::Decode()(raw); + case Haio::Format::DDS: return Haio::Decode()(raw); + default: throw std::runtime_error("unsupported decode format: " + std::string(Haio::formatName(format))); + } +} + +Haio::Blob blobFromImage(Haio::Image image, std::string path = {}) { + auto format = image.type; + return Haio::Blob{format, std::string(Haio::contentTypeFor(format)), std::move(path), std::move(image.data)}; +} + +} + +namespace Haio { + +Image decodeBlob(const Blob& blob, Format requested) { + return decodeByFormat(blob, inferInputFormat(blob, requested)); +} + +Blob encodeImage(const Image& image, Format format, std::string path) { + switch (format) { + case Format::PNG: return blobFromImage(Encode()(image), std::move(path)); + case Format::PPM: return blobFromImage(Encode()(image), std::move(path)); + case Format::WEBP: return blobFromImage(Encode()(image), std::move(path)); + case Format::ETC1: return blobFromImage(Encode()(image), std::move(path)); + case Format::RGB565: return blobFromImage(Encode()(image), std::move(path)); + case Format::KTX: return blobFromImage(Encode()(image), std::move(path)); + case Format::KTX2: return blobFromImage(Encode()(image), std::move(path)); + case Format::PVR: return blobFromImage(Encode()(image), std::move(path)); + case Format::DDS: return blobFromImage(Encode()(image), std::move(path)); + default: throw std::runtime_error("unsupported encode format: " + std::string(formatName(format))); + } +} + +Blob runPipeline(Blob input, const Pipeline& pipeline) { + bool hasImage = false; + Image image{}; + Format explicitDecode = Format::RAW; + Format outputFormat = Format::RAW; + + auto ensureImage = [&] { + if (!hasImage) { + image = decodeBlob(input, explicitDecode); + hasImage = true; + } + }; + + for (const auto& token : pipeline.tokens()) { + switch (token.kind) { + case TokenKind::Source: + break; + case TokenKind::DecodeAuto: + explicitDecode = Format::RAW; + ensureImage(); + break; + case TokenKind::Decode: + explicitDecode = token.format; + ensureImage(); + break; + case TokenKind::Crop: + ensureImage(); + image = cropImage(image, token.rect); + break; + case TokenKind::Resize: + ensureImage(); + image = resizeImage(image, token.size); + break; + case TokenKind::Radius: + ensureImage(); + image = roundImageCorners(image, token.radius); + break; + case TokenKind::Encode: + outputFormat = token.format; + break; + } + } + + if (outputFormat == Format::RAW) { + if (!hasImage) return input; + return Blob{image.type, std::string(contentTypeFor(image.type)), input.path, std::move(image.data)}; + } + + ensureImage(); + return encodeImage(image, outputFormat, input.path); +} + +} diff --git a/library/pipeline/pipeline.cpp b/library/pipeline/pipeline.cpp new file mode 100644 index 0000000..c5fce40 --- /dev/null +++ b/library/pipeline/pipeline.cpp @@ -0,0 +1,134 @@ +#include + +#include +#include + +#include +#include + +namespace { + +int parseInt(std::string_view value) { + int out = 0; + const auto* begin = value.data(); + const auto* end = value.data() + value.size(); + const auto [ptr, ec] = std::from_chars(begin, end, out); + if (ec != std::errc{} || ptr != end) throw std::runtime_error("invalid integer: " + std::string(value)); + return out; +} + +std::vector split(std::string_view value, char sep) { + std::vector parts; + while (true) { + const auto pos = value.find(sep); + parts.push_back(value.substr(0, pos)); + if (pos == std::string_view::npos) break; + value.remove_prefix(pos + 1); + } + return parts; +} + +Haio::Size parseSize(std::string_view value) { + const auto sep = value.find_first_of("xX"); + if (sep == std::string_view::npos) { + const auto side = parseInt(value); + return {side, side}; + } + return {parseInt(value.substr(0, sep)), parseInt(value.substr(sep + 1))}; +} + +Haio::Rect parseRect(std::string_view value) { + const auto parts = split(value, ','); + if (parts.size() != 4) throw std::runtime_error("crop expects x,y,width,height"); + return {parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), parseInt(parts[3])}; +} + +} + +namespace Haio { + +Pipeline& Pipeline::operator|=(Token token) { + tokens_.push_back(std::move(token)); + return *this; +} + +const std::vector& Pipeline::tokens() const { + return tokens_; +} + +namespace Tokens { +Token Source(std::string bucket, std::string path) { + Token token{TokenKind::Source}; + token.bucket = std::move(bucket); + token.path = std::move(path); + return token; +} + +Token DecodeAuto() { + return Token{TokenKind::DecodeAuto}; +} + +Token Decode(Format format) { + Token token{TokenKind::Decode}; + token.format = format; + return token; +} + +Token Crop(Rect rect) { + Token token{TokenKind::Crop}; + token.rect = rect; + return token; +} + +Token Resize(Size size) { + Token token{TokenKind::Resize}; + token.size = size; + return token; +} + +Token Radius(int radius) { + Token token{TokenKind::Radius}; + token.radius = radius; + return token; +} + +Token Encode(Format format) { + Token token{TokenKind::Encode}; + token.format = format; + return token; +} +} + +std::unordered_map parseQueryMap(std::string_view query) { + std::unordered_map out; + if (!query.empty() && query.front() == '?') query.remove_prefix(1); + if (query.empty()) return out; + + std::string target = "/?"; + target.append(query); + + auto parsed = boost::urls::parse_origin_form(target); + if (!parsed) throw std::runtime_error("invalid query: " + parsed.error().message()); + + boost::urls::encoding_opts opts; + opts.space_as_plus = true; + for (const auto& param : parsed->params(opts)) { + if (!param.key.empty()) out[param.key] = param.has_value ? param.value : std::string{}; + } + return out; +} + +std::vector parseQueryTokens(std::string_view query) { + const auto values = parseQueryMap(query); + std::vector tokens; + + if (auto it = values.find("crop"); it != values.end()) tokens.push_back(Tokens::Crop(parseRect(it->second))); + if (auto it = values.find("size"); it != values.end()) tokens.push_back(Tokens::Resize(parseSize(it->second))); + if (auto it = values.find("resize"); it != values.end()) tokens.push_back(Tokens::Resize(parseSize(it->second))); + if (auto it = values.find("radius"); it != values.end()) tokens.push_back(Tokens::Radius(parseInt(it->second))); + if (auto it = values.find("format"); it != values.end()) tokens.push_back(Tokens::Encode(formatFromName(it->second))); + + return tokens; +} + +} diff --git a/library/transforms/image.cpp b/library/transforms/image.cpp new file mode 100644 index 0000000..7a4eb78 --- /dev/null +++ b/library/transforms/image.cpp @@ -0,0 +1,92 @@ +#include + +#include +#include +#include + +namespace { + +void requireRgba(const Haio::Image& image, std::string_view op) { + if (image.type != Haio::Format::RGBA8888) { + throw std::runtime_error(std::string(op) + " expects rgba8888 image"); + } + const auto expected = static_cast(image.width) * static_cast(image.height) * 4; + if (image.width <= 0 || image.height <= 0 || image.data.size() != expected) { + throw std::runtime_error(std::string(op) + " got invalid image data"); + } +} + +} + +namespace Haio { + +Image cropImage(const Image& image, Rect rect) { + requireRgba(image, "crop"); + const int x0 = std::clamp(rect.x, 0, image.width); + const int y0 = std::clamp(rect.y, 0, image.height); + const int x1 = std::clamp(rect.x + rect.width, x0, image.width); + const int y1 = std::clamp(rect.y + rect.height, y0, image.height); + const int width = x1 - x0; + const int height = y1 - y0; + + std::vector out(static_cast(width) * static_cast(height) * 4); + for (int y = 0; y < height; y++) { + const auto src = (static_cast(y0 + y) * static_cast(image.width) + static_cast(x0)) * 4; + const auto dst = static_cast(y) * static_cast(width) * 4; + std::copy_n(image.data.data() + src, static_cast(width) * 4, out.data() + dst); + } + return Image{Format::RGBA8888, width, height, std::move(out)}; +} + +Image resizeImage(const Image& image, Size size) { + requireRgba(image, "resize"); + if (size.width <= 0 && size.height <= 0) throw std::runtime_error("resize expects a positive size"); + + int width = size.width; + int height = size.height; + if (width <= 0) width = std::max(1, image.width * height / image.height); + if (height <= 0) height = std::max(1, image.height * width / image.width); + + std::vector out(static_cast(width) * static_cast(height) * 4); + for (int y = 0; y < height; y++) { + const int sy = std::min(image.height - 1, y * image.height / height); + for (int x = 0; x < width; x++) { + const int sx = std::min(image.width - 1, x * image.width / width); + const auto src = (static_cast(sy) * static_cast(image.width) + static_cast(sx)) * 4; + const auto dst = (static_cast(y) * static_cast(width) + static_cast(x)) * 4; + std::copy_n(image.data.data() + src, 4, out.data() + dst); + } + } + return Image{Format::RGBA8888, width, height, std::move(out)}; +} + +Image roundImageCorners(const Image& image, int radius) { + requireRgba(image, "radius"); + if (radius <= 0) return image; + + Image out = image; + const int r = std::min(radius, std::min(image.width, image.height) / 2); + const int r2 = r * r; + + auto maskCorner = [&](int x, int y, int cx, int cy) { + const int dx = x - cx; + const int dy = y - cy; + if (dx * dx + dy * dy > r2) { + const auto off = (static_cast(y) * static_cast(out.width) + static_cast(x)) * 4 + 3; + out.data[off] = 0; + } + }; + + for (int y = 0; y < r; y++) { + for (int x = 0; x < r; x++) { + maskCorner(x, y, r - 1, r - 1); + maskCorner(out.width - 1 - x, y, out.width - r, r - 1); + maskCorner(x, out.height - 1 - y, r - 1, out.height - r); + maskCorner(out.width - 1 - x, out.height - 1 - y, out.width - r, out.height - r); + } + } + + return out; +} + +} diff --git a/library/vulkan/runtime.cpp b/library/vulkan/runtime.cpp deleted file mode 100644 index 706f26a..0000000 --- a/library/vulkan/runtime.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "vm.hpp" -#include -#include - -namespace Haio { - - Vulkan::Vulkan(): vm(new Vm{}) { - if (volkInitialize() != VK_SUCCESS) { - throw std::runtime_error("failed to load vulkan library"); - } - - VkApplicationInfo appInfo{}; - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "Haio"; - appInfo.apiVersion = VK_API_VERSION_1_2; - - VkInstanceCreateInfo createInfo{}; - createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - createInfo.pApplicationInfo = &appInfo; - - if (vkCreateInstance(&createInfo, nullptr, &vm->instance) != VK_SUCCESS) { - throw std::runtime_error("failed to create instance"); - } - volkLoadInstance(vm->instance); - - uint32_t deviceCount = 0; - vkEnumeratePhysicalDevices(vm->instance, &deviceCount, nullptr); - std::vector devices(deviceCount); - vkEnumeratePhysicalDevices(vm->instance, &deviceCount, devices.data()); - - /** @todo choose beter GPU */ - vm->physicalDevice = devices[0]; - - uint32_t queueFamilyCount = 0; - vkGetPhysicalDeviceQueueFamilyProperties(vm->physicalDevice, &queueFamilyCount, nullptr); - std::vector queueFamilies(queueFamilyCount); - vkGetPhysicalDeviceQueueFamilyProperties(vm->physicalDevice, &queueFamilyCount, queueFamilies.data()); - - int computeFamilyIndex = -1; - for (uint32_t i = 0; i < queueFamilyCount; i++) { - if (queueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { - computeFamilyIndex = i; - break; - } - } - if (computeFamilyIndex == -1) { - throw std::runtime_error("no compute queue found"); - } - - float queuePriority = 1.0f; - VkDeviceQueueCreateInfo queueCreateInfo{}; - queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queueCreateInfo.queueFamilyIndex = computeFamilyIndex; - queueCreateInfo.queueCount = 1; - queueCreateInfo.pQueuePriorities = &queuePriority; - - VkPhysicalDeviceShaderFloat16Int8Features int8Features{}; - int8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES; - int8Features.shaderInt8 = VK_TRUE; - - VkDeviceCreateInfo deviceCreateInfo{}; - deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - deviceCreateInfo.pNext = &int8Features; - deviceCreateInfo.queueCreateInfoCount = 1; - deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; - - if (vkCreateDevice(vm->physicalDevice, &deviceCreateInfo, nullptr, &vm->device) != VK_SUCCESS) { - throw std::runtime_error("failed to create device"); - } - volkLoadDevice(vm->device); - - vm->computeFamilyIndex = computeFamilyIndex; - vkGetDeviceQueue(vm->device, computeFamilyIndex, 0, &vm->computeQueue); - } - - Vulkan::~Vulkan(){ - if (vm->device) { - vkDestroyDevice(vm->device, nullptr); - vm->device = VK_NULL_HANDLE; - } - if (vm->instance) { - vkDestroyInstance(vm->instance, nullptr); - vm->instance = VK_NULL_HANDLE; - } - - delete vm; - } - - Vulkan& Vulkan::getInstance() { - static Vulkan instance; - return instance; - } -} diff --git a/library/vulkan/transformers.cpp b/library/vulkan/transformers.cpp deleted file mode 100644 index ce2dea2..0000000 --- a/library/vulkan/transformers.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "vm.hpp" -#include -#include - -namespace Haio { - -static uint32_t findMemoryType(VkPhysicalDevice physDev, uint32_t filter, VkMemoryPropertyFlags props) { - VkPhysicalDeviceMemoryProperties memProps; - vkGetPhysicalDeviceMemoryProperties(physDev, &memProps); - for (uint32_t i = 0; i < memProps.memoryTypeCount; i++) { - if ((filter & (1u << i)) && (memProps.memoryTypes[i].propertyFlags & props) == props) - return i; - } - throw std::runtime_error("no suitable memory type"); -} - -static void makeBuffer(VkDevice device, VkPhysicalDevice physDev, VkDeviceSize size, - VkBuffer& buf, VkDeviceMemory& mem) { - VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - info.size = size; - info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - vkCreateBuffer(device, &info, nullptr, &buf); - - VkMemoryRequirements req; - vkGetBufferMemoryRequirements(device, buf, &req); - - VkMemoryAllocateInfo allocInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocInfo.allocationSize = req.size; - allocInfo.memoryTypeIndex = findMemoryType(physDev, req.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - vkAllocateMemory(device, &allocInfo, nullptr, &mem); - vkBindBufferMemory(device, buf, mem, 0); -} - -void Vulkan::transformRGBAtoRGB(const std::vector& input, std::vector& output) { - const size_t pixelCount = input.size() / 4; - const VkDeviceSize inSize = pixelCount * 4; - const VkDeviceSize outSize = pixelCount * 3; - - VkBuffer inBuf, outBuf; - VkDeviceMemory inMem, outMem; - makeBuffer(vm->device, vm->physicalDevice, inSize, inBuf, inMem); - makeBuffer(vm->device, vm->physicalDevice, outSize, outBuf, outMem); - - void* mapped; - vkMapMemory(vm->device, inMem, 0, inSize, 0, &mapped); - std::memcpy(mapped, input.data(), inSize); - vkUnmapMemory(vm->device, inMem); - - VkShaderModuleCreateInfo shaderInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - shaderInfo.codeSize = rgba8888_to_rgb888_len; - shaderInfo.pCode = reinterpret_cast(rgba8888_to_rgb888); - VkShaderModule shaderMod; - vkCreateShaderModule(vm->device, &shaderInfo, nullptr, &shaderMod); - - VkDescriptorSetLayoutBinding bindings[2]{}; - for (int b = 0; b < 2; b++) { - bindings[b].binding = b; - bindings[b].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - bindings[b].descriptorCount = 1; - bindings[b].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - } - VkDescriptorSetLayout descLayout; - VkDescriptorSetLayoutCreateInfo descLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - descLayoutInfo.bindingCount = 2; - descLayoutInfo.pBindings = bindings; - vkCreateDescriptorSetLayout(vm->device, &descLayoutInfo, nullptr, &descLayout); - - VkPipelineLayout pipeLayout; - VkPipelineLayoutCreateInfo pipeLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - pipeLayoutInfo.setLayoutCount = 1; - pipeLayoutInfo.pSetLayouts = &descLayout; - vkCreatePipelineLayout(vm->device, &pipeLayoutInfo, nullptr, &pipeLayout); - - VkComputePipelineCreateInfo pipeInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - pipeInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - pipeInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - pipeInfo.stage.module = shaderMod; - pipeInfo.stage.pName = "main"; - pipeInfo.layout = pipeLayout; - VkPipeline pipeline; - vkCreateComputePipelines(vm->device, VK_NULL_HANDLE, 1, &pipeInfo, nullptr, &pipeline); - - VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; - VkDescriptorPool descPool; - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - vkCreateDescriptorPool(vm->device, &poolInfo, nullptr, &descPool); - - VkDescriptorSet descSet; - VkDescriptorSetAllocateInfo descAllocInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - descAllocInfo.descriptorPool = descPool; - descAllocInfo.descriptorSetCount = 1; - descAllocInfo.pSetLayouts = &descLayout; - vkAllocateDescriptorSets(vm->device, &descAllocInfo, &descSet); - - VkDescriptorBufferInfo bufInfos[2] = {{inBuf, 0, inSize}, {outBuf, 0, outSize}}; - VkWriteDescriptorSet writes[2]{}; - for (int b = 0; b < 2; b++) { - writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[b].dstSet = descSet; - writes[b].dstBinding = b; - writes[b].descriptorCount = 1; - writes[b].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[b].pBufferInfo = &bufInfos[b]; - } - vkUpdateDescriptorSets(vm->device, 2, writes, 0, nullptr); - - VkCommandPool cmdPool; - VkCommandPoolCreateInfo cmdPoolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; - cmdPoolInfo.queueFamilyIndex = vm->computeFamilyIndex; - vkCreateCommandPool(vm->device, &cmdPoolInfo, nullptr, &cmdPool); - - VkCommandBuffer cmd; - VkCommandBufferAllocateInfo cmdAllocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - cmdAllocInfo.commandPool = cmdPool; - cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cmdAllocInfo.commandBufferCount = 1; - vkAllocateCommandBuffers(vm->device, &cmdAllocInfo, &cmd); - - VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &beginInfo); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeLayout, 0, 1, &descSet, 0, nullptr); - vkCmdDispatch(cmd, static_cast((pixelCount + 31) / 32), 1, 1); - vkEndCommandBuffer(cmd); - - VkFence fence; - VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - vkCreateFence(vm->device, &fenceInfo, nullptr, &fence); - - VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO}; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &cmd; - vkQueueSubmit(vm->computeQueue, 1, &submitInfo, fence); - vkWaitForFences(vm->device, 1, &fence, VK_TRUE, UINT64_MAX); - - const size_t outOffset = output.size(); - output.resize(outOffset + outSize); - vkMapMemory(vm->device, outMem, 0, outSize, 0, &mapped); - std::memcpy(output.data() + outOffset, mapped, outSize); - vkUnmapMemory(vm->device, outMem); - - vkDestroyFence(vm->device, fence, nullptr); - vkDestroyCommandPool(vm->device, cmdPool, nullptr); - vkDestroyDescriptorPool(vm->device, descPool, nullptr); - vkDestroyPipeline(vm->device, pipeline, nullptr); - vkDestroyPipelineLayout(vm->device, pipeLayout, nullptr); - vkDestroyDescriptorSetLayout(vm->device, descLayout, nullptr); - vkDestroyShaderModule(vm->device, shaderMod, nullptr); - vkFreeMemory(vm->device, outMem, nullptr); - vkDestroyBuffer(vm->device, outBuf, nullptr); - vkFreeMemory(vm->device, inMem, nullptr); - vkDestroyBuffer(vm->device, inBuf, nullptr); -} - -} diff --git a/library/vulkan/vm.hpp b/library/vulkan/vm.hpp deleted file mode 100644 index 87bd193..0000000 --- a/library/vulkan/vm.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include -#include -#include - -struct Haio::Vulkan::Vm { - VkInstance instance; - VkPhysicalDevice physicalDevice; - VkDevice device; - VkQueue computeQueue; - uint32_t computeFamilyIndex; -}; diff --git a/scripts/replace.cpp b/scripts/replace.cpp deleted file mode 100644 index d186bed..0000000 --- a/scripts/replace.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include -#include -#include - -int main(int argc, char* argv[]) { - if (argc != 4) { - std::cerr << "Usage: replace \n"; - return 1; - } - - std::string path = argv[1]; - std::regex pattern(argv[2]); - std::string replacement = argv[3]; - - std::ifstream in(path); - if (!in) { - std::cerr << "Cannot open: " << path << "\n"; - return 1; - } - - std::string content((std::istreambuf_iterator(in)), - std::istreambuf_iterator()); - in.close(); - - std::string result = std::regex_replace(content, pattern, replacement); - - std::ofstream out(path); - if (!out) { - std::cerr << "Cannot write: " << path << "\n"; - return 1; - } - out << result; - - return 0; -} \ No newline at end of file diff --git a/scripts/xxd.cpp b/scripts/xxd.cpp deleted file mode 100644 index 7053f6b..0000000 --- a/scripts/xxd.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include -#include -#include -#include -#include - -std::string basename(const std::string& path) { - auto pos = path.find_last_of("/\\"); - std::string name = (pos == std::string::npos) ? path : path.substr(pos + 1); - auto dot = name.find_last_of('.'); - if (dot != std::string::npos) name = name.substr(0, dot); - return name; -} - -int main(int argc, char** argv) { - if (argc < 2) return 1; - - std::ifstream file(argv[1], std::ios::binary); - if (!file) return 1; - - std::vector buffer( - (std::istreambuf_iterator(file)), - std::istreambuf_iterator() - ); - - constexpr int perline = 12; - std::string varname = basename(argv[1]); - - std::cout << "const unsigned char " << varname << "[] = {\n"; - - for (size_t i = 0; i < buffer.size(); ++i) { - if (i % perline == 0) std::cout << " "; - - std::cout << "0x" - << std::hex << std::setw(2) << std::setfill('0') - << static_cast(buffer[i]); - - if (i + 1 < buffer.size()) std::cout << ", "; - if ((i + 1) % perline == 0) std::cout << "\n"; - } - - if (buffer.size() % perline != 0) std::cout << "\n"; - - std::cout << "};\n"; - std::cout << "const unsigned int " << varname << "_len = " - << std::dec << buffer.size() << ";\n"; - - return 0; -} diff --git a/shaders/rgba8888_to_rgb888.glsl b/shaders/rgba8888_to_rgb888.glsl deleted file mode 100644 index fc757ba..0000000 --- a/shaders/rgba8888_to_rgb888.glsl +++ /dev/null @@ -1,19 +0,0 @@ -#version 450 -#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require - -layout(local_size_x = 32) in; - -layout(binding = 0) readonly buffer Input { - uint8_t inBytes[]; -}; - -layout(binding = 1) writeonly buffer Output { - uint8_t outBytes[]; -}; - -void main() { - uint idx = gl_GlobalInvocationID.x; - outBytes[idx * 3 + 0] = inBytes[idx * 4 + 0]; - outBytes[idx * 3 + 1] = inBytes[idx * 4 + 1]; - outBytes[idx * 3 + 2] = inBytes[idx * 4 + 2]; -} diff --git a/source/main.cpp b/source/main.cpp index 1033d72..5c3f83c 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,16 +1,140 @@ -#include "haio.hpp" - -auto main() -> int { - auto &vulkan = Haio::Vulkan::getInstance(); - - /*auto window = Haio::CreateWindow("janela", 800, 600); - - window->init(); - - while (!window->shouldClose()) { - window->pollEvents(); - } - - window->shutdown();*/ - return 0; -} +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +std::vector readFile(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + if (!in) throw std::runtime_error("cannot open input: " + path.string()); + return {(std::istreambuf_iterator(in)), std::istreambuf_iterator()}; +} + +void writeFile(const std::filesystem::path& path, const std::vector& data) { + std::ofstream out(path, std::ios::binary); + if (!out) throw std::runtime_error("cannot open output: " + path.string()); + out.write(reinterpret_cast(data.data()), static_cast(data.size())); +} + +std::string consumeValue(int& i, int argc, char* argv[], std::string_view arg, std::string_view name) { + const std::string prefix = std::string(name) + "="; + if (arg.starts_with(prefix)) return std::string(arg.substr(prefix.size())); + if (i + 1 >= argc) throw std::runtime_error("missing value for " + std::string(name)); + return argv[++i]; +} + +int convertCommand(int argc, char* argv[]) { + if (argc < 3) { + std::cerr << "usage: haio convert [--crop x,y,w,h] [--size wxh] [--radius r] [--format fmt]\n"; + return 1; + } + + const std::filesystem::path inputPath = argv[1]; + const std::filesystem::path outputPath = argv[2]; + std::string query; + auto appendQuery = [&](std::string key, std::string value) { + if (!query.empty()) query += '&'; + query += std::move(key) + '=' + std::move(value); + }; + + bool explicitFormat = false; + for (int i = 3; i < argc; i++) { + const std::string_view arg = argv[i]; + if (arg == "--crop" || arg.starts_with("--crop=")) appendQuery("crop", consumeValue(i, argc, argv, arg, "--crop")); + else if (arg == "--size" || arg.starts_with("--size=")) appendQuery("size", consumeValue(i, argc, argv, arg, "--size")); + else if (arg == "--resize" || arg.starts_with("--resize=")) appendQuery("resize", consumeValue(i, argc, argv, arg, "--resize")); + else if (arg == "--radius" || arg.starts_with("--radius=")) appendQuery("radius", consumeValue(i, argc, argv, arg, "--radius")); + else if (arg == "--format" || arg.starts_with("--format=")) { + appendQuery("format", consumeValue(i, argc, argv, arg, "--format")); + explicitFormat = true; + } else { + throw std::runtime_error("unknown convert option: " + std::string(arg)); + } + } + + Haio::Blob input; + input.path = inputPath.string(); + input.format = Haio::formatFromExtension(input.path); + input.contentType = Haio::contentTypeFor(input.format); + input.data = readFile(inputPath); + + Haio::Pipeline pipeline; + pipeline |= Haio::Tokens::Source("file", input.path); + for (auto token : Haio::parseQueryTokens(query)) pipeline |= std::move(token); + if (!explicitFormat) pipeline |= Haio::Tokens::Encode(Haio::formatFromExtension(outputPath.string())); + + const auto output = Haio::runPipeline(std::move(input), pipeline); + writeFile(outputPath, output.data); + return 0; +} + +int cdnCommand(int argc, char* argv[]) { + std::filesystem::path configPath; + std::filesystem::path root = "."; + auto config = Haio::Cdn::loadConfig({}, root); + + for (int i = 1; i < argc; i++) { + const std::string_view arg = argv[i]; + if (arg == "--port" || arg.starts_with("--port=")) config.port = static_cast(std::stoi(consumeValue(i, argc, argv, arg, "--port"))); + else if (arg == "--host" || arg.starts_with("--host=")) config.host = consumeValue(i, argc, argv, arg, "--host"); + else if (arg == "--config" || arg.starts_with("--config=")) configPath = consumeValue(i, argc, argv, arg, "--config"); + else if (arg == "--root" || arg.starts_with("--root=")) root = consumeValue(i, argc, argv, arg, "--root"); + else throw std::runtime_error("unknown cdn option: " + std::string(arg)); + } + + if (!configPath.empty()) { + const auto host = config.host; + const auto port = config.port; + config = Haio::Cdn::loadConfig(configPath, root); + config.host = host; + config.port = port; + } else { + config.buckets["file"].root = root; + } + + boost::asio::io_context io; + boost::asio::signal_set signals(io, SIGINT, SIGTERM); + signals.async_wait([&](auto, auto) { io.stop(); }); + boost::asio::co_spawn(io, Haio::Cdn::runServer(std::move(config)), boost::asio::detached); + io.run(); + return 0; +} + +void printHelp() { + std::cout << "usage:\n" + << " haio convert [--crop x,y,w,h] [--size wxh] [--radius r] [--format fmt]\n" + << " haio cdn [--host 0.0.0.0] [--port 8080] [--root .] [--config haio.toml]\n"; +} + +} + +auto main(int argc, char* argv[]) -> int { + try { + if (argc < 2) { + printHelp(); + return 1; + } + + const std::string_view command = argv[1]; + if (command == "convert") return convertCommand(argc - 1, argv + 1); + if (command == "cdn") return cdnCommand(argc - 1, argv + 1); + if (command == "help" || command == "--help" || command == "-h") { + printHelp(); + return 0; + } + + std::cerr << "unknown command: " << command << "\n"; + printHelp(); + return 1; + } catch (const std::exception& err) { + std::cerr << "error: " << err.what() << "\n"; + return 1; + } +} diff --git a/tests/test_gpu_formats_roundtrip.cpp b/tests/test_gpu_formats_roundtrip.cpp new file mode 100644 index 0000000..22e3138 --- /dev/null +++ b/tests/test_gpu_formats_roundtrip.cpp @@ -0,0 +1,90 @@ +#include + +#include +#include +#include +#include + +namespace { + +Haio::Image makeImage(int width, int height) { + std::vector data(static_cast(width) * static_cast(height) * 4); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + const auto off = (static_cast(y) * static_cast(width) + static_cast(x)) * 4; + data[off + 0] = static_cast(x * 31 + y * 7); + data[off + 1] = static_cast(x * 5 + y * 23); + data[off + 2] = static_cast(x * 11 + y * 13); + data[off + 3] = 255; + } + } + return Haio::Image{Haio::Format::RGBA8888, width, height, std::move(data)}; +} + +double psnr(const Haio::Image& a, const Haio::Image& b) { + double mse = 0.0; + size_t count = 0; + for (size_t i = 0; i < a.data.size(); i += 4) { + for (int c = 0; c < 3; c++) { + const double d = static_cast(a.data[i + c]) - static_cast(b.data[i + c]); + mse += d * d; + count++; + } + } + mse /= static_cast(count); + return mse == 0.0 ? 99.0 : 10.0 * std::log10((255.0 * 255.0) / mse); +} + +void require(bool ok, const char* message) { + if (!ok) throw std::runtime_error(message); +} + +template +void testContainer(const Haio::Image& img) { + auto container = Haio::Encode()(img); + auto restored = Haio::Decode()(container); + require(restored.type == img.type, "container format mismatch"); + require(restored.width == img.width && restored.height == img.height, "container dimensions mismatch"); + require(restored.data == img.data, "container payload mismatch"); +} + +} + +auto main() -> int { + try { + const auto rgba = makeImage(8, 8); + const auto rgb565 = Haio::Encode()(rgba); + const auto rgb565Rgba = Haio::Decode()(rgb565); + const auto etc1 = Haio::Encode()(rgba); + + require(psnr(rgba, rgb565Rgba) > 40.0, "rgb565 psnr is too low"); + + testContainer(rgba); + testContainer(rgb565); + testContainer(etc1); + + testContainer(rgba); + testContainer(rgb565); + testContainer(etc1); + + testContainer(rgba); + testContainer(rgb565); + + testContainer(rgba); + testContainer(rgb565); + testContainer(etc1); + + bool ddsRejectedEtc1 = false; + try { + (void)Haio::Encode()(etc1); + } catch (const std::runtime_error&) { + ddsRejectedEtc1 = true; + } + require(ddsRejectedEtc1, "dds should reject etc1"); + + return 0; + } catch (const std::exception& err) { + std::cerr << err.what() << "\n"; + return 1; + } +} diff --git a/tests/test_image_convert_png_to_etc1_ppm.cpp b/tests/test_image_convert_png_to_etc1_ppm.cpp new file mode 100644 index 0000000..97010d0 --- /dev/null +++ b/tests/test_image_convert_png_to_etc1_ppm.cpp @@ -0,0 +1,18 @@ +#include + +auto main(int argc, char* argv[]) -> int { + if (argc != 3) { + std::cerr << "Usage: convert_png_2_etc1_ppm \n"; + return 1; + } + + auto pipe = Haio::Decode() + | Haio::Encode() + | Haio::Decode() + | Haio::Encode(); + auto input = std::ifstream(argv[1], std::ios::binary); + auto output = std::ofstream(argv[2], std::ios::binary); + + input >> pipe >> output; + return 0; +} diff --git a/tests/test_image_etc1_roundtrip.cpp b/tests/test_image_etc1_roundtrip.cpp new file mode 100644 index 0000000..0ef99e6 --- /dev/null +++ b/tests/test_image_etc1_roundtrip.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include +#include + +namespace { + +Haio::Image makeImage(int width, int height, auto pixel) { + std::vector data(static_cast(width) * static_cast(height) * 4); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + const auto [r, g, b, a] = pixel(x, y); + const auto offset = (static_cast(y) * static_cast(width) + static_cast(x)) * 4; + data[offset + 0] = r; + data[offset + 1] = g; + data[offset + 2] = b; + data[offset + 3] = a; + } + } + return Haio::Image{Haio::Format::RGBA8888, width, height, std::move(data)}; +} + +bool hasOpaqueAlpha(const Haio::Image& img) { + for (size_t i = 3; i < img.data.size(); i += 4) { + if (img.data[i] != 255) return false; + } + return true; +} + +double psnr(const Haio::Image& a, const Haio::Image& b) { + double mse = 0.0; + size_t count = 0; + for (size_t i = 0; i < a.data.size(); i += 4) { + for (int c = 0; c < 3; c++) { + const double diff = static_cast(a.data[i + c]) - static_cast(b.data[i + c]); + mse += diff * diff; + count++; + } + } + mse /= static_cast(count); + return mse == 0.0 ? 99.0 : 10.0 * std::log10((255.0 * 255.0) / mse); +} + +bool throwsRuntimeError(auto func) { + try { + func(); + return false; + } catch (const std::runtime_error&) { + return true; + } +} + +void require(bool condition, const char* message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +} // namespace + +auto main() -> int { + try { + auto encode = Haio::Encode(); + auto decode = Haio::Decode(); + + auto solid = makeImage(4, 4, [](int, int) { + return std::tuple{255, 0, 0, 255}; + }); + auto solidEtc1 = encode(solid); + auto solidRoundtrip = decode(solidEtc1); + require(solidEtc1.type == Haio::Format::ETC1, "solid encode should output etc1"); + require(solidEtc1.data.size() == 8, "solid 4x4 etc1 should be one block"); + require(solidRoundtrip.type == Haio::Format::RGBA8888, "solid decode should output rgba8888"); + require(solidRoundtrip.width == 4 && solidRoundtrip.height == 4, "solid roundtrip should keep dimensions"); + require(hasOpaqueAlpha(solidRoundtrip), "solid roundtrip alpha should be opaque"); + + auto gradient = makeImage(16, 16, [](int x, int y) { + return std::tuple{ + static_cast(32 + x * 8), + static_cast(48 + y * 8), + static_cast(96 + (x + y) * 2), + 255 + }; + }); + auto gradientRoundtrip = decode(encode(gradient)); + const double gradientPsnr = psnr(gradient, gradientRoundtrip); + std::cout << "gradient psnr: " << gradientPsnr << " db\n"; + require(gradientPsnr > 30.0, "gradient psnr is too low"); + + auto odd = makeImage(5, 7, [](int x, int y) { + return std::tuple{ + static_cast(40 + x * 20), + static_cast(60 + y * 15), + static_cast(120 + x * 3 + y * 2), + 255 + }; + }); + auto oddEtc1 = encode(odd); + auto oddRoundtrip = decode(oddEtc1); + require(oddEtc1.data.size() == 2 * 2 * 8, "5x7 etc1 should be four blocks"); + require(oddRoundtrip.width == 5 && oddRoundtrip.height == 7, "odd roundtrip should keep logical dimensions"); + + require(throwsRuntimeError([&] { + decode(Haio::Image{Haio::Format::ETC1, 0, 4, std::vector(8)}); + }), "decode with zero width should throw"); + require(throwsRuntimeError([&] { + decode(Haio::Image{Haio::Format::ETC1, 4, 4, std::vector(7)}); + }), "decode with invalid data size should throw"); + require(throwsRuntimeError([&] { + encode(Haio::Image{Haio::Format::RGB888, 4, 4, std::vector(4 * 4 * 3)}); + }), "encode with invalid format should throw"); + + return 0; + } catch (const std::exception& err) { + std::cerr << err.what() << "\n"; + return 1; + } +} diff --git a/tests/test_url_parsing.cpp b/tests/test_url_parsing.cpp new file mode 100644 index 0000000..23d4244 --- /dev/null +++ b/tests/test_url_parsing.cpp @@ -0,0 +1,30 @@ +#include + +#include +#include + +int main() { + const auto values = Haio::parseQueryMap("?size=16x16&format=webp&name=hello+world&file=a%2Fb.png"); + assert(values.at("size") == "16x16"); + assert(values.at("format") == "webp"); + assert(values.at("name") == "hello world"); + assert(values.at("file") == "a/b.png"); + + const auto tokens = Haio::parseQueryTokens("resize=8x4&radius=2"); + assert(tokens.size() == 2); + assert(tokens[0].kind == Haio::TokenKind::Resize); + assert(tokens[0].size.width == 8); + assert(tokens[0].size.height == 4); + assert(tokens[1].kind == Haio::TokenKind::Radius); + assert(tokens[1].radius == 2); + + bool failed = false; + try { + (void)Haio::parseQueryMap("size=%zz"); + } catch (const std::runtime_error&) { + failed = true; + } + assert(failed); + + return 0; +}