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

WebGPU Canvas Demo #2632

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
25 changes: 25 additions & 0 deletions apps/paper/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-wgpu (0.1.7):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- React-nativeconfig (0.75.2)
- React-NativeModulesApple (0.75.2):
- glog
Expand Down Expand Up @@ -1697,6 +1718,7 @@ DEPENDENCIES:
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
- "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)"
- "react-native-slider (from `../../../node_modules/@react-native-community/slider`)"
- react-native-wgpu (from `../../../node_modules/react-native-wgpu`)
- React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
Expand Down Expand Up @@ -1811,6 +1833,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/@shopify/react-native-skia"
react-native-slider:
:path: "../../../node_modules/@react-native-community/slider"
react-native-wgpu:
:path: "../../../node_modules/react-native-wgpu"
React-nativeconfig:
:path: "../../../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
Expand Down Expand Up @@ -1913,6 +1937,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: ab8f4a3d8180913bd78ae75dd599c94cce3d5e9a
react-native-skia: 94c1a75519ba65c8450f21867358739486b1992b
react-native-slider: 97ce0bd921f40de79cead9754546d5e4e7ba44f8
react-native-wgpu: fb9d60b0f4c63a03fb60bd986d758aeffca6ee13
React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d
React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1
React-perflogger: 8a360ccf603de6ddbe9ff8f54383146d26e6c936
Expand Down
1 change: 1 addition & 0 deletions apps/paper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-native-safe-area-context": "^4.10.9",
"react-native-screens": "^3.34.0",
"react-native-svg": "^15.6.0",
"react-native-wgpu": "^0.1.7",
"typescript": "^5.2.2"
},
"devDependencies": {
Expand Down
11 changes: 10 additions & 1 deletion apps/paper/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
FrostedCard,
SpeedTest,
Video,
WebGPU
} from "./Examples";
import { CI, Tests } from "./Tests";
import { HomeScreen } from "./Home";
Expand Down Expand Up @@ -98,7 +99,7 @@ const App = () => {
screenOptions={{
headerLeft: HeaderLeft,
}}
initialRouteName={CI ? "Tests" : "Home"}
initialRouteName={CI ? "Tests" : "WebGPU"}
>
<Stack.Screen
name="Home"
Expand All @@ -108,6 +109,14 @@ const App = () => {
title: "🎨 Skia",
}}
/>
<Stack.Screen
name="WebGPU"
key="WebGPU"
component={WebGPU}
options={{
title: "🏔️ WebGPU",
}}
/>
<Stack.Screen
key="Tests"
name="Tests"
Expand Down
37 changes: 37 additions & 0 deletions apps/paper/src/Examples/WebGPU/WebGPU.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import { useFrameCallback } from "react-native-reanimated";
import { Canvas } from "react-native-wgpu";
import { useLoop } from "../../components/Animations";
import { drawBreatheDemo, useSkiaContext } from "./utils";


export function WebGPU() {
const {ref, context} = useSkiaContext();

const progress = useLoop({ duration: 3000 });

useFrameCallback(() => {
if (!context.value) {
return;
}
const ctx = context.value;
drawBreatheDemo(ctx, progress.value);
ctx.present();
});

return (
<View style={style.container}>
<Canvas ref={ref} style={style.webgpu} />
</View>
);
}

const style = StyleSheet.create({
container: {
flex: 1,
},
webgpu: {
flex: 1,
},
});
1 change: 1 addition & 0 deletions apps/paper/src/Examples/WebGPU/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./WebGPU";
67 changes: 67 additions & 0 deletions apps/paper/src/Examples/WebGPU/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { BlendMode, BlurStyle, mix, polar2Canvas, Skia, SkiaContext } from "@shopify/react-native-skia";
import { Dimensions, PixelRatio } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { useCanvasEffect } from "react-native-wgpu";

export const useSkiaContext = () => {
const context = useSharedValue<SkiaContext | null>(null);
const ref = useCanvasEffect(() => {
const nativeSurface = ref.current!.getNativeSurface();
context.value = Skia.Context(
nativeSurface.surface,
nativeSurface.width * pd,
nativeSurface.height * pd
);
});
return {
context,
ref
}
};

const pd = PixelRatio.get();
const { width, height } = Dimensions.get("window");
const center = { x: width / 2, y: height / 2 };
const R = width / 4;

const c1 = "#61bea2";
const c2 = "#529ca0";
const root = Skia.Paint();
root.setBlendMode(BlendMode.Screen);
root.setMaskFilter(Skia.MaskFilter.MakeBlur(BlurStyle.Solid, 10, true));
const p1 = root.copy();
p1.setColor(Skia.Color(c1));
const p2 = root.copy();
p2.setColor(Skia.Color(c2));


export const drawBreatheDemo = (ctx: SkiaContext, progress: number) => {
"worklet";

const surface = ctx.getSurface();
const canvas = surface.getCanvas();
canvas.clear(Skia.Color(`rgb(36,43,56)`));
canvas.save();
canvas.scale(pd, pd);
canvas.rotate(progress * -180, center.x, center.y);
{new Array(6).fill(0).map((_, index) => {
canvas.save();
const theta = (index * (2 * Math.PI)) / 6;
const { x, y } = polar2Canvas(
{ theta, radius: progress * R },
{ x: 0, y: 0 }
);

Check warning on line 53 in apps/paper/src/Examples/WebGPU/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Nested block is redundant
const scale = mix(progress, 0.3, 1);

canvas.translate(center.x, center.y);
canvas.translate(x, y);
canvas.scale(scale, scale);
canvas.translate(-center.x, -center.y);

const paint = index % 2 ? p1 : p2;
canvas.drawCircle(center.x, center.y, R, paint);
canvas.restore();
})}
canvas.restore();

}
1 change: 1 addition & 0 deletions apps/paper/src/Examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from "./Stickers";
export * from "./FrostedCard";
export * from "./SpeedTest";
export * from "./Video";
export * from "./WebGPU";
5 changes: 5 additions & 0 deletions apps/paper/src/Home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const HomeScreen = () => {
route="API"
testId="API"
/>
<HomeScreenButton
title="🏔️ WebGPU"
description="WebGPU"
route="WebGPU"
/>
<HomeScreenButton
title="🎥 Reanimated"
description="Reanimated & Gesture Handler"
Expand Down
1 change: 1 addition & 0 deletions apps/paper/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type StackParamList = {
WebGPU: undefined;
Tests: {
title?: string;
path?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext {
return SkiaOpenGLSurfaceFactory::makeOffscreenSurface(width, height);
}

std::shared_ptr<SkiaContext> makeContextFromNativeSurface(void *surface, int width, int height) override {
return SkiaOpenGLSurfaceFactory::makeContext(reinterpret_cast<ANativeWindow*>(surface), width, height);
}

sk_sp<SkImage> makeImageFromNativeBuffer(void *buffer) override {
return SkiaOpenGLSurfaceFactory::makeImageFromHardwareBuffer(buffer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,86 @@ sk_sp<SkSurface> WindowSurfaceHolder::getSurface() {
return _skSurface;
}


sk_sp<SkSurface> AndroidSkiaContext::getSurface() {
if (_skSurface == nullptr) {

// 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;
}

// Now we can create a surface
_glSurface = SkiaOpenGLHelper::createWindowedSurface(_window);
if (_glSurface == EGL_NO_SURFACE) {
RNSkLogger::logToConsole(
"Could not create EGL Surface from native window / surface.");
return nullptr;
}

// Now make this one current
if (!SkiaOpenGLHelper::makeCurrent(
&ThreadContextHolder::ThreadSkiaOpenGLContext, _glSurface)) {
RNSkLogger::logToConsole(
"Could not create EGL Surface from native window / surface. Could "
"not set new surface as current surface.");
return nullptr;
}

// Set up parameters for the render target so that it
// matches the underlying OpenGL context.
GrGLFramebufferInfo fboInfo;

// We pass 0 as the framebuffer id, since the
// underlying Skia GrGlGpu will read this when wrapping the context in the
// render target and the GrGlGpu object.
fboInfo.fFBOID = 0;
fboInfo.fFormat = 0x8058; // GL_RGBA8

GLint stencil;
glGetIntegerv(GL_STENCIL_BITS, &stencil);

GLint samples;
glGetIntegerv(GL_SAMPLES, &samples);

auto colorType = kN32_SkColorType;

auto maxSamples =
ThreadContextHolder::ThreadSkiaOpenGLContext.directContext
->maxSurfaceSampleCountForColorType(colorType);

if (samples > maxSamples) {
samples = maxSamples;
}

auto renderTarget = GrBackendRenderTargets::MakeGL(_width, _height, samples,
stencil, fboInfo);

SkSurfaceProps props(0, kUnknown_SkPixelGeometry);

struct ReleaseContext {
EGLSurface glSurface;
};

auto releaseCtx = new ReleaseContext({_glSurface});

// Create surface object
_skSurface = SkSurfaces::WrapBackendRenderTarget(
ThreadContextHolder::ThreadSkiaOpenGLContext.directContext.get(),
renderTarget, kBottomLeft_GrSurfaceOrigin, colorType, nullptr, &props,
[](void *addr) {
auto releaseCtx = reinterpret_cast<ReleaseContext *>(addr);
SkiaOpenGLHelper::destroySurface(releaseCtx->glSurface);
delete releaseCtx;
},
reinterpret_cast<void *>(releaseCtx));
}

return _skSurface;
}

} // namespace RNSkia
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <unordered_map>

#include "SkiaOpenGLHelper.h"
#include "SkiaContext.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"
Expand Down Expand Up @@ -139,6 +140,35 @@ class WindowSurfaceHolder {
int _height = 0;
};

class AndroidSkiaContext: public SkiaContext {
public:
AndroidSkiaContext(ANativeWindow* window, int width, int height)
: _window(window), _width(width), _height(height) {
}

~AndroidSkiaContext() {
}

sk_sp<SkSurface> getSurface() override;

void present() override {
// Flush and submit the direct context
ThreadContextHolder::ThreadSkiaOpenGLContext.directContext
->flushAndSubmit();

// Swap buffers
SkiaOpenGLHelper::swapBuffers(
&ThreadContextHolder::ThreadSkiaOpenGLContext, _glSurface);
}

private:
ANativeWindow *_window;
sk_sp<SkSurface> _skSurface = nullptr;
EGLSurface _glSurface = EGL_NO_SURFACE;
int _width = 0;
int _height = 0;
};

class SkiaOpenGLSurfaceFactory {
public:
/**
Expand All @@ -152,6 +182,12 @@ class SkiaOpenGLSurfaceFactory {
static sk_sp<SkImage>
makeImageFromHardwareBuffer(void *buffer, bool requireKnownFormat = false);

static std::shared_ptr<AndroidSkiaContext>
makeContext(ANativeWindow* surface, int width, int height) {
return std::make_shared<AndroidSkiaContext>(surface, width, height);
}


/**
* Creates a windowed Skia Surface holder.
* @param width Initial width of surface
Expand Down
2 changes: 2 additions & 0 deletions packages/skia/cpp/api/JsiSkApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "JsiSkTypefaceFontProviderFactory.h"
#include "JsiSkVertices.h"
#include "JsiVideo.h"
#include "JsiSkiaContext.h"

namespace RNSkia {

Expand All @@ -69,6 +70,7 @@ class JsiSkApi : public JsiSkHostObject {
// slow to do it on demand
JsiSkFontMgrFactory::getFontMgr(getContext());
installFunction("Video", JsiVideo::createCtor(context));
installFunction("Context", JsiSkiaContext::createCtor(context));
installFunction("Font", JsiSkFont::createCtor(context));
installFunction("Paint", JsiSkPaint::createCtor(context));
installFunction("RSXform", JsiSkRSXform::createCtor(context));
Expand Down
Loading
Loading