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 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7199f12
feat: add MakeTextureFromImage
terrysahaidak Sep 16, 2024
cf5fe4b
:green_heart:
wcandillon Sep 17, 2024
3ea5d78
:green_heart:
wcandillon Sep 17, 2024
a3f96de
:green_heart:
wcandillon Sep 17, 2024
3d18ff7
:green_heart:
wcandillon Sep 17, 2024
ac1172f
:green_heart:
wcandillon Sep 17, 2024
b0031dc
Update useImageAsTexture implementation
wcandillon Sep 17, 2024
f753b02
remove bogus change
wcandillon Sep 18, 2024
7a23c3b
:green_heart:
wcandillon Sep 20, 2024
41c051a
Merge branch 'terrysahaidak/upload-texture' of https://github.com/ter…
wcandillon Sep 20, 2024
be6efa1
:wrench:
wcandillon Sep 20, 2024
c685193
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 25, 2024
64965bf
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 25, 2024
f84e80d
:green_heart:
wcandillon Sep 25, 2024
84a9fc8
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 26, 2024
3eb7f24
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 27, 2024
970fe0d
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 29, 2024
e0c1f6e
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 29, 2024
6ef1296
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 30, 2024
2d2764b
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Sep 30, 2024
fc707b2
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Oct 17, 2024
9f00adf
Merge branch 'main' into terrysahaidak/upload-texture
wcandillon Nov 16, 2024
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 @@ -168,6 +168,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 @@ -199,6 +199,14 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
runtime, std::make_shared<JsiSkImage>(getContext(), rasterImage));
}

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 @@ -210,6 +218,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 @@ -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
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 @@ -74,6 +74,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 @@ -224,4 +224,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;
};
42 changes: 41 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,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(
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