diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index 6764be52c..cecc30b90 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries(BabylonNativeJNI GraphicsDevice NativeCamera NativeEngine + DataStream NativeInput NativeOptimizations NativeXr diff --git a/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp b/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp index c7a2b6be9..aec3b23f6 100644 --- a/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp +++ b/Apps/Playground/Android/BabylonNative/src/main/cpp/BabylonNativeJNI.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -123,6 +124,7 @@ extern "C" nativeInput = &Babylon::Plugins::NativeInput::CreateForJavaScript(env); Babylon::Plugins::NativeCamera::Initialize(env); + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::Window::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 2e4003db9..424874d40 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -132,6 +132,7 @@ target_link_libraries(Playground PRIVATE GraphicsDevice PRIVATE NativeCapture PRIVATE NativeEngine + PRIVATE DataStream PRIVATE NativeInput PRIVATE NativeOptimizations PRIVATE ScriptLoader diff --git a/Apps/Playground/UWP/App.cpp b/Apps/Playground/UWP/App.cpp index 17d1dd21b..3bacc841f 100644 --- a/Apps/Playground/UWP/App.cpp +++ b/Apps/Playground/UWP/App.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -393,7 +394,7 @@ void App::RestartRuntime(Windows::Foundation::Rect bounds) m_nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); Babylon::Polyfills::Window::Initialize(env); - + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); Babylon::Plugins::NativeEngine::Initialize(env); diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 9f4f8a767..1858cadc7 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -177,7 +178,7 @@ namespace }); Babylon::Polyfills::Window::Initialize(env); - + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); diff --git a/Apps/Playground/X11/App.cpp b/Apps/Playground/X11/App.cpp index 992bf2548..2dc46d8b7 100644 --- a/Apps/Playground/X11/App.cpp +++ b/Apps/Playground/X11/App.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,7 @@ namespace }); Babylon::Polyfills::Window::Initialize(env); + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); diff --git a/Apps/Playground/iOS/LibNativeBridge.mm b/Apps/Playground/iOS/LibNativeBridge.mm index aacfecb74..32a4f4947 100644 --- a/Apps/Playground/iOS/LibNativeBridge.mm +++ b/Apps/Playground/iOS/LibNativeBridge.mm @@ -8,6 +8,7 @@ #import #import #import +#import #import #import #import @@ -81,7 +82,7 @@ - (void)init:(MTKView*)view screenScale:(float)inScreenScale width:(int)inWidth nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); Babylon::Polyfills::Window::Initialize(env); - + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); Babylon::Plugins::NativeCamera::Initialize(env); diff --git a/Apps/Playground/macOS/ViewController.mm b/Apps/Playground/macOS/ViewController.mm index 81190ccdf..d916dafad 100644 --- a/Apps/Playground/macOS/ViewController.mm +++ b/Apps/Playground/macOS/ViewController.mm @@ -4,6 +4,7 @@ #import #import #import +#import #import #import #import @@ -123,7 +124,7 @@ - (void)refreshBabylon { }); Babylon::Polyfills::Window::Initialize(env); - + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); diff --git a/Apps/Playground/visionOS/LibNativeBridge.mm b/Apps/Playground/visionOS/LibNativeBridge.mm index bd3043881..d43024870 100644 --- a/Apps/Playground/visionOS/LibNativeBridge.mm +++ b/Apps/Playground/visionOS/LibNativeBridge.mm @@ -5,6 +5,7 @@ #import #import #import +#import #import #import #import @@ -60,7 +61,7 @@ - (bool)initializeWithWidth:(NSInteger)width height:(NSInteger)height { self->_nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); Babylon::Polyfills::Window::Initialize(env); - + Babylon::Plugins::DataStream::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); Babylon::Plugins::NativeEngine::Initialize(env); diff --git a/Apps/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Apps/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index e8049cf63..bc0eb6022 100644 --- a/Apps/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Apps/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -35,6 +35,7 @@ target_link_libraries(UnitTestsJNI PRIVATE GraphicsDevice PRIVATE NativeCamera PRIVATE NativeEngine + PRIVATE DataStream PRIVATE NativeInput PRIVATE NativeOptimizations PRIVATE NativeEncoding diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index 3b4d70a89..ce2c5eb02 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries(UnitTests PRIVATE Console PRIVATE GraphicsDevice PRIVATE NativeEngine + PRIVATE DataStream PRIVATE NativeEncoding PRIVATE ScriptLoader PRIVATE UrlLib diff --git a/Apps/package-lock.json b/Apps/package-lock.json index aca8e20fd..5ae738721 100644 --- a/Apps/package-lock.json +++ b/Apps/package-lock.json @@ -11,11 +11,11 @@ "UnitTests" ], "dependencies": { - "babylonjs": "^8.28.2", - "babylonjs-gltf2interface": "^8.28.2", - "babylonjs-gui": "^8.28.2", - "babylonjs-loaders": "^8.28.2", - "babylonjs-materials": "^8.28.2", + "babylonjs": "^8.34.0", + "babylonjs-gltf2interface": "^8.34.0", + "babylonjs-gui": "^8.34.0", + "babylonjs-loaders": "^8.34.0", + "babylonjs-materials": "^8.34.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" } @@ -2600,44 +2600,44 @@ } }, "node_modules/babylonjs": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-8.28.2.tgz", - "integrity": "sha512-mid1cYg2VGXKj4neNy+78MHwSV8tMCHbvSEaIXQuEj1JxKmEb+UQ6/XwqiSGMrw1IoFxRZ693kelfe1tuSdKJg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-8.34.0.tgz", + "integrity": "sha512-8b9bchC7vnS/DFbz0btxJtnzs66EQ2uYsNKOLJkENO6o6HBje1EhQ8w6XTGczWgfV0JQ4HpJNLaB0wgPECh/Rg==", "hasInstallScript": true, "license": "Apache-2.0" }, "node_modules/babylonjs-gltf2interface": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.28.2.tgz", - "integrity": "sha512-TGusApjAPNkPhyyU/05FNoiEc9l85Kd4ZGHO/5hsRkLLYVXvmsqAEa9yGIVLhYM3Yezf/b1DFwMPvY1D/NEiVA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.34.0.tgz", + "integrity": "sha512-S9NTYnnI+shdM1rbZus4FZP0fbOvW4380o0BWL14v21Or/5SKtbtgnbNabUGpiJQ2hDTMeslSj5vXkmlZ/me+g==", "license": "Apache-2.0" }, "node_modules/babylonjs-gui": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-8.28.2.tgz", - "integrity": "sha512-sFURHbGEfPiRPGQM4jKsPbG4ZxWj9cejNbaxbjNMCB8izc33rzXB67+yHxDdSfjkiEeB1VM30OdhdEW6/JAq+g==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-8.34.0.tgz", + "integrity": "sha512-pKji7/lMi4HJ1jHNINvV09SPACearZ2mzkEIjg/z4MocP4H5ZbyIHprPM+xM+ckcWQlca89GLPeVaCuaMjB5BA==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "^8.28.2" + "babylonjs": "^8.34.0" } }, "node_modules/babylonjs-loaders": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-8.28.2.tgz", - "integrity": "sha512-WIbR3gaHhxSvRzs7Yuz578YlhPXqlB36tEEqIo0E+s5FqgzFy+o2TBN0O8nWnGuyk7s4wAZpA8CBQ58KsGfw5Q==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-8.34.0.tgz", + "integrity": "sha512-W5SYqBEw3p2jMVzSjZcOm31qsSBwFRTiEhPHYMFtkVJexEogSWLPJKVm0dxUg/UEYXjryNMNHo4fZDiosoF54A==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "^8.28.2", - "babylonjs-gltf2interface": "^8.28.2" + "babylonjs": "^8.34.0", + "babylonjs-gltf2interface": "^8.34.0" } }, "node_modules/babylonjs-materials": { - "version": "8.28.2", - "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-8.28.2.tgz", - "integrity": "sha512-5/pURv40ugE7tIeiuW/6C4Eo6VXOcHZJhtjrwdLDlpfMFpAU7d2JTNyaa9NqnHkRVRqXtad3/cu6FoVw6+AsZw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-8.34.0.tgz", + "integrity": "sha512-Bu8k8ktFZ3GC995Bb1u3KqyRI2VuyGJ+Nafd+vgkR5rNon6RNNM6aqClqWCcsziagybESv+gyOU5dqardPbX5g==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "^8.28.2" + "babylonjs": "^8.34.0" } }, "node_modules/balanced-match": { diff --git a/Apps/package.json b/Apps/package.json index 10eb897c7..63a6cd0ac 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -9,11 +9,11 @@ "getNightly": "node scripts/getNightly.js" }, "dependencies": { - "babylonjs": "^8.28.2", - "babylonjs-gltf2interface": "^8.28.2", - "babylonjs-gui": "^8.28.2", - "babylonjs-loaders": "^8.28.2", - "babylonjs-materials": "^8.28.2", + "babylonjs": "^8.34.0", + "babylonjs-gltf2interface": "^8.34.0", + "babylonjs-gui": "^8.34.0", + "babylonjs-loaders": "^8.34.0", + "babylonjs-materials": "^8.34.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 620732307..0a46e111f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,13 @@ FetchContent_Declare(SPIRV-Cross FetchContent_Declare(libwebp GIT_REPOSITORY https://github.com/webmproject/libwebp.git GIT_TAG 57e324e2eb99be46df46d77b65705e34a7ae616c) +FetchContent_Declare(miniz + GIT_REPOSITORY https://github.com/richgel999/miniz.git + GIT_TAG 174573d60290f447c13a2b1b3405de2b96e27d6c) +FetchContent_Declare(libdeflate + GIT_REPOSITORY https://github.com/ebiggers/libdeflate.git + GIT_TAG c8c56a20f8f621e6a966b716b31f1dedab6a41e3) + # -------------------------------------------------- FetchContent_MakeAvailable(CMakeExtensions) @@ -102,6 +109,7 @@ option(BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS "Include Babylon Native Plugin option(BABYLON_NATIVE_PLUGIN_NATIVETRACING "Include Babylon Native Plugin NativeTracing." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEXR "Include Babylon Native Plugin XR." ON) option(BABYLON_NATIVE_PLUGIN_TESTUTILS "Include Babylon Native Plugin TestUtils." ON) +option(BABYLON_NATIVE_PLUGIN_DATASTREAM "Include Babylon Native Plugin DataStream." ON) # Polyfills option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index dd3b9f2c1..99157729c 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -233,3 +233,21 @@ endif() if(WINDOWS_STORE) add_subdirectory(WindowsAppSDK) endif() + +# -------------------------------------------------- +# miniz +# -------------------------------------------------- +if(BABYLON_NATIVE_PLUGIN_DATASTREAM) + FetchContent_MakeAvailable_With_Message(miniz) + set_property(TARGET miniz PROPERTY UNITY_BUILD false) +endif() + +# -------------------------------------------------- +# libdeflate +# -------------------------------------------------- +if(BABYLON_NATIVE_PLUGIN_DATASTREAM) + set(LIBDEFLATE_BUILD_SHARED_LIB OFF) + set(LIBDEFLATE_COMPRESSION_SUPPORT OFF) + FetchContent_MakeAvailable_With_Message(libdeflate) + set_property(TARGET libdeflate_static PROPERTY UNITY_BUILD false) +endif() diff --git a/Install/Test/CMakeLists.txt b/Install/Test/CMakeLists.txt index ca24bb2a0..a1530050d 100644 --- a/Install/Test/CMakeLists.txt +++ b/Install/Test/CMakeLists.txt @@ -159,6 +159,7 @@ target_link_libraries(TestInstall NativeCamera NativeCapture NativeEngine + DataStream NativeInput NativeOptimizations NativeTracing diff --git a/NOTICE.md b/NOTICE.md index 362e73f0f..1fc87d659 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -2258,4 +2258,58 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file +``` + +# miniz + +``` +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC + +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +# libdeflate + +``` +Copyright 2016 Eric Biggers +Copyright 2024 Google LLC + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index 5ee02d811..3aa2aa0bf 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -37,3 +37,7 @@ endif() if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) add_subdirectory(NativeEncoding) endif() + +if(BABYLON_NATIVE_PLUGIN_DATASTREAM) + add_subdirectory(DataStream) +endif() diff --git a/Plugins/DataStream/CMakeLists.txt b/Plugins/DataStream/CMakeLists.txt new file mode 100644 index 000000000..03608e0e2 --- /dev/null +++ b/Plugins/DataStream/CMakeLists.txt @@ -0,0 +1,24 @@ +set(SOURCES + "Include/Babylon/Plugins/DataStream.h" + "Source/DataStream.h" + "Source/DataStream.cpp" + "Source/TextDecoder.h" + "Source/ReadableStream.h" + "Source/DecompressionStream.h" + "Source/Response.h") + +add_library(DataStream ${SOURCES}) +warnings_as_errors(DataStream) + +target_include_directories(DataStream PUBLIC + "Include") + +target_link_libraries(DataStream + PUBLIC napi + PRIVATE miniz + PRIVATE libdeflate_static + PRIVATE bx + PRIVATE JsRuntimeInternal) + +set_property(TARGET DataStream PROPERTY FOLDER Plugins) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Plugins/DataStream/Include/Babylon/Plugins/DataStream.h b/Plugins/DataStream/Include/Babylon/Plugins/DataStream.h new file mode 100644 index 000000000..90a474003 --- /dev/null +++ b/Plugins/DataStream/Include/Babylon/Plugins/DataStream.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace Babylon::Plugins::DataStream +{ + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Plugins/DataStream/Source/DataStream.cpp b/Plugins/DataStream/Source/DataStream.cpp new file mode 100644 index 000000000..f206000a1 --- /dev/null +++ b/Plugins/DataStream/Source/DataStream.cpp @@ -0,0 +1,102 @@ +#include "DataStream.h" + +#include +#include +#include +#include +#include "TextDecoder.h" +#include "ReadableStream.h" +#include "DecompressionStream.h" +#include "Response.h" + +BX_PRAGMA_DIAGNOSTIC_PUSH(); +BX_PRAGMA_DIAGNOSTIC_IGNORED_CLANG_GCC("-Wunused-function"); +BX_PRAGMA_DIAGNOSTIC_IGNORED_MSVC(4505) // error C4505: '' : unreferenced local function has been removed + +#include "miniz.h" + +BX_PRAGMA_DIAGNOSTIC_POP(); + +namespace Babylon::Plugins::Internal::DataStream +{ + void Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Object fflate = Napi::Object::New(env); + + fflate.Set("unzipSync", Napi::Function::New(env, DataStream::UnzipSync, "unzipSync")); + + // Attach to global scope + env.Global().Set("fflate", fflate); + } + + Napi::Value UnzipSync(const Napi::CallbackInfo& info) + { + const Napi::Env env{info.Env()}; + if (info.Length() < 1 || !info[0].IsTypedArray()) { + throw Napi::Error::New(env, "Expected Uint8Array argument"); + } + + // zip content + Napi::Uint8Array input = info[0].As(); + const uint8_t* zipData = input.Data(); + const size_t zipSize = input.ByteLength(); + + mz_zip_archive zip_archive; + memset(&zip_archive, 0, sizeof(zip_archive)); + + if (!mz_zip_reader_init_mem(&zip_archive, zipData, zipSize, 0)) { + throw Napi::Error::New(env, "Failed to initialize zip archive"); + } + + Napi::Object result = Napi::Object::New(env); + const auto fileCount = mz_zip_reader_get_num_files(&zip_archive); + + for (mz_uint i = 0; i < fileCount; i++) + { + mz_zip_archive_file_stat file_stat; + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) + { + continue; + } + + if (mz_zip_reader_is_file_a_directory(&zip_archive, i)) + { + continue; // skip directories + } + + std::string filename(file_stat.m_filename); + + size_t uncompressed_size = (size_t)file_stat.m_uncomp_size; + std::vector buffer(uncompressed_size); + + if (!mz_zip_reader_extract_to_mem(&zip_archive, i, buffer.data(), uncompressed_size, 0)) + { + throw Napi::Error::New(env, "Failed to extract file"); + continue; + } + + Napi::ArrayBuffer jsBuffer = Napi::ArrayBuffer::New(env, uncompressed_size); + memcpy(jsBuffer.Data(), buffer.data(), uncompressed_size); + Napi::Uint8Array jsArray = Napi::Uint8Array::New(env, uncompressed_size, jsBuffer, 0); + + // result[filename] = Uint8Array + result.Set(Napi::String::New(env, filename), jsArray); + } + mz_zip_reader_end(&zip_archive); + return result; + } +} + +namespace Babylon::Plugins::DataStream +{ + void BABYLON_API Initialize(Napi::Env env) + { + Internal::DataStream::Initialize(env); + Internal::TextDecoder::Initialize(env); + Internal::DecompressionStream::Initialize(env); + Internal::ReadableStream::Initialize(env); + Internal::Response::Initialize(env); + } +} diff --git a/Plugins/DataStream/Source/DataStream.h b/Plugins/DataStream/Source/DataStream.h new file mode 100644 index 000000000..79ff5409d --- /dev/null +++ b/Plugins/DataStream/Source/DataStream.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#include + +namespace Babylon::Plugins::Internal +{ + namespace DataStream + { + static void Initialize(Napi::Env env); + Napi::Value UnzipSync(const Napi::CallbackInfo& info); + }; +} // namespace diff --git a/Plugins/DataStream/Source/DecompressionStream.h b/Plugins/DataStream/Source/DecompressionStream.h new file mode 100644 index 000000000..b5541c08c --- /dev/null +++ b/Plugins/DataStream/Source/DecompressionStream.h @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include + +#include "libdeflate.h" + +namespace Babylon::Plugins::Internal +{ + class DecompressionStream final : public Napi::ObjectWrap + { + public: + static void Initialize(Napi::Env env); + + explicit DecompressionStream(const Napi::CallbackInfo& info); + const std::vector& GetDecompressedData() const { return m_data; } + private: + void Enqueue(const Napi::CallbackInfo& info); + void Close(const Napi::CallbackInfo& info); + + std::vector DecompressGzip(gsl::span compressedBuffer); + + std::vector m_data; + }; + + static constexpr auto JS_DECOMPRESSIONSTREAM_CONSTRUCTOR_NAME = "DecompressionStream"; + + void DecompressionStream::Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_DECOMPRESSIONSTREAM_CONSTRUCTOR_NAME, + { + InstanceMethod("enqueue", &DecompressionStream::Enqueue), + InstanceMethod("close", &DecompressionStream::Close), + }); + + JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_DECOMPRESSIONSTREAM_CONSTRUCTOR_NAME, func); + } + + DecompressionStream::DecompressionStream(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + const Napi::Env env{info.Env()}; + if (info.Length() == 1 && info[0].IsString()) { + std::string compressionType = info[0].As().Utf8Value(); + if (compressionType != "gzip") { + throw Napi::Error::New(env, "Unexpected compression type."); + } + } + } + + std::vector DecompressionStream::DecompressGzip(gsl::span compressedBuffer) + { + std::vector result; + + if (compressedBuffer.size() < 18) { + throw std::runtime_error("Invalid gzip data: too small"); + } + + // Verify gzip header magic bytes (0x1f, 0x8b) + if (compressedBuffer[0] != 0x1f || compressedBuffer[1] != 0x8b) { + throw std::runtime_error("Invalid gzip header"); + } + + // Get uncompressed size from gzip footer (last 4 bytes, little-endian) + size_t uncompressed_size = + static_cast(compressedBuffer[compressedBuffer.size() - 4]) | + (static_cast(compressedBuffer[compressedBuffer.size() - 3]) << 8) | + (static_cast(compressedBuffer[compressedBuffer.size() - 2]) << 16) | + (static_cast(compressedBuffer[compressedBuffer.size() - 1]) << 24); + + // Allocate output buffer + result.resize(uncompressed_size); + + // Create decompressor + struct libdeflate_decompressor* decompressor = libdeflate_alloc_decompressor(); + if (!decompressor) { + throw std::runtime_error("Failed to allocate decompressor"); + } + + // Decompress gzip data + size_t actual_size; + enum libdeflate_result decompress_result = libdeflate_gzip_decompress( + decompressor, + compressedBuffer.data(), + compressedBuffer.size(), + result.data(), + result.size(), + &actual_size + ); + + // Free decompressor + libdeflate_free_decompressor(decompressor); + + // Check result + if (decompress_result != LIBDEFLATE_SUCCESS) { + switch (decompress_result) { + case LIBDEFLATE_BAD_DATA: + throw std::runtime_error("Gzip decompression failed: bad or corrupted data"); + case LIBDEFLATE_SHORT_OUTPUT: + throw std::runtime_error("Gzip decompression failed: output buffer too small"); + case LIBDEFLATE_INSUFFICIENT_SPACE: + throw std::runtime_error("Gzip decompression failed: insufficient space"); + default: + throw std::runtime_error("Gzip decompression failed: unknown error"); + } + } + + // Resize to actual decompressed size (in case it differs from footer) + result.resize(actual_size); + + return result; + } + + void DecompressionStream::Enqueue(const Napi::CallbackInfo& info) + { + const Napi::Env env{info.Env()}; + auto value = info[0]; + Napi::TypedArray typed = value.As(); + + if (typed.TypedArrayType() == napi_uint8_array) + { + Napi::Uint8Array array = typed.As(); + + gsl::span buffer = {array.Data(), array.ByteLength()}; + if (buffer.empty()) + { + throw Napi::Error::New(env, "GZip data buffer is empty."); + } + + m_data = DecompressGzip(buffer); + } + } + + + void DecompressionStream::Close(const Napi::CallbackInfo& /*info*/) + { + } +} diff --git a/Plugins/DataStream/Source/ReadableStream.h b/Plugins/DataStream/Source/ReadableStream.h new file mode 100644 index 000000000..93409dbe6 --- /dev/null +++ b/Plugins/DataStream/Source/ReadableStream.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +namespace Babylon::Plugins::Internal +{ + class ReadableStream final : public Napi::ObjectWrap + { + public: + virtual ~ReadableStream(); + static void Initialize(Napi::Env env); + + explicit ReadableStream(const Napi::CallbackInfo& info); + private: + Napi::Value PipeThrough(const Napi::CallbackInfo& info); + + Napi::FunctionReference m_startHandlerRef; + Napi::ObjectReference m_pipeThroughRef; + }; + + static constexpr auto JS_READABLESTREAM_CONSTRUCTOR_NAME = "ReadableStream"; + + void ReadableStream::Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_READABLESTREAM_CONSTRUCTOR_NAME, + { + InstanceMethod("pipeThrough", &ReadableStream::PipeThrough), + }); + + JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_READABLESTREAM_CONSTRUCTOR_NAME, func); + } + + ReadableStream::ReadableStream(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + if (!info[0].IsObject()) + { + return; + } + + auto object = info[0].ToObject(); + if (object.Has("start")) { + Napi::Value startFunction = object.Get("start"); + if (startFunction.IsFunction()) + { + Napi::Function handler{startFunction.As()}; + m_startHandlerRef = Napi::Persistent(handler); + } + } + } + + ReadableStream::~ReadableStream() + { + m_pipeThroughRef.Reset(); + m_startHandlerRef.Reset(); + } + + Napi::Value ReadableStream::PipeThrough(const Napi::CallbackInfo& info) + { + Napi::Object stream = info[0].As(); + m_pipeThroughRef = Napi::Persistent(stream); + m_startHandlerRef.Call({stream}); + return stream; + } +} diff --git a/Plugins/DataStream/Source/Response.h b/Plugins/DataStream/Source/Response.h new file mode 100644 index 000000000..8bf8d48b6 --- /dev/null +++ b/Plugins/DataStream/Source/Response.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include "DecompressionStream.h" + +namespace Babylon::Plugins::Internal +{ + class Response final : public Napi::ObjectWrap + { + public: + static void Initialize(Napi::Env env); + + explicit Response(const Napi::CallbackInfo& info); + private: + Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); + std::vector m_data; + }; + + static constexpr auto JS_RESPONSE_CONSTRUCTOR_NAME = "Response"; + + void Response::Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_RESPONSE_CONSTRUCTOR_NAME, + { + InstanceMethod("arrayBuffer", &Response::ArrayBuffer), + }); + + JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_RESPONSE_CONSTRUCTOR_NAME, func); + } + + Response::Response(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + m_data = DecompressionStream::Unwrap(info[0].As())->GetDecompressedData(); + } + + Napi::Value Response::ArrayBuffer(const Napi::CallbackInfo& ) + { + const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size()); + std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size()); + + const auto deferred = Napi::Promise::Deferred::New(Env()); + deferred.Resolve(arrayBuffer); + return deferred.Promise(); + } +} diff --git a/Plugins/DataStream/Source/TextDecoder.h b/Plugins/DataStream/Source/TextDecoder.h new file mode 100644 index 000000000..037347183 --- /dev/null +++ b/Plugins/DataStream/Source/TextDecoder.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace Babylon::Plugins::Internal +{ + class TextDecoder final : public Napi::ObjectWrap + { + public: + static void Initialize(Napi::Env env); + + explicit TextDecoder(const Napi::CallbackInfo& info); + private: + Napi::Value Decode(const Napi::CallbackInfo& info); + }; + + static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; + + void TextDecoder::Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_TEXTDECODER_CONSTRUCTOR_NAME, + { + InstanceMethod("decode", &TextDecoder::Decode), + }); + + JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_TEXTDECODER_CONSTRUCTOR_NAME, func); + } + + TextDecoder::TextDecoder(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + { + } + + Napi::Value TextDecoder::Decode(const Napi::CallbackInfo& info) + { + const Napi::Env env{info.Env()}; + if (info.Length() < 1 || !info[0].IsTypedArray()) { + throw Napi::Error::New(info.Env(), "Expected Uint8Array argument"); + } + + // content + Napi::Uint8Array input = info[0].As(); + const uint8_t* inputData = input.Data(); + const size_t inputSize = input.ByteLength(); + + return Napi::Value::From(env, std::string(reinterpret_cast(inputData), inputSize)); + } +} diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 19b646327..2247fd7ba 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -254,6 +254,7 @@ namespace Babylon { assert( image->m_format == bimg::TextureFormat::R16 || + image->m_format == bimg::TextureFormat::RG16F || image->m_format == bimg::TextureFormat::RGB8 || image->m_format == bimg::TextureFormat::RGBA8 || image->m_format == bimg::TextureFormat::RGBA16 || @@ -287,7 +288,7 @@ namespace Babylon image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA8, *image, false); bimg::imageFree(oldImage); } - else if (image->m_format == bimg::TextureFormat::RGBA16F || image->m_format == bimg::TextureFormat::RGBA16 || image->m_format == bimg::TextureFormat::R16) + else if (image->m_format == bimg::TextureFormat::RGBA16F || image->m_format == bimg::TextureFormat::RGBA16 || image->m_format == bimg::TextureFormat::R16 || image->m_format == bimg::TextureFormat::RG16F) { bimg::ImageContainer* oldImage{image}; image = bimg::imageConvert(&allocator, bimg::TextureFormat::RGBA32F, *image, false); diff --git a/Polyfills/Canvas/CMakeLists.txt b/Polyfills/Canvas/CMakeLists.txt index e7e7a2381..7e28438c2 100644 --- a/Polyfills/Canvas/CMakeLists.txt +++ b/Polyfills/Canvas/CMakeLists.txt @@ -54,6 +54,13 @@ target_link_libraries(Canvas PRIVATE UrlLib PRIVATE base-n) +if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_WEBP) + target_compile_definitions(Canvas + PRIVATE WEBP) + target_link_libraries(Canvas + PRIVATE webp) +endif() + if(SHADERC_PATH AND NOT IOS AND NOT VISIONOS AND NOT ANDROID) if(EXISTS "${SHADERC_PATH}") message(STATUS "Using shaderc from: ${SHADERC_PATH}") diff --git a/Polyfills/Canvas/Source/Image.cpp b/Polyfills/Canvas/Source/Image.cpp index 8e4424728..dc64645cf 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -13,6 +13,10 @@ #include #include +#ifdef WEBP +#include +#endif + namespace Babylon::Polyfills::Internal { static constexpr auto JS_IMAGE_CONSTRUCTOR_NAME = "Image"; @@ -30,6 +34,7 @@ namespace Babylon::Polyfills::Internal InstanceAccessor("naturalWidth", &NativeCanvasImage::GetNaturalWidth, nullptr), InstanceAccessor("naturalHeight", &NativeCanvasImage::GetNaturalHeight, nullptr), InstanceAccessor("src", &NativeCanvasImage::GetSrc, &NativeCanvasImage::SetSrc), + InstanceAccessor("crossOrigin", &NativeCanvasImage::GetCrossOrigin, &NativeCanvasImage::SetCrossOrigin), InstanceAccessor("onload", nullptr, &NativeCanvasImage::SetOnload), InstanceAccessor("onerror", nullptr, &NativeCanvasImage::SetOnerror), // TODO: This should be set directly on the JS Object rather than via an instanceAccessor see: https://github.com/BabylonJS/BabylonNative/issues/1030 @@ -104,7 +109,20 @@ namespace Babylon::Polyfills::Internal if (m_imageContainer == nullptr) { +#ifdef WEBP + int width; + int height; + if (WebPGetInfo(reinterpret_cast(buffer.data()), buffer.size(), &width, &height)) + { + m_imageContainer = bimg::imageAlloc(&Graphics::DeviceContext::GetDefaultAllocator(), bimg::TextureFormat::RGBA8, static_cast(width), static_cast(height), 1, 1, false, false); + if (!WebPDecodeRGBAInto(reinterpret_cast(buffer.data()), buffer.size(), static_cast(m_imageContainer->m_data), static_cast(m_imageContainer->m_size), width * 4)) + { + return false; + } + } +#else return false; +#endif } m_width = m_imageContainer->m_width; @@ -117,53 +135,100 @@ namespace Babylon::Polyfills::Internal return true; } - void NativeCanvasImage::SetSrc(const Napi::CallbackInfo& info, const Napi::Value& value) + void NativeCanvasImage::SetCrossOrigin(const Napi::CallbackInfo& info, const Napi::Value& value) { - auto text{value.As().Utf8Value()}; + m_crossOrigin = value.As().Utf8Value(); + } + + Napi::Value NativeCanvasImage::GetCrossOrigin(const Napi::CallbackInfo& info) + { + return Napi::Value::From(info.Env(), m_crossOrigin); + } - // try with base64 - static const std::string base64{"base64,"}; - const auto pos = text.find(base64); - if (pos != std::string::npos) + void NativeCanvasImage::SetSrc(const Napi::CallbackInfo& info, const Napi::Value& value) + { + if (value.IsString()) { - arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [env{info.Env()}, this, text{std::move(text)}, pos]() { - std::vector base64Buffer; - bn::decode_b64(text.begin() + pos + base64.length(), text.end(), std::back_inserter(base64Buffer)); - gsl::span buffer = {reinterpret_cast(base64Buffer.data()), base64Buffer.size()}; + // url string + auto text{ value.As().Utf8Value() }; + + // try with base64 + static const std::string base64{ "base64," }; + const auto pos = text.find(base64); + if (pos != std::string::npos) + { + arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [env{ info.Env() }, this, text{ std::move(text) }, pos]() { + std::vector base64Buffer; + bn::decode_b64(text.begin() + pos + base64.length(), text.end(), std::back_inserter(base64Buffer)); + gsl::span buffer = { reinterpret_cast(base64Buffer.data()), base64Buffer.size() }; + + if (!SetBuffer(buffer)) + { + HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided base64 source.")); + } + }); + return; + } + + // try with URL + UrlLib::UrlRequest request{}; + request.Open(UrlLib::UrlMethod::Get, text); + request.ResponseType(UrlLib::UrlResponseType::Buffer); + request.SendAsync().then(m_runtimeScheduler, *m_cancellationSource, [env{ info.Env() }, this, cancellationSource{ m_cancellationSource }, request{ std::move(request) }, text](arcana::expected result) { + if (result.has_error()) + { + HandleLoadImageError(Napi::Error::New(env, result.error())); + return; + } + + Dispose(); + + auto buffer{ request.ResponseBuffer() }; + if (buffer.data() == nullptr || buffer.size_bytes() == 0) + { + HandleLoadImageError(Napi::Error::New(env, "Image with provided source returned empty response or invalid base64.")); + return; + } if (!SetBuffer(buffer)) { - HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided base64 source.")); + HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided source URL.")); } }); - return; } - - // try with URL - UrlLib::UrlRequest request{}; - request.Open(UrlLib::UrlMethod::Get, text); - request.ResponseType(UrlLib::UrlResponseType::Buffer); - request.SendAsync().then(m_runtimeScheduler, *m_cancellationSource, [env{info.Env()}, this, cancellationSource{m_cancellationSource}, request{std::move(request)}, text](arcana::expected result) { - if (result.has_error()) - { - HandleLoadImageError(Napi::Error::New(env, result.error())); - return; + else if (value.IsObject()) { + Napi::Object blob = value.As(); + /* + * optional mimetype not used here. + if (blob.Has("type")) { + auto mimeType = blob.Get("type").As().Utf8Value(); } - - Dispose(); - - auto buffer{request.ResponseBuffer()}; - if (buffer.data() == nullptr || buffer.size_bytes() == 0) - { - HandleLoadImageError(Napi::Error::New(env, "Image with provided source returned empty response or invalid base64.")); - return; + */ + if (blob.Has((uint32_t)0)) { + Napi::Value v = blob.Get((uint32_t)0); + if (v.IsTypedArray()) { + Napi::Uint8Array array = v.As(); + gsl::span buffer = { reinterpret_cast(array.Data()), array.ByteLength() }; + if (!SetBuffer(buffer)) + { + HandleLoadImageError(Napi::Error::New(info.Env(), "Unable to decode image with provided blob.")); + } + } } + } + else if (value.IsTypedArray()) { + Napi::TypedArray typed = value.As(); - if (!SetBuffer(buffer)) - { - HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided source URL.")); + if (typed.TypedArrayType() == napi_uint8_array) { + Napi::Uint8Array array = typed.As(); + gsl::span buffer = { reinterpret_cast(array.Data()), array.ByteLength() }; + if (!SetBuffer(buffer)) + { + HandleLoadImageError(Napi::Error::New(info.Env(), "Unable to decode image with provided source typed buffer.")); + } } - }); + } + } void NativeCanvasImage::SetOnload(const Napi::CallbackInfo&, const Napi::Value& value) diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index 9e161e9ad..50924030a 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -31,8 +31,10 @@ namespace Babylon::Polyfills::Internal Napi::Value GetNaturalWidth(const Napi::CallbackInfo&); Napi::Value GetNaturalHeight(const Napi::CallbackInfo&); Napi::Value GetSrc(const Napi::CallbackInfo&); + Napi::Value GetCrossOrigin(const Napi::CallbackInfo& info); Napi::Value GetImageContainer(const Napi::CallbackInfo&); void SetSrc(const Napi::CallbackInfo&, const Napi::Value&); + void SetCrossOrigin(const Napi::CallbackInfo& info, const Napi::Value& value); void SetOnload(const Napi::CallbackInfo&, const Napi::Value&); void SetOnerror(const Napi::CallbackInfo&, const Napi::Value&); void HandleLoadImageError(const Napi::Error& error); @@ -43,6 +45,7 @@ namespace Babylon::Polyfills::Internal uint32_t m_height{1}; std::string m_src{}; + std::string m_crossOrigin{}; JsRuntimeScheduler m_runtimeScheduler; Napi::FunctionReference m_onloadHandlerRef;