diff --git a/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h b/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h index 1892fb50b5..137500274d 100644 --- a/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h +++ b/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h @@ -168,6 +168,10 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext { void stopDrawLoop() override { _jniPlatformContext->stopDrawLoop(); } + GrDirectContext *getDirectContext() override { + return ThreadContextHolder::ThreadSkiaOpenGLContext.directContext.get(); + } + private: JniPlatformContext *_jniPlatformContext; }; diff --git a/packages/skia/cpp/api/JsiSkImage.h b/packages/skia/cpp/api/JsiSkImage.h index 585197cc4f..6464b7d96d 100644 --- a/packages/skia/cpp/api/JsiSkImage.h +++ b/packages/skia/cpp/api/JsiSkImage.h @@ -199,6 +199,14 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { runtime, std::make_shared(getContext(), rasterImage)); } + JSI_HOST_FUNCTION(isTextureBacked) { + return static_cast(getObject()->isTextureBacked()); + } + + JSI_HOST_FUNCTION(textureSize) { + return static_cast(getObject()->textureSize()); + } + EXPORT_JSI_API_TYPENAME(JsiSkImage, Image) JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImage, width), @@ -210,6 +218,8 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { JSI_EXPORT_FUNC(JsiSkImage, encodeToBase64), JSI_EXPORT_FUNC(JsiSkImage, readPixels), JSI_EXPORT_FUNC(JsiSkImage, makeNonTextureImage), + JSI_EXPORT_FUNC(JsiSkImage, isTextureBacked), + JSI_EXPORT_FUNC(JsiSkImage, textureSize), JSI_EXPORT_FUNC(JsiSkImage, dispose)) JsiSkImage(std::shared_ptr context, diff --git a/packages/skia/cpp/api/JsiSkImageFactory.h b/packages/skia/cpp/api/JsiSkImageFactory.h index 14e0da7dbe..e28c44e36b 100644 --- a/packages/skia/cpp/api/JsiSkImageFactory.h +++ b/packages/skia/cpp/api/JsiSkImageFactory.h @@ -10,6 +10,7 @@ #include "JsiSkHostObjects.h" #include "JsiSkImage.h" #include "JsiSkImageInfo.h" +#include namespace RNSkia { @@ -78,11 +79,32 @@ class JsiSkImageFactory : public JsiSkHostObject { }); } + JSI_HOST_FUNCTION(MakeTextureFromImage) { + auto directContext = getContext()->getDirectContext(); + + if (directContext == nullptr) { + return jsi::Value::null(); + } + + auto image = JsiSkImage::fromValue(runtime, arguments[0]); + + auto texture = SkImages::TextureFromImage(directContext, image); + + if (texture == nullptr) { + return jsi::Value::null(); + } + + return jsi::Object::createFromHostObject( + runtime, + std::make_shared(getContext(), std::move(texture))); + } + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromEncoded), JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromViewTag), JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImageFromNativeBuffer), - JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImage)) + JSI_EXPORT_FUNC(JsiSkImageFactory, MakeImage), + JSI_EXPORT_FUNC(JsiSkImageFactory, MakeTextureFromImage)) explicit JsiSkImageFactory(std::shared_ptr context) : JsiSkHostObject(std::move(context)) {} diff --git a/packages/skia/cpp/rnskia/RNSkPlatformContext.h b/packages/skia/cpp/rnskia/RNSkPlatformContext.h index 6ed5ca50d7..aea473878e 100644 --- a/packages/skia/cpp/rnskia/RNSkPlatformContext.h +++ b/packages/skia/cpp/rnskia/RNSkPlatformContext.h @@ -173,6 +173,8 @@ class RNSkPlatformContext { }); } + virtual GrDirectContext *getDirectContext() = 0; + /** * Raises an exception on the platform. This function does not necessarily * throw an exception and stop execution, so it is important to stop execution diff --git a/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.h b/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.h index 8dab8e789a..b801a7efd7 100644 --- a/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.h +++ b/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.h @@ -74,6 +74,7 @@ class RNSkiOSPlatformContext : public RNSkPlatformContext { const std::string &sourceUri, const std::function)> &op) override; + GrDirectContext *getDirectContext() override; void raiseError(const std::exception &err) override; sk_sp makeOffscreenSurface(int width, int height) override; sk_sp createFontMgr() override; diff --git a/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm b/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm index 5588b95e31..e0d4b9525f 100644 --- a/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm +++ b/packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm @@ -224,4 +224,8 @@ } } +GrDirectContext *RNSkiOSPlatformContext::getDirectContext() { + return ThreadContextHolder::ThreadSkiaMetalContext.skContext.get(); +} + } // namespace RNSkia diff --git a/packages/skia/src/external/reanimated/textures.tsx b/packages/skia/src/external/reanimated/textures.tsx index 8eecc6d737..4f511ab06e 100644 --- a/packages/skia/src/external/reanimated/textures.tsx +++ b/packages/skia/src/external/reanimated/textures.tsx @@ -67,27 +67,12 @@ export const usePictureAsTexture = ( }; export const useImageAsTexture = (source: DataSourceParam) => { + const texture = Rea.useSharedValue(null); const image = useImage(source); - const size = useMemo(() => { - if (image) { - return { width: image.width(), height: image.height() }; - } - return { width: 0, height: 0 }; - }, [image]); - const picture = useMemo(() => { - if (image) { - const recorder = Skia.PictureRecorder(); - const canvas = recorder.beginRecording({ - x: 0, - y: 0, - width: size.width, - height: size.height, - }); - canvas.drawImage(image, 0, 0); - return recorder.finishRecordingAsPicture(); - } else { - return null; + useEffect(() => { + if (image !== null) { + texture.value = Skia.Image.MakeTextureFromImage(image); } - }, [size, image]); - return usePictureAsTexture(picture, size); + }); + return texture; }; diff --git a/packages/skia/src/renderer/__tests__/e2e/Offscreen.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Offscreen.spec.tsx index 81945fc59a..4243a742c5 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Offscreen.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Offscreen.spec.tsx @@ -1,10 +1,50 @@ import React from "react"; -import { checkImage, docPath } from "../../../__tests__/setup"; +import { checkImage, CI, docPath } from "../../../__tests__/setup"; import { Circle } from "../../components"; import { surface, importSkia } from "../setup"; describe("Offscreen Drawings", () => { + it("isTextureBacked()", async () => { + const { width, height } = surface; + const supported = surface.OS !== "web" && surface.OS !== "node"; + const result = await surface.eval( + (Skia, ctx) => { + const r = ctx.width / 2; + const offscreen = Skia.Surface.MakeOffscreen(ctx.width, ctx.height)!; + if (!offscreen) { + throw new Error("Could not create offscreen surface"); + } + const canvas = offscreen.getCanvas(); + const paint = Skia.Paint(); + paint.setColor(Skia.Color("lightblue")); + canvas.drawCircle(r, r, r, paint); + offscreen.flush(); + // Currently GPU is not available in github action (software adapter) + // therefore these would fail + if (ctx.CI && ctx.supported) { + return [true, false, true]; + } + const r0 = offscreen.makeImageSnapshot(); + const r1 = r0.makeNonTextureImage(); + const r2 = Skia.Image.MakeTextureFromImage(r1); + if (!r2) { + return []; + } + return [ + r0.isTextureBacked(), + r1.isTextureBacked(), + r2.isTextureBacked(), + ]; + }, + { width, height, CI, supported } + ); + if (!supported) { + expect(result).toEqual([false, false, false]); + } else { + expect(result).toEqual([true, false, true]); + } + }); it("Should use the canvas API to build an image", async () => { const { width, height } = surface; const raw = await surface.eval( diff --git a/packages/skia/src/skia/types/Image/Image.ts b/packages/skia/src/skia/types/Image/Image.ts index 2179999cb0..92e9aa2ae4 100644 --- a/packages/skia/src/skia/types/Image/Image.ts +++ b/packages/skia/src/skia/types/Image/Image.ts @@ -130,4 +130,17 @@ export interface SkImage extends SkJSIInstance<"Image"> { * bitmap, or if encoded in a stream. */ makeNonTextureImage(): SkImage; + + /** + * Returns true if the image is backed by a GPU texture. + * Usually true if the image was uploaded manually to GPU (ImageFactory.MakeTextureFromImage) + * or if the image is a snapshot of a GPU backed surface (surface.makeImageSnapshot). + */ + isTextureBacked(): boolean; + + /** + * Returns an approximation of the amount of texture memory used by the image. + * Returns zero if the image is not texture backed or if the texture has an external format. + */ + textureSize(): number; } diff --git a/packages/skia/src/skia/types/Image/ImageFactory.ts b/packages/skia/src/skia/types/Image/ImageFactory.ts index d8dc0b82d0..5f6bb6affe 100644 --- a/packages/skia/src/skia/types/Image/ImageFactory.ts +++ b/packages/skia/src/skia/types/Image/ImageFactory.ts @@ -87,4 +87,12 @@ export interface ImageFactory { * @param bytesPerRow */ MakeImage(info: ImageInfo, data: SkData, bytesPerRow: number): SkImage | null; + + /** + * Uploads image to GPU memory and in case of success returns a texture backed image. + * The old image can be safely disposed. + * @param image - Image to be uploaded to GPU + * @returns Returns texture backed image if the image is valid, null otherwise. + */ + MakeTextureFromImage(image: SkImage): SkImage | null; } diff --git a/packages/skia/src/skia/web/JsiSkImage.ts b/packages/skia/src/skia/web/JsiSkImage.ts index 650c719914..71acf43255 100644 --- a/packages/skia/src/skia/web/JsiSkImage.ts +++ b/packages/skia/src/skia/web/JsiSkImage.ts @@ -49,6 +49,14 @@ export class JsiSkImage extends HostObject implements SkImage { super(CanvasKit, ref, "Image"); } + isTextureBacked(): boolean { + return false; + } + + textureSize(): number { + return 0; + } + height() { return this.ref.height(); } diff --git a/packages/skia/src/skia/web/JsiSkImageFactory.ts b/packages/skia/src/skia/web/JsiSkImageFactory.ts index 09b6521542..7c1e9463c8 100644 --- a/packages/skia/src/skia/web/JsiSkImageFactory.ts +++ b/packages/skia/src/skia/web/JsiSkImageFactory.ts @@ -20,9 +20,14 @@ export class JsiSkImageFactory extends Host implements ImageFactory { super(CanvasKit); } + MakeTextureFromImage(image: SkImage) { + return image; + } + MakeImageFromViewTag(viewTag: number): Promise { const view = viewTag as unknown as HTMLElement; - // TODO: Implement screenshot from view in React JS + // TODO: implement once this API is available: + // https://x.com/fserb/status/1794058245901824349 console.log(view); return Promise.resolve(null); }