Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ set_property(TARGET UnitTests PROPERTY UNITY_BUILD false)

target_link_libraries(UnitTests
PRIVATE AppRuntime
PRIVATE Blob
PRIVATE Canvas
PRIVATE Console
PRIVATE GraphicsDevice
Expand Down
19 changes: 5 additions & 14 deletions Apps/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,9 @@ describe("PostProcesses", function () {
describe("NativeEncoding", function () {
this.timeout(0);

function expectValidPNG(arrayBuffer: ArrayBuffer) {
expect(arrayBuffer).to.be.instanceOf(ArrayBuffer);
async function expectValidPNG(blob: Blob) {
expect(blob).to.be.instanceOf(Blob);
const arrayBuffer = await blob.arrayBuffer();
expect(arrayBuffer.byteLength).to.be.greaterThan(0);

const pngSignature = new Uint8Array(arrayBuffer.slice(0, 4));
Expand All @@ -309,7 +310,7 @@ describe("NativeEncoding", function () {
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);
await expectValidPNG(result);
});

it("should handle multiple concurrent encoding tasks", async function () {
Expand All @@ -320,17 +321,7 @@ describe("NativeEncoding", function () {
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;
}
await Promise.all(results.map(b => expectValidPNG(b)));
});
});

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 @@ -5,6 +5,7 @@
#include <Babylon/Polyfills/Console.h>
#include <Babylon/Polyfills/Window.h>
#include <Babylon/Polyfills/Canvas.h>
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Plugins/NativeEngine.h>
#include <Babylon/Plugins/NativeEncoding.h>
#include <Babylon/ScriptLoader.h>
Expand Down Expand Up @@ -73,6 +74,7 @@ TEST(JavaScript, All)
std::cout.flush();
});
Babylon::Polyfills::Window::Initialize(env);
Babylon::Polyfills::Blob::Initialize(env);
nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env));
Babylon::Plugins::NativeEngine::Initialize(env);
Babylon::Plugins::NativeEncoding::Initialize(env);
Expand Down
5 changes: 5 additions & 0 deletions Install/Install.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,8 @@ if(TARGET XMLHttpRequest)
install_targets(XMLHttpRequest)
install_include_for_targets(XMLHttpRequest)
endif()

if(TARGET Blob)
install_targets(Blob)
install_include_for_targets(Blob)
endif()
20 changes: 15 additions & 5 deletions Plugins/NativeEncoding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,36 @@

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.).

## Prerequisites

A **Blob implementation** must be registered before using this plugin. The Babylon polyfill provides one that can be initialized from C++ using `Babylon::Polyfills::Blob::Initialize()`.

## Limitations

Currently, **only PNG encoding** is supported.
Currently, **only PNG encoding** is supported. If a different MIME type is requested (e.g., "image/jpeg"), it will be ignored.

## Design
## 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.
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>;
EncodeImageAsync: (
pixelData: ArrayBufferView,
width: number,
height: number,
mimeType?: string,
invertY?: boolean
) => Promise<Blob>;
}
```

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

36 changes: 20 additions & 16 deletions Plugins/NativeEncoding/Source/NativeEncoding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Babylon::Plugins
{
namespace
{
std::shared_ptr<std::vector<uint8_t>> EncodePNG(const std::vector<uint8_t>& pixelData, uint32_t width, uint32_t height, bool invertY)
std::shared_ptr<std::vector<std::byte>> EncodePNG(const std::vector<std::byte>& pixelData, uint32_t width, uint32_t height, bool invertY)
{
auto memoryBlock{bx::MemoryBlock(&Graphics::DeviceContext::GetDefaultAllocator())};
auto writer{bx::MemoryWriter(&memoryBlock)};
Expand All @@ -35,28 +35,22 @@ namespace Babylon::Plugins
throw std::runtime_error("Failed to encode PNG image: output is empty");
}

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

return result;
}

Napi::Value EncodeImageAsync(const Napi::CallbackInfo& info)
{
auto buffer{info[0].As<Napi::Uint8Array>()};
auto buffer{info[0].As<Napi::TypedArray>()}; // ArrayBufferView
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 mimeType{info.Length() > 3 && !info[3].IsUndefined() ? info[3].As<Napi::String>().Utf8Value() : "image/png"};
auto invertY{info.Length() > 4 && !info[4].IsUndefined() ? info[4].As<Napi::Boolean>().Value() : false};

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)
{
Expand All @@ -65,14 +59,15 @@ namespace Babylon::Plugins
}

auto runtimeScheduler{std::make_shared<JsRuntimeScheduler>(JsRuntime::GetFromJavaScript(env))};
auto pixelData{std::vector<uint8_t>(buffer.Data(), buffer.Data() + buffer.ByteLength())};
auto start = static_cast<std::byte*>(buffer.ArrayBuffer().Data()) + buffer.ByteOffset();
auto pixelData{std::vector<std::byte>(start, start + buffer.ByteLength())};

arcana::make_task(arcana::threadpool_scheduler, arcana::cancellation_source::none(),
[pixelData{std::move(pixelData)}, width, height, invertY]() {
return EncodePNG(pixelData, width, height, invertY);
})
.then(*runtimeScheduler, arcana::cancellation_source::none(),
[runtimeScheduler, deferred, env](const arcana::expected<std::shared_ptr<std::vector<uint8_t>>, std::exception_ptr>& result) {
[runtimeScheduler, deferred, env](const arcana::expected<std::shared_ptr<std::vector<std::byte>>, 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())
Expand All @@ -83,8 +78,17 @@ namespace Babylon::Plugins

auto& imageData = result.value();
auto arrayBuffer{Napi::ArrayBuffer::New(env, imageData->data(), imageData->size(), [imageData](Napi::Env, void*) {})};

deferred.Resolve(arrayBuffer);

auto blobCtor = env.Global().Get("Blob").As<Napi::Function>();
auto blobParts = Napi::Array::New(env, 1);
blobParts.Set(uint32_t{0}, arrayBuffer);

auto options = Napi::Object::New(env);
options.Set("type", Napi::String::New(env, "image/png"));

auto blob = blobCtor.New({blobParts, options});

deferred.Resolve(blob);
});

return deferred.Promise();
Expand Down