Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MakeTextureFromImage #2637

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext {

void stopDrawLoop() override { _jniPlatformContext->stopDrawLoop(); }

GrDirectContext *getDirectContext() override {
return ThreadContextHolder::ThreadSkiaOpenGLContext.directContext.get();
}

private:
JniPlatformContext *_jniPlatformContext;
};
Expand Down
10 changes: 10 additions & 0 deletions packages/skia/cpp/api/JsiSkImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
runtime, std::make_shared<JsiSkImage>(getContext(), std::move(image)));
}

JSI_HOST_FUNCTION(isTextureBacked) {
wcandillon marked this conversation as resolved.
Show resolved Hide resolved
return static_cast<bool>(getObject()->isTextureBacked());
}

JSI_HOST_FUNCTION(textureSize) {
return static_cast<double>(getObject()->textureSize());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usefull to have an idea how much memory your textures use. In the future, we could allow users to configure skia memory cache size so you can then calculate also for each texture the pressure and also call purge when you need.

For now added it so we can collect info on how much GPU memory we used by our textures.

}

EXPORT_JSI_API_TYPENAME(JsiSkImage, Image)

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImage, width),
Expand All @@ -197,6 +205,8 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
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<RNSkPlatformContext> context,
Expand Down
24 changes: 23 additions & 1 deletion packages/skia/cpp/api/JsiSkImageFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "JsiSkHostObjects.h"
#include "JsiSkImage.h"
#include "JsiSkImageInfo.h"
#include <include/gpu/ganesh/SkImageGanesh.h>

namespace RNSkia {

Expand Down Expand Up @@ -78,11 +79,32 @@ class JsiSkImageFactory : public JsiSkHostObject {
});
}

JSI_HOST_FUNCTION(MakeTextureFromImage) {
wcandillon marked this conversation as resolved.
Show resolved Hide resolved
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<JsiSkImage>(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<RNSkPlatformContext> context)
: JsiSkHostObject(std::move(context)) {}
Expand Down
2 changes: 2 additions & 0 deletions packages/skia/cpp/rnskia/RNSkPlatformContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,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
Expand Down
1 change: 1 addition & 0 deletions packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class RNSkiOSPlatformContext : public RNSkPlatformContext {
const std::string &sourceUri,
const std::function<void(std::unique_ptr<SkStreamAsset>)> &op) override;

GrDirectContext *getDirectContext() override;
void raiseError(const std::exception &err) override;
sk_sp<SkSurface> makeOffscreenSurface(int width, int height) override;
sk_sp<SkFontMgr> createFontMgr() override;
Expand Down
4 changes: 4 additions & 0 deletions packages/skia/ios/RNSkia-iOS/RNSkiOSPlatformContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,8 @@
}
}

GrDirectContext *RNSkiOSPlatformContext::getDirectContext() {
return ThreadContextHolder::ThreadSkiaMetalContext.skContext.get();
}

} // namespace RNSkia
27 changes: 6 additions & 21 deletions packages/skia/src/external/reanimated/textures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,12 @@ export const usePictureAsTexture = (
};

export const useImageAsTexture = (source: DataSourceParam) => {
const texture = Rea.useSharedValue<SkImage | null>(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;
};
6 changes: 5 additions & 1 deletion packages/skia/src/renderer/HostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const appendNode = (parent: Node<unknown>, child: Node<unknown>) => {
parent.addChild(child);
};

const removeNode = (parent: Node<unknown>, child: Node<unknown>, unmounted = false) => {
const removeNode = (
parent: Node<unknown>,
child: Node<unknown>,
unmounted = false
) => {
// If the drawing is unmounted we don't want to update it.
// We can just stop the reanimated mappers
unbindReanimatedNode(child);
Expand Down
41 changes: 40 additions & 1 deletion packages/skia/src/renderer/__tests__/e2e/Offscreen.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,49 @@
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 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 CI (software adapter)
// therefore these would fail
if (ctx.CI) {
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 }
);
if (surface.OS === "web" || surface.OS === "node") {
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(
Expand Down
13 changes: 13 additions & 0 deletions packages/skia/src/skia/types/Image/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
8 changes: 8 additions & 0 deletions packages/skia/src/skia/types/Image/ImageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
8 changes: 8 additions & 0 deletions packages/skia/src/skia/web/JsiSkImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export class JsiSkImage extends HostObject<Image, "Image"> implements SkImage {
super(CanvasKit, ref, "Image");
}

isTextureBacked(): boolean {
return false;
}

textureSize(): number {
return 0;
}

height() {
return this.ref.height();
}
Expand Down
7 changes: 6 additions & 1 deletion packages/skia/src/skia/web/JsiSkImageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export class JsiSkImageFactory extends Host implements ImageFactory {
super(CanvasKit);
}

MakeTextureFromImage(image: SkImage) {
return image;
}

MakeImageFromViewTag(viewTag: number): Promise<SkImage | null> {
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);
}
Expand Down
Loading