diff --git a/example/android/build.gradle b/example/android/build.gradle index c8ed00a62a..03386c0dbc 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -3,7 +3,7 @@ buildscript { ext { buildToolsVersion = "33.0.0" - minSdkVersion = 21 + minSdkVersion = 26 compileSdkVersion = 33 targetSdkVersion = 33 diff --git a/package/android/CMakeLists.txt b/package/android/CMakeLists.txt index 650f6af571..a9e2d9f054 100644 --- a/package/android/CMakeLists.txt +++ b/package/android/CMakeLists.txt @@ -47,6 +47,7 @@ add_library( "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/RNSkOpenGLCanvasProvider.cpp" "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp" "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/GrAHardwareBufferUtils.cpp" + "${PROJECT_SOURCE_DIR}/cpp/rnskia-android/AHardwareBufferUtils.cpp" "${PROJECT_SOURCE_DIR}/../cpp/jsi/JsiHostObject.cpp" "${PROJECT_SOURCE_DIR}/../cpp/jsi/JsiValue.cpp" diff --git a/package/android/cpp/rnskia-android/AHardwareBufferUtils.cpp b/package/android/cpp/rnskia-android/AHardwareBufferUtils.cpp new file mode 100644 index 0000000000..8c6bec37d3 --- /dev/null +++ b/package/android/cpp/rnskia-android/AHardwareBufferUtils.cpp @@ -0,0 +1,31 @@ +#if __ANDROID_API__ >= 26 + +#include "AHardwareBufferUtils.h" +#include + +namespace RNSkia { + +uint32_t GetBufferFormatFromSkColorType(SkColorType bufferFormat) { + switch (bufferFormat) { + case kRGBA_8888_SkColorType: + return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + case kRGB_888x_SkColorType: + return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM; + case kRGBA_F16_SkColorType: + return AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT; + case kRGB_565_SkColorType: + return AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; + case kRGBA_1010102_SkColorType: + return AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM; +#if __ANDROID_API__ >= 33 + case kAlpha_8_SkColorType: + return AHARDWAREBUFFER_FORMAT_R8_UNORM; +#endif + default: + return AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + } +} + +} // namespace RNSkia + +#endif \ No newline at end of file diff --git a/package/android/cpp/rnskia-android/AHardwareBufferUtils.h b/package/android/cpp/rnskia-android/AHardwareBufferUtils.h new file mode 100644 index 0000000000..afa6febcb4 --- /dev/null +++ b/package/android/cpp/rnskia-android/AHardwareBufferUtils.h @@ -0,0 +1,13 @@ +#pragma once + +#include "include/core/SkColorType.h" + +#if __ANDROID_API__ >= 26 + +namespace RNSkia { + +uint32_t GetBufferFormatFromSkColorType(SkColorType bufferFormat); + +} // namespace RNSkia + +#endif \ No newline at end of file diff --git a/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h b/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h index ad834eb0fe..7c04af6147 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h +++ b/package/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h @@ -1,10 +1,15 @@ #pragma once +// TODO: Add android flags +#if __ANDROID_API__ >= 26 +#include +#endif #include #include #include #include +#include "AHardwareBufferUtils.h" #include "JniPlatformContext.h" #include "RNSkPlatformContext.h" #include "SkiaOpenGLSurfaceFactory.h" @@ -52,6 +57,72 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext { return SkiaOpenGLSurfaceFactory::makeImageFromHardwareBuffer(buffer); } + void releasePlatformBuffer(uint64_t pointer) override { +#if __ANDROID_API__ >= 26 + AHardwareBuffer *buffer = reinterpret_cast(pointer); + AHardwareBuffer_release(buffer); +#endif + } + + uint64_t makePlatformBuffer(sk_sp image) override { +#if __ANDROID_API__ >= 26 + auto bytesPerPixel = image->imageInfo().bytesPerPixel(); + int bytesPerRow = image->width() * bytesPerPixel; + auto buf = SkData::MakeUninitialized(image->width() * image->height() * + bytesPerPixel); + SkImageInfo info = + SkImageInfo::Make(image->width(), image->height(), image->colorType(), + image->alphaType()); + image->readPixels(nullptr, info, const_cast(buf->data()), + bytesPerRow, 0, 0); + const void *pixelData = buf->data(); + + // Define the buffer description + AHardwareBuffer_Desc desc = {}; + // TODO: use image info here + desc.width = image->width(); + desc.height = image->height(); + desc.layers = 1; // Single image layer + desc.format = GetBufferFormatFromSkColorType( + image->colorType()); // Assuming the image + // is in this format + desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | + AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | + AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; + desc.stride = bytesPerRow; // Stride in pixels, not in bytes + + // Allocate the buffer + AHardwareBuffer *buffer = nullptr; + if (AHardwareBuffer_allocate(&desc, &buffer) != 0) { + // Handle allocation failure + return 0; + } + + // Map the buffer to gain access to its memory + void *mappedBuffer = nullptr; + AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, + nullptr, &mappedBuffer); + if (mappedBuffer == nullptr) { + // Handle mapping failure + AHardwareBuffer_release(buffer); + return 0; + } + + // Copy the image data to the buffer + memcpy(mappedBuffer, pixelData, desc.height * bytesPerRow); + + // Unmap the buffer + AHardwareBuffer_unlock(buffer, nullptr); + + // Return the buffer pointer as a uint64_t. It's the caller's responsibility + // to manage this buffer. + return reinterpret_cast(buffer); +#else + return 0; +#endif + } + sk_sp createFontMgr() override { return SkFontMgr_New_Android(nullptr); } diff --git a/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp index e8e7af64ac..377a4e7520 100644 --- a/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp +++ b/package/android/cpp/rnskia-android/SkiaOpenGLSurfaceFactory.cpp @@ -18,6 +18,15 @@ thread_local SkiaOpenGLContext ThreadContextHolder::ThreadSkiaOpenGLContext; sk_sp SkiaOpenGLSurfaceFactory::makeImageFromHardwareBuffer(void *buffer) { #if __ANDROID_API__ >= 26 + // Setup OpenGL and Skia: + if (!SkiaOpenGLHelper::createSkiaDirectContextIfNecessary( + &ThreadContextHolder::ThreadSkiaOpenGLContext)) { + + RNSkLogger::logToConsole( + "Could not create Skia Surface from native window / surface. " + "Failed creating Skia Direct Context"); + return nullptr; + } const AHardwareBuffer *hardwareBuffer = static_cast(buffer); DeleteImageProc deleteImageProc = nullptr; diff --git a/package/cpp/api/JsiPlatformBuffer.h b/package/cpp/api/JsiPlatformBuffer.h new file mode 100644 index 0000000000..b2bb75b2f0 --- /dev/null +++ b/package/cpp/api/JsiPlatformBuffer.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +#include "JsiSkImage.h" + +namespace RNSkia { + +namespace jsi = facebook::jsi; + +/** + Implementation of the ParagraphBuilderFactory for making ParagraphBuilder JSI + object + */ +class JsiPlatformBufferFactory : public JsiSkHostObject { +public: + JSI_HOST_FUNCTION(MakeFromImage) { + auto image = JsiSkImage::fromValue(runtime, arguments[0]); + image->makeNonTextureImage(); + + uint64_t pointer = getContext()->makePlatformBuffer(image); + jsi::HostFunctionType deleteFunc = + [=](jsi::Runtime &runtime, const jsi::Value &thisArg, + const jsi::Value *args, size_t count) -> jsi::Value { + getContext()->releasePlatformBuffer(pointer); + return jsi::Value::undefined(); + }; + return jsi::BigInt::fromUint64(runtime, pointer); + } + + JSI_HOST_FUNCTION(Release) { + + jsi::BigInt pointer = arguments[0].asBigInt(runtime); + const uintptr_t platformBufferPointer = pointer.asUint64(runtime); + + getContext()->releasePlatformBuffer(platformBufferPointer); + return jsi::Value::undefined(); + } + + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiPlatformBufferFactory, Release), + JSI_EXPORT_FUNC(JsiPlatformBufferFactory, MakeFromImage)) + + explicit JsiPlatformBufferFactory( + std::shared_ptr context) + : JsiSkHostObject(std::move(context)) {} +}; + +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h index 11e4c3c348..db73ad045e 100644 --- a/package/cpp/api/JsiSkApi.h +++ b/package/cpp/api/JsiSkApi.h @@ -6,6 +6,7 @@ #include "JsiSkHostObjects.h" +#include "JsiPlatformBuffer.h" #include "JsiSkAnimatedImage.h" #include "JsiSkAnimatedImageFactory.h" #include "JsiSkColor.h" @@ -122,6 +123,9 @@ class JsiSkApi : public JsiSkHostObject { installReadonlyProperty( "ParagraphBuilder", std::make_shared(context)); + + installReadonlyProperty( + "PlatformBuffer", std::make_shared(context)); } }; } // namespace RNSkia diff --git a/package/cpp/api/JsiSkImageFactory.h b/package/cpp/api/JsiSkImageFactory.h index a4d708315d..19ae0b9d00 100644 --- a/package/cpp/api/JsiSkImageFactory.h +++ b/package/cpp/api/JsiSkImageFactory.h @@ -82,7 +82,7 @@ class JsiSkImageFactory : public JsiSkHostObject { JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromViewTag), JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromPlatformBuffer), - JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImage), ) + JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImage)) explicit JsiSkImageFactory(std::shared_ptr context) : JsiSkHostObject(std::move(context)) {} diff --git a/package/cpp/rnskia/RNSkPlatformContext.h b/package/cpp/rnskia/RNSkPlatformContext.h index 8f8b128513..03a038ee95 100644 --- a/package/cpp/rnskia/RNSkPlatformContext.h +++ b/package/cpp/rnskia/RNSkPlatformContext.h @@ -142,6 +142,10 @@ class RNSkPlatformContext { */ virtual sk_sp makeImageFromPlatformBuffer(void *buffer) = 0; + virtual void releasePlatformBuffer(uint64_t pointer) = 0; + + virtual uint64_t makePlatformBuffer(sk_sp image) = 0; + /** * Return the Platform specific font manager */ diff --git a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h index 9bbf6f7263..92dd6ebbdb 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h +++ b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.h @@ -61,6 +61,10 @@ class RNSkiOSPlatformContext : public RNSkPlatformContext { sk_sp makeImageFromPlatformBuffer(void *buffer) override; + uint64_t makePlatformBuffer(sk_sp image) override; + + void releasePlatformBuffer(uint64_t pointer) override; + virtual void performStreamOperation( const std::string &sourceUri, const std::function)> &op) override; diff --git a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm index c31d031359..b3134ee5e8 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm @@ -57,6 +57,89 @@ std::thread(loader).detach(); } +void RNSkiOSPlatformContext::releasePlatformBuffer(uint64_t pointer) { + CMSampleBufferRef sampleBuffer = reinterpret_cast(pointer); + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + if (sampleBuffer) { + CFRelease(sampleBuffer); + } + if (pixelBuffer) { + CFRelease(pixelBuffer); + } +} + +uint64_t RNSkiOSPlatformContext::makePlatformBuffer(sk_sp image) { + auto bytesPerPixel = image->imageInfo().bytesPerPixel(); + int bytesPerRow = image->width() * bytesPerPixel; + auto buf = SkData::MakeUninitialized(image->width() * image->height() * + bytesPerPixel); + SkImageInfo info = SkImageInfo::Make(image->width(), image->height(), + image->colorType(), image->alphaType()); + image->readPixels(nullptr, info, const_cast(buf->data()), bytesPerRow, + 0, 0); + auto pixelData = const_cast(buf->data()); + + // Create a CVPixelBuffer from the raw pixel data + CVPixelBufferRef pixelBuffer = nullptr; + // OSType pixelFormatType = MapSkColorTypeToOSType(image->colorType()); + + // You will need to fill in the details for creating the pixel buffer + // CVPixelBufferCreateWithBytes or CVPixelBufferCreateWithPlanarBytes + // Create the CVPixelBuffer with the image data + void *context = static_cast( + new sk_sp(buf)); // Create a copy for the context + CVReturn r = CVPixelBufferCreateWithBytes( + nullptr, // allocator + image->width(), image->height(), kCVPixelFormatType_32BGRA, + pixelData, // pixel data + bytesPerRow, // bytes per row + [](void *releaseRefCon, const void *baseAddress) { // release callback + auto buf = static_cast *>(releaseRefCon); + buf->reset(); // This effectively calls unref on the SkData object + delete buf; // Cleanup the dynamically allocated context + }, + context, // release callback context + nullptr, // pixel buffer attributes + &pixelBuffer // the newly created pixel buffer + ); + + if (r != kCVReturnSuccess) { + return 0; // or handle error appropriately + } + + // Wrap the CVPixelBuffer in a CMSampleBuffer + CMSampleBufferRef sampleBuffer = nullptr; + + CMFormatDescriptionRef formatDescription = nullptr; + CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, + &formatDescription); + + // Assuming no specific timing is required, we initialize the timing info to + // zero. + CMSampleTimingInfo timingInfo = {0}; + timingInfo.duration = kCMTimeInvalid; // Indicate an unknown duration. + timingInfo.presentationTimeStamp = kCMTimeZero; // Start at time zero. + timingInfo.decodeTimeStamp = kCMTimeInvalid; // No specific decode time. + + // Create the sample buffer. + OSStatus status = CMSampleBufferCreateReadyWithImageBuffer( + kCFAllocatorDefault, pixelBuffer, formatDescription, &timingInfo, + &sampleBuffer); + + if (status != noErr) { + if (formatDescription) { + CFRelease(formatDescription); + } + if (pixelBuffer) { + CFRelease(pixelBuffer); + } + return 0; + } + + // Return sampleBuffer casted to uint64_t + return reinterpret_cast(sampleBuffer); +} + void RNSkiOSPlatformContext::raiseError(const std::exception &err) { RCTFatal(RCTErrorWithMessage([NSString stringWithUTF8String:err.what()])); } @@ -69,7 +152,32 @@ sk_sp RNSkiOSPlatformContext::makeImageFromPlatformBuffer(void *buffer) { CMSampleBufferRef sampleBuffer = (CMSampleBufferRef)buffer; - return SkiaMetalSurfaceFactory::makeImageFromCMSampleBuffer(sampleBuffer); + // DO the CPU transfer (debugging only) + // Step 1: Extract the CVPixelBufferRef from the CMSampleBufferRef + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + + // Step 2: Lock the pixel buffer to access the raw pixel data + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + + // Step 3: Get information about the image + void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer); + size_t width = CVPixelBufferGetWidth(pixelBuffer); + size_t height = CVPixelBufferGetHeight(pixelBuffer); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); + + // Assuming the pixel format is 32BGRA, which is common for iOS video frames. + // You might need to adjust this based on the actual pixel format. + SkImageInfo info = SkImageInfo::Make(width, height, kRGBA_8888_SkColorType, + kUnpremul_SkAlphaType); + + // Step 4: Create an SkImage from the pixel buffer + sk_sp data = + SkData::MakeWithoutCopy(baseAddress, height * bytesPerRow); + sk_sp image = SkImages::RasterFromData(info, data, bytesPerRow); + auto texture = SkiaMetalSurfaceFactory::makeTextureFromImage(image); + // Step 5: Unlock the pixel buffer + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return texture; } sk_sp RNSkiOSPlatformContext::createFontMgr() { diff --git a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h index cbaa05be2f..8d9ac40f8e 100644 --- a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h +++ b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.h @@ -26,12 +26,10 @@ class SkiaMetalSurfaceFactory { int height); static sk_sp makeOffscreenSurface(int width, int height); - static sk_sp - makeImageFromCMSampleBuffer(CMSampleBufferRef sampleBuffer); + static sk_sp makeTextureFromImage(sk_sp image); private: static id device; static bool createSkiaDirectContextIfNecessary(SkiaMetalContext *threadContext); - static CVMetalTextureCacheRef getTextureCache(); }; diff --git a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm index 3dde746d12..0e95d2c1bf 100644 --- a/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm +++ b/package/ios/RNSkia-iOS/SkiaMetalSurfaceFactory.mm @@ -13,24 +13,10 @@ #import #import #import +#import #pragma clang diagnostic pop -#include -#if TARGET_RT_BIG_ENDIAN -#define FourCC2Str(fourcc) \ - (const char[]) { \ - *((char *)&fourcc), *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 2), \ - *(((char *)&fourcc) + 3), 0 \ - } -#else -#define FourCC2Str(fourcc) \ - (const char[]) { \ - *(((char *)&fourcc) + 3), *(((char *)&fourcc) + 2), \ - *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 0), 0 \ - } -#endif - thread_local SkiaMetalContext ThreadContextHolder::ThreadSkiaMetalContext; struct OffscreenRenderContext { @@ -120,88 +106,12 @@ return surface; } -CVMetalTextureCacheRef SkiaMetalSurfaceFactory::getTextureCache() { - static thread_local CVMetalTextureCacheRef textureCache = nil; - static thread_local size_t accessCounter = 0; - if (textureCache == nil) { - // Create a new Texture Cache - auto result = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, - nil, &textureCache); - if (result != kCVReturnSuccess || textureCache == nil) { - throw std::runtime_error("Failed to create Metal Texture Cache!"); - } - } - accessCounter++; - if (accessCounter > 5) { - // Every 5 accesses, we perform some internal recycling/housekeeping - // operations. - CVMetalTextureCacheFlush(textureCache, 0); - accessCounter = 0; - } - return textureCache; -} - -sk_sp SkiaMetalSurfaceFactory::makeImageFromCMSampleBuffer( - CMSampleBufferRef sampleBuffer) { +sk_sp +SkiaMetalSurfaceFactory::makeTextureFromImage(sk_sp image) { if (!SkiaMetalSurfaceFactory::createSkiaDirectContextIfNecessary( &ThreadContextHolder::ThreadSkiaMetalContext)) { throw std::runtime_error("Failed to create Skia Context for this Thread!"); } - - if (!CMSampleBufferIsValid(sampleBuffer)) { - throw std::runtime_error("The given CMSampleBuffer is not valid!"); - } - - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - double width = CVPixelBufferGetWidth(pixelBuffer); - double height = CVPixelBufferGetHeight(pixelBuffer); - - // Make sure the format is RGB (BGRA_8888) - OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer); - if (format != kCVPixelFormatType_32BGRA) { - // TODO: Also support YUV (kCVPixelFormatType_420YpCbCr8Planar) as that is - // much more efficient! - auto error = std::string("CMSampleBuffer has unknown Pixel Format (") + - FourCC2Str(format) + - std::string(") - cannot convert to SkImage!"); - throw std::runtime_error(error); - } - - CVMetalTextureCacheRef textureCache = getTextureCache(); - - // Convert CMSampleBuffer* -> CVMetalTexture* - CVMetalTextureRef cvTexture; - CVReturn result = CVMetalTextureCacheCreateTextureFromImage( - kCFAllocatorDefault, textureCache, pixelBuffer, nil, - MTLPixelFormatBGRA8Unorm, width, height, - 0, // plane index - &cvTexture); - if (result != kCVReturnSuccess) { - throw std::runtime_error( - "Failed to create Metal Texture from CMSampleBuffer! Result: " + - std::to_string(result)); - } - - id mtlTexture = CVMetalTextureGetTexture(cvTexture); - if (mtlTexture == nil) { - throw std::runtime_error( - "Failed to convert CMSampleBuffer to SkImage - cannot get MTLTexture!"); - } - - // Convert the rendered MTLTexture to an SkImage - GrMtlTextureInfo textureInfo; - textureInfo.fTexture.retain((__bridge void *)mtlTexture); - GrBackendTexture backendTexture((int)mtlTexture.width, (int)mtlTexture.height, - skgpu::Mipmapped::kNo, textureInfo); - - auto context = ThreadContextHolder::ThreadSkiaMetalContext.skContext; - // TODO: Adopt or Borrow? - auto image = SkImages::AdoptTextureFrom( - context.get(), backendTexture, kTopLeft_GrSurfaceOrigin, - kBGRA_8888_SkColorType, kOpaque_SkAlphaType, SkColorSpace::MakeSRGB()); - - // Release the Texture wrapper (it will still be strong) - CFRelease(cvTexture); - - return image; + return SkImages::TextureFromImage( + ThreadContextHolder::ThreadSkiaMetalContext.skContext.get(), image); } diff --git a/package/src/__tests__/snapshots/cyan-buffer.png b/package/src/__tests__/snapshots/cyan-buffer.png new file mode 100644 index 0000000000..c1228c55e4 Binary files /dev/null and b/package/src/__tests__/snapshots/cyan-buffer.png differ diff --git a/package/src/__tests__/snapshots/platform-buffer.png b/package/src/__tests__/snapshots/platform-buffer.png new file mode 100644 index 0000000000..888e185e62 Binary files /dev/null and b/package/src/__tests__/snapshots/platform-buffer.png differ diff --git a/package/src/renderer/__tests__/e2e/PlatformBuffer.spec.tsx b/package/src/renderer/__tests__/e2e/PlatformBuffer.spec.tsx new file mode 100644 index 0000000000..d4a32e8917 --- /dev/null +++ b/package/src/renderer/__tests__/e2e/PlatformBuffer.spec.tsx @@ -0,0 +1,90 @@ +import { checkImage } from "../../../__tests__/setup"; +import { setupSkia } from "../../../skia/__tests__/setup"; +import { surface } from "../setup"; + +const shouldPlatformBufferTestRun = () => { + // Skip outside iOS and Android + if (surface.OS !== "ios" && surface.OS !== "android") { + return false; + } + // Skip test on Fabric (it runs on API Level 21 which doesn't support platform buffers) + if (surface.arch === "fabric" && surface.OS === "android") { + return false; + } + return true; +}; + +describe("Platform Buffers", () => { + it("On non supported platforms MakeImageFromPlatformBuffer() should throw", async () => { + const { Skia: Sk } = setupSkia(); + if (!shouldPlatformBufferTestRun()) { + return; + } + const result = await surface.eval((Skia) => { + const sur = Skia.Surface.Make(256, 256)!; + const canvas = sur.getCanvas(); + canvas.drawColor(Skia.Color("cyan")); + const platformBuffer = Skia.PlatformBuffer.MakeFromImage( + sur.makeImageSnapshot() + ); + return platformBuffer.toString(); + }); + const pointer = BigInt(result); + expect(pointer).not.toBe(BigInt(0)); + const t = () => { + Sk.Image.MakeImageFromPlatformBuffer(pointer); + }; + expect(t).toThrow(Error); + // Now we need to release the platform buffer + const success = await surface.eval( + (Skia, ctx) => { + Skia.PlatformBuffer.Release(BigInt(ctx.pointer)); + return true; + }, + { pointer: pointer.toString() } + ); + expect(success).toBe(true); + }); + it("creates a platform buffer from an image", async () => { + if (!shouldPlatformBufferTestRun()) { + return; + } + const result = await surface.eval((Skia) => { + const sur = Skia.Surface.Make(256, 256)!; + const canvas = sur.getCanvas(); + const paint = Skia.Paint(); + paint.setColor(Skia.Color("cyan")); + canvas.drawCircle(128, 128, 128, paint); + const platformBuffer = Skia.PlatformBuffer.MakeFromImage( + sur.makeImageSnapshot() + ); + const r = platformBuffer.toString(); + Skia.PlatformBuffer.Release(platformBuffer); + return r; + }); + expect(BigInt(result)).not.toBe(BigInt(0)); + }); + it("creates an image from a platform buffer", async () => { + const { Skia: Sk } = setupSkia(); + // Skip outside iOS and Android + if (!shouldPlatformBufferTestRun()) { + return; + } + const result = await surface.eval((Skia) => { + const sur = Skia.Surface.Make(256, 256)!; + const canvas = sur.getCanvas(); + canvas.drawColor(Skia.Color("cyan")); + const platformBuffer = Skia.PlatformBuffer.MakeFromImage( + sur.makeImageSnapshot() + ); + const image = Skia.Image.MakeImageFromPlatformBuffer(platformBuffer); + Skia.PlatformBuffer.Release(platformBuffer); + return Array.from(image.encodeToBytes()); + }); + const image = Sk.Image.MakeImageFromEncoded( + Sk.Data.fromBytes(new Uint8Array(result)) + )!; + expect(image).not.toBeNull(); + checkImage(image, "snapshots/cyan-buffer.png"); + }); +}); diff --git a/package/src/skia/types/PlatformBuffer/PlatformBufferFactory.ts b/package/src/skia/types/PlatformBuffer/PlatformBufferFactory.ts new file mode 100644 index 0000000000..34c2bd9f61 --- /dev/null +++ b/package/src/skia/types/PlatformBuffer/PlatformBufferFactory.ts @@ -0,0 +1,14 @@ +import type { SkImage } from "../Image"; + +export type PlatformBuffer = bigint; + +export interface PlatformBufferFactory { + /** + * Copy pixels to a platform buffer. (for testing purposes) + */ + MakeFromImage: (image: SkImage) => PlatformBuffer; + /** + * Release a platform buffer that was created with `MakeFromImage`. + */ + Release: (platformBuffer: PlatformBuffer) => void; +} diff --git a/package/src/skia/types/PlatformBuffer/index.ts b/package/src/skia/types/PlatformBuffer/index.ts new file mode 100644 index 0000000000..f092969e32 --- /dev/null +++ b/package/src/skia/types/PlatformBuffer/index.ts @@ -0,0 +1 @@ +export * from "./PlatformBufferFactory"; diff --git a/package/src/skia/types/Skia.ts b/package/src/skia/types/Skia.ts index 9414954c46..ea8276a412 100644 --- a/package/src/skia/types/Skia.ts +++ b/package/src/skia/types/Skia.ts @@ -30,6 +30,7 @@ import type { Color, SkColor } from "./Color"; import type { TypefaceFontProviderFactory } from "./Paragraph/TypefaceFontProviderFactory"; import type { AnimatedImageFactory } from "./AnimatedImage"; import type { ParagraphBuilderFactory } from "./Paragraph/ParagraphBuilder"; +import type { PlatformBufferFactory } from "./PlatformBuffer"; /** * Declares the interface for the native Skia API @@ -93,6 +94,6 @@ export interface Skia { SVG: SVGFactory; TextBlob: TextBlobFactory; Surface: SurfaceFactory; - // Paragraph ParagraphBuilder: ParagraphBuilderFactory; + PlatformBuffer: PlatformBufferFactory; } diff --git a/package/src/skia/types/index.ts b/package/src/skia/types/index.ts index 8d0ce140b4..275c8fd6db 100644 --- a/package/src/skia/types/index.ts +++ b/package/src/skia/types/index.ts @@ -29,3 +29,4 @@ export * from "./TextBlob"; export * from "./Size"; export * from "./Paragraph"; export * from "./Matrix4"; +export * from "./PlatformBuffer"; diff --git a/package/src/skia/web/JsiSkImageFactory.ts b/package/src/skia/web/JsiSkImageFactory.ts index ffa932a861..74b2de7ab3 100644 --- a/package/src/skia/web/JsiSkImageFactory.ts +++ b/package/src/skia/web/JsiSkImageFactory.ts @@ -1,7 +1,12 @@ import type { CanvasKit } from "canvaskit-wasm"; -import type { SkData, ImageInfo, SkImage } from "../types"; -import type { ImageFactory } from "../types/Image/ImageFactory"; +import type { + SkData, + ImageInfo, + SkImage, + PlatformBuffer, + ImageFactory, +} from "../types"; import { Host, getEnum } from "./Host"; import { JsiSkImage } from "./JsiSkImage"; @@ -19,9 +24,9 @@ export class JsiSkImageFactory extends Host implements ImageFactory { return Promise.resolve(null); } - MakeImageFromPlatformBuffer(platformBuffer: bigint): SkImage { + MakeImageFromPlatformBuffer(_platformBuffer: PlatformBuffer): SkImage { throw new Error( - `MakeImageFromPlatformBuffer(${platformBuffer}) is only available on iOS and Android!` + "MakeImageFromPlatformBuffer() is only available on iOS and Android!" ); } diff --git a/package/src/skia/web/JsiSkPlatformBufferFactory.ts b/package/src/skia/web/JsiSkPlatformBufferFactory.ts new file mode 100644 index 0000000000..9d62c58304 --- /dev/null +++ b/package/src/skia/web/JsiSkPlatformBufferFactory.ts @@ -0,0 +1,22 @@ +import type { CanvasKit } from "canvaskit-wasm"; + +import type { PlatformBuffer, PlatformBufferFactory, SkImage } from "../types"; + +import { Host, NotImplementedOnRNWeb } from "./Host"; + +export class JsiSkPlatformBufferFactory + extends Host + implements PlatformBufferFactory +{ + constructor(CanvasKit: CanvasKit) { + super(CanvasKit); + } + + MakeFromImage(_image: SkImage): PlatformBuffer { + throw new NotImplementedOnRNWeb(); + } + + Release(_platformBuffer: PlatformBuffer) { + throw new NotImplementedOnRNWeb(); + } +} diff --git a/package/src/skia/web/JsiSkia.ts b/package/src/skia/web/JsiSkia.ts index 266b3c7072..270b033417 100644 --- a/package/src/skia/web/JsiSkia.ts +++ b/package/src/skia/web/JsiSkia.ts @@ -41,6 +41,7 @@ import { JsiSkTypefaceFontProviderFactory } from "./JsiSkTypefaceFontProviderFac import { JsiSkFontMgrFactory } from "./JsiSkFontMgrFactory"; import { JsiSkAnimatedImageFactory } from "./JsiSkAnimatedImageFactory"; import { JsiSkParagraphBuilderFactory } from "./JsiSkParagraphBuilderFactory"; +import { JsiSkPlatformBufferFactory } from "./JsiSkPlatformBufferFactory"; export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({ Point: (x: number, y: number) => @@ -125,4 +126,5 @@ export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({ TypefaceFontProvider: new JsiSkTypefaceFontProviderFactory(CanvasKit), FontMgr: new JsiSkFontMgrFactory(CanvasKit), ParagraphBuilder: new JsiSkParagraphBuilderFactory(CanvasKit), + PlatformBuffer: new JsiSkPlatformBufferFactory(CanvasKit), });