Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5b174c4
Add NativeEncoding plugin
alexchuber Oct 15, 2025
d201676
Update installations
alexchuber Oct 15, 2025
5c1468f
Add readme
alexchuber Oct 15, 2025
f874d8e
Capitalize and add readme
alexchuber Oct 15, 2025
34db646
Add error handling
alexchuber Oct 20, 2025
dcccbd8
Promisify EncodeImage
alexchuber Oct 20, 2025
e9fd332
Asynchronize EncodeImage
alexchuber Oct 20, 2025
7ae9345
Add NativeEncoding to installation test
alexchuber Oct 20, 2025
7ec843e
Add unit tests
alexchuber Oct 20, 2025
3002bcf
Update readme
alexchuber Oct 20, 2025
c8e9f3c
Remove NativeEncoding from Playground (will handle in separate PR)
alexchuber Oct 20, 2025
be49d00
Update tests
alexchuber Oct 20, 2025
bf18698
Test async operations
alexchuber Oct 20, 2025
7718f35
Add comments
alexchuber Oct 21, 2025
507b939
Copy buffer & tie cancellation to JS env lifetime
alexchuber Oct 21, 2025
ef5c562
Meh, remove cancellation source until better pattern comes up
alexchuber Oct 21, 2025
aa84b88
Remove teardown test (known crash) and block test (not much of a point)
alexchuber Oct 22, 2025
9071bf3
Remove a copy; clean up syntax
alexchuber Oct 22, 2025
1cc8713
Comments
alexchuber Oct 22, 2025
ea0a2b1
Remove unneeded shared_ptr
alexchuber Oct 22, 2025
d3acc69
Debugging notes
alexchuber Oct 24, 2025
baa96e0
Remove const using cast in order to prevent copy
alexchuber Oct 24, 2025
d3a8d9c
Experiment with shared_ptr return
alexchuber Oct 24, 2025
dde1c28
Experiment with unique_ptr return
alexchuber Oct 25, 2025
903738d
Notes
alexchuber Oct 25, 2025
40b4628
PR feedback
alexchuber Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Apps/UnitTests/Android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ target_link_libraries(UnitTestsJNI
PRIVATE NativeEngine
PRIVATE NativeInput
PRIVATE NativeOptimizations
PRIVATE NativeEncoding
PRIVATE ScriptLoader
PRIVATE XMLHttpRequest
PRIVATE gtest_main
Expand Down
1 change: 1 addition & 0 deletions Apps/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ target_link_libraries(UnitTests
PRIVATE Console
PRIVATE GraphicsDevice
PRIVATE NativeEngine
PRIVATE NativeEncoding
PRIVATE ScriptLoader
PRIVATE UrlLib
PRIVATE Window
Expand Down
42 changes: 42 additions & 0 deletions Apps/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,48 @@ describe("PostProcesses", function () {
});*/
});

describe("NativeEncoding", function () {
this.timeout(0);

function expectValidPNG(arrayBuffer: ArrayBuffer) {
expect(arrayBuffer).to.be.instanceOf(ArrayBuffer);
expect(arrayBuffer.byteLength).to.be.greaterThan(0);

const pngSignature = new Uint8Array(arrayBuffer.slice(0, 4));
expect(pngSignature[0]).to.equal(137); // PNG signature bytes
expect(pngSignature[1]).to.equal(80); // 'P'
expect(pngSignature[2]).to.equal(78); // 'N'
expect(pngSignature[3]).to.equal(71); // 'G'
}

it("should encode a PNG", async function () {
const pixelData = new Uint8Array(4).fill(255);
const result = await _native.EncodeImageAsync(pixelData, 1, 1, "image/png", false);
expectValidPNG(result);
});

it("should handle multiple concurrent encoding tasks", async function () {
const pixelDatas = [];
for (let i = 0; i < 10; i++) {
pixelDatas.push(new Uint8Array(4).fill(255));
}
const results = await Promise.all(pixelDatas.map((pixelData) =>
_native.EncodeImageAsync(pixelData, 1, 1, "image/png", false)
));
results.forEach(expectValidPNG);
});

it("should reject if MIME type not supported", async function () {
const pixelData = new Uint8Array([255, 0, 0, 255]);
try {
await _native.EncodeImageAsync(pixelData, 1, 1, "bad-mimetype", false);
expect.fail("Expected promise to reject with unsupported mime type");
} catch (error) {
expect(error).to.exist;
}
});
});

mocha.run((failures) => {
// Test program will wait for code to be set before exiting
if (failures > 0) {
Expand Down
2 changes: 2 additions & 0 deletions Apps/UnitTests/Shared/Shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <Babylon/Polyfills/Window.h>
#include <Babylon/Polyfills/Canvas.h>
#include <Babylon/Plugins/NativeEngine.h>
#include <Babylon/Plugins/NativeEncoding.h>
#include <Babylon/ScriptLoader.h>
#include <Babylon/ShaderCache.h>
#include <chrono>
Expand Down Expand Up @@ -74,6 +75,7 @@ TEST(JavaScript, All)
Babylon::Polyfills::Window::Initialize(env);
nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env));
Babylon::Plugins::NativeEngine::Initialize(env);
Babylon::Plugins::NativeEncoding::Initialize(env);

auto setExitCodeCallback = Napi::Function::New(
env, [&exitCodePromise](const Napi::CallbackInfo& info) {
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,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_NATIVEENCODING "Include Babylon Native Plugin NativeEncoding." ON)

# Polyfills
option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON)
Expand Down
5 changes: 5 additions & 0 deletions Install/Install.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ if(TARGET NativeXr)
install_include_for_targets(NativeXr)
endif()

if(TARGET NativeEncoding)
install_targets(NativeEncoding)
install_include_for_targets(NativeEncoding)
endif()

# ----------------
# Polyfills
# ----------------
Expand Down
1 change: 1 addition & 0 deletions Install/Test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ target_link_libraries(TestInstall
NativeInput
NativeOptimizations
NativeTracing
NativeEncoding
OGLCompiler
OSDependent
ScriptLoader
Expand Down
4 changes: 4 additions & 0 deletions Plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ endif()
if(BABYLON_NATIVE_PLUGIN_NATIVEXR AND (ANDROID OR IOS))
add_subdirectory(NativeXr)
endif()

if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING)
add_subdirectory(NativeEncoding)
endif()
18 changes: 18 additions & 0 deletions Plugins/NativeEncoding/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
set(SOURCES
"Include/Babylon/Plugins/NativeEncoding.h"
"Source/NativeEncoding.cpp")

add_library(NativeEncoding ${SOURCES})
warnings_as_errors(NativeEncoding)

target_include_directories(NativeEncoding
PUBLIC "Include")

target_link_libraries(NativeEncoding
PUBLIC napi
PRIVATE GraphicsDevice
PRIVATE GraphicsDeviceContext
PRIVATE JsRuntimeInternal)

set_property(TARGET NativeEncoding PROPERTY FOLDER Plugins)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Plugins::NativeEncoding
{
void BABYLON_API Initialize(Napi::Env env);
}
28 changes: 28 additions & 0 deletions Plugins/NativeEncoding/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# NativeEncoding

> ⚠️ **This plugin is experimental and subject to change.**
The NativeEncoding plugin provides native image encoding capabilities to Babylon, allowing raw pixel data to be encoded into standard image formats (PNG, JPEG, WebP, etc.).

## Design

Unlike a traditional polyfill which would implement Canvas's `toBlob()` or `toDataURL()` methods, NativeEncoding exists as a plugin because:
1. **No standard Web API exists** for general-purpose image encoding separate from Canvas
2. **Simplicity** - Exposes only what Babylon actually needs: direct pixel-to-bytes encoding
3. **Efficiency** - Avoids extra routing through the Canvas API via intermediate data structures
4. **Modularity** - Image encoding is a separate concern from 2D canvas rendering
5. **Extensibility** - New codecs can be added in the future without bloating other components

An encoding function is exposed on the `_native` global object, similar to NativeOptimizations.

```typescript
interface INative {
EncodeImageAsync: (pixelData: Uint8Array, width: number, height: number, mimeType: string, invertY: boolean) => Promise<ArrayBuffer>;
}
```

It should be wrapped by higher-level Babylon.js APIs (e.g., DumpTools) for common workflows like asset exports and screenshots.

## Limitations

Currently, **only PNG encoding** is supported.
102 changes: 102 additions & 0 deletions Plugins/NativeEncoding/Source/NativeEncoding.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include <Babylon/Plugins/NativeEncoding.h>
#include <Babylon/JsRuntime.h>
#include <Babylon/JsRuntimeScheduler.h>
#include <Babylon/Graphics/DeviceContext.h>

#include <napi/napi.h>

#include <bimg/encode.h>
#include <bx/readerwriter.h>

#include <arcana/threading/task.h>
#include <arcana/threading/task_schedulers.h>

namespace Babylon::Plugins
{
namespace
{
std::vector<uint8_t> EncodePNG(const std::vector<uint8_t>& pixelData, uint32_t width, uint32_t height, bool invertY)
{
auto memoryBlock{bx::MemoryBlock(&Graphics::DeviceContext::GetDefaultAllocator())};
auto writer{bx::MemoryWriter(&memoryBlock)};
auto err{bx::Error()};

bimg::imageWritePng(&writer, width, height, width * 4, pixelData.data(), bimg::TextureFormat::RGBA8, !invertY, &err);

auto byteLength{memoryBlock.getSize()};

if (!err.isOk())
{
throw std::runtime_error("Failed to encode PNG image: " + std::string(err.getMessage().getCPtr()));
}

if (byteLength == 0)
{
throw std::runtime_error("Failed to encode PNG image: output is empty");
}

auto result{std::vector<uint8_t>(byteLength)};
std::memcpy(result.data(), memoryBlock.more(0), byteLength);

return result;
}

Napi::Promise EncodeImageAsync(const Napi::CallbackInfo& info)
{
auto buffer{info[0].As<Napi::Uint8Array>()};
auto width{info[1].As<Napi::Number>().Uint32Value()};
auto height{info[2].As<Napi::Number>().Uint32Value()};
auto mimeType{info[3].As<Napi::String>().Utf8Value()};
auto invertY{info[4].As<Napi::Boolean>().Value()};

auto env{info.Env()};
auto deferred{Napi::Promise::Deferred::New(env)};

if (mimeType != "image/png")
{
deferred.Reject(Napi::Error::New(env, "Unsupported mime type: " + mimeType + ". Only image/png is currently supported.").Value());
return deferred.Promise();
}

if (buffer.ByteLength() != width * height * 4)
{
deferred.Reject(Napi::Error::New(env, "Buffer byte length does not match RGBA8 format (4 bytes per pixel) of provided dimensions.").Value());
return deferred.Promise();
}

auto runtimeScheduler{std::make_shared<JsRuntimeScheduler>(JsRuntime::GetFromJavaScript(env))};
auto pixelData{std::make_shared<std::vector<uint8_t>>(buffer.Data(), buffer.Data() + buffer.ByteLength())};

arcana::make_task(arcana::threadpool_scheduler, arcana::cancellation_source::none(),
[pixelData, width, height, invertY]() {
return EncodePNG(*pixelData, width, height, invertY);
})
.then(*runtimeScheduler, arcana::cancellation_source::none(),
[runtimeScheduler, deferred, env](const arcana::expected<std::vector<uint8_t>, std::exception_ptr>& result) {
// TODO: Crash risk on JS teardown - this async work isn't tied to any JS object lifetime,
// unlike other plugins that cancel / clean up pending work in their destructors.
if (result.has_error())
{
deferred.Reject(Napi::Error::New(env, result.error()).Value());
return;
}

auto imageData{std::make_shared<std::vector<uint8_t>>(std::move(result.value()))};
auto arrayBuffer{Napi::ArrayBuffer::New(env, imageData->data(), imageData->size(), [imageData](Napi::Env, void*){})};

deferred.Resolve(arrayBuffer);
});

return deferred.Promise();
}
}
}

namespace Babylon::Plugins::NativeEncoding
{
void BABYLON_API Initialize(Napi::Env env)
{
auto native{JsRuntime::NativeObject::GetFromJavaScript(env)};
native.Set("EncodeImageAsync", Napi::Function::New(env, EncodeImageAsync, "EncodeImageAsync"));
}
}
Loading