Skip to content
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
65 changes: 58 additions & 7 deletions apps/docs/docs/canvas/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ The Canvas component is the root of your Skia drawing.
You can treat it as a regular React Native view and assign a view style.
Behind the scenes, it is using its own React renderer.

| Name | Type | Description |
|:-----|:---------|:-----------------|
| style? | `ViewStyle` | View style |
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
| onSize? | `SharedValue<Size>` | Reanimated value to which the canvas size will be assigned (see [canvas size](#canvas-size)) |
| highBitDepth? | `boolean` | Render into a surface with more than 8 bits per channel (see [high bit depth](#high-bit-depth)) |
| androidWarmup? | `boolean` | Draw the first frame directly on the Android compositor. Use it for static icons or fully opaque drawings鈥攁nimated or translucent canvases can misrender, so it remains opt-in. |
| Name | Type | Description |
| :---------------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| style? | `ViewStyle` | View style |
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
| onSize? | `SharedValue<Size>` | Reanimated value to which the canvas size will be assigned (see [canvas size](#canvas-size)) |
| onFramePresented? | `() => void` | Called on the JavaScript thread after each successful on-screen render crosses the platform presentation boundary (see [frame presentation](#frame-presentation)) |
| highBitDepth? | `boolean` | Render into a surface with more than 8 bits per channel (see [high bit depth](#high-bit-depth)) |
| androidWarmup? | `boolean` | Draw the first frame directly on the Android compositor. Use it for static icons or fully opaque drawings鈥攁nimated or translucent canvases can misrender, so it remains opt-in. |

## Canvas size

Expand Down Expand Up @@ -85,6 +86,56 @@ const Demo = () => {
};
```

## Frame presentation

`onFramePresented` runs once for every successfully rendered on-screen frame.
Unlike React commit or Skia recording callbacks, it runs after the backend has
reached its native presentation boundary. This makes it useful for removing a
placeholder, taking measurements, or starting a capture without racing the
frame that contains the pixels you are waiting for.

```tsx twoslash
import { useState } from "react";
import { Canvas, Circle } from "@shopify/react-native-skia";

const LoadingPlaceholder = () => null;

const Demo = () => {
const [presented, setPresented] = useState(false);
return (
<>
<Canvas
style={{ width: 256, height: 256 }}
onFramePresented={() => setPresented(true)}
>
<Circle cx={128} cy={128} r={96} color="cyan" />
</Canvas>
{!presented && <LoadingPlaceholder />}
</>
);
};
```

The exact native boundary depends on the renderer:

- iOS and tvOS Metal call the callback only when Metal reports a nonzero
`presentedTime`, meaning that the drawable was displayed on screen.
- Android OpenGL calls it after a successful `eglSwapBuffers`. At that point
the buffer has been handed to the system compositor; this does not prove
physical display scanout.
- Graphite calls it after WebGPU `Surface::Present()` returns. This confirms
presentation submission, not physical display scanout.
- macOS and Mac Catalyst call it after the Metal presentation command buffer
completes because `MTLDrawable.addPresentedHandler` is unavailable there.
- Web does not currently emit this callback.

The callback is delivered asynchronously on the JavaScript thread, so later JS
work can delay it. Frames that fail before presentation (for example, because
no drawable or surface is available) do not emit a callback. Updating or
clearing the prop cancels native notifications that have not started; a
callback already queued to the JavaScript thread can still run once. Callback
exceptions are normal JavaScript exceptions and are not swallowed by the
renderer.

## High bit depth

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ sk_sp<SkSurface> OpenGLWindowContext::getSurface() {
return _skSurface;
}

void OpenGLWindowContext::present() {
void OpenGLWindowContext::present(std::function<void()> onPresented) {
_glContext->makeCurrent(_glSurface.get());
_directContext->flushAndSubmit();
_glSurface->present();
if (_glSurface->present() && onPresented) {
onPresented();
}
}

} // namespace RNSkia
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class OpenGLWindowContext : public WindowContext {

sk_sp<SkSurface> getSurface() override;

void present() override;
void present(std::function<void()> onPresented = nullptr) override;

int getWidth() override { return ANativeWindow_getWidth(_window); };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ bool RNSkOpenGLCanvasProvider::renderToCanvas(
// Draw into canvas using callback
cb(surface->getCanvas());
// Swap buffers and show on screen
_surfaceHolder->present();
_surfaceHolder->present(makeFramePresentedNotifier());
return true;
} else {
// the render context did not provide a surface
Expand Down
2 changes: 1 addition & 1 deletion packages/skia/apple/MetalWindowContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class MetalWindowContext : public RNSkia::WindowContext {

sk_sp<SkSurface> getSurface() override;

void present() override;
void present(std::function<void()> onPresented = nullptr) override;

int getWidth() override {
return _layer.frame.size.width * _layer.contentsScale;
Expand Down
17 changes: 16 additions & 1 deletion packages/skia/apple/MetalWindowContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,27 @@
return _skSurface;
}

void MetalWindowContext::present() {
void MetalWindowContext::present(std::function<void()> onPresented) {
if (auto dContext = GrAsDirectContext(_skSurface->recordingContext())) {
dContext->flushAndSubmit();
}

id<MTLCommandBuffer> commandBuffer([_commandQueue commandBuffer]);
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
if (onPresented) {
[commandBuffer addCompletedHandler:^(__unused id<MTLCommandBuffer> buffer) {
onPresented();
}];
}
#else
if (onPresented) {
[_currentDrawable addPresentedHandler:^(id<MTLDrawable> drawable) {
if (drawable.presentedTime > 0) {
onPresented();
}
}];
}
#endif
[commandBuffer presentDrawable:_currentDrawable];
[commandBuffer commit];
_skSurface = nullptr;
Expand Down
2 changes: 1 addition & 1 deletion packages/skia/apple/RNSkMetalCanvasProvider.mm
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
}
auto canvas = surface->getCanvas();
cb(canvas);
_ctx->present();
_ctx->present(makeFramePresentedNotifier());
return true;
}
return false;
Expand Down
52 changes: 41 additions & 11 deletions packages/skia/cpp/jsi/ViewProperty.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <utility>
#include <variant>

#include "api/JsiSkPicture.h"
#include "RuntimeLifecycleMonitor.h"
#include "api/JsiSkPicture.h"

namespace RNJsi {
namespace jsi = facebook::jsi;
Expand Down Expand Up @@ -38,6 +39,43 @@ class RuntimeAwareRuntimeGuard : public RuntimeLifecycleListener {
class ViewProperty {
public:
ViewProperty(jsi::Runtime &runtime, const jsi::Value &value) {
setObjectValue(runtime, value);
}

template <typename PlatformContext>
ViewProperty(jsi::Runtime &runtime, const jsi::Value &value,
PlatformContext platformContext, size_t) {
if (value.isObject()) {
auto object = value.asObject(runtime);
if (object.isFunction(runtime)) {
auto runtimeGuard = std::make_shared<RuntimeAwareRuntimeGuard>(runtime);
auto function =
std::make_shared<jsi::Function>(object.getFunction(runtime));
_value = [runtimeGuard, function,
platformContext = std::move(platformContext)]() {
platformContext->runOnJavascriptThread([runtimeGuard, function]() {
auto runtime = runtimeGuard->getRuntime();
if (runtime != nullptr) {
function->call(*runtime);
}
});
};
return;
}
}
setObjectValue(runtime, value);
}

bool isNull() { return std::holds_alternative<std::nullptr_t>(_value); }

sk_sp<SkPicture> getPicture() { return std::get<sk_sp<SkPicture>>(_value); }

std::function<void()> getFunction() {
return std::get<std::function<void()>>(_value);
}

private:
void setObjectValue(jsi::Runtime &runtime, const jsi::Value &value) {
if (value.isObject()) {
auto object = value.asObject(runtime);
if (object.isHostObject(runtime)) {
Expand All @@ -51,15 +89,7 @@ class ViewProperty {
}
}

template <typename PlatformContext>
ViewProperty(jsi::Runtime &runtime, const jsi::Value &value,
PlatformContext platformContext, size_t nativeId) {}

bool isNull() { return std::holds_alternative<std::nullptr_t>(_value); }

sk_sp<SkPicture> getPicture() { return std::get<sk_sp<SkPicture>>(_value); }

private:
std::variant<std::nullptr_t, sk_sp<SkPicture>> _value = nullptr;
std::variant<std::nullptr_t, sk_sp<SkPicture>, std::function<void()>> _value =
nullptr;
};
} // namespace RNJsi
5 changes: 4 additions & 1 deletion packages/skia/cpp/rnskia/RNDawnWindowContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace RNSkia {

void DawnWindowContext::present() {
void DawnWindowContext::present(std::function<void()> onPresented) {
auto recording = _recorder->snap();
if (!recording) {
throw std::runtime_error("Failed to create graphite recording");
Expand All @@ -14,6 +14,9 @@ void DawnWindowContext::present() {
dawn::native::metal::WaitForCommandsToBeScheduled(_device.Get());
#endif
_surface.Present();
if (onPresented) {
onPresented();
}
}

} // namespace RNSkia
2 changes: 1 addition & 1 deletion packages/skia/cpp/rnskia/RNDawnWindowContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class DawnWindowContext : public WindowContext {
return surface;
}

void present() override;
void present(std::function<void()> onPresented = nullptr) override;

void resize(int width, int height) override {
_width = width;
Expand Down
3 changes: 2 additions & 1 deletion packages/skia/cpp/rnskia/RNSkJsiViewApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ class RNSkJsiViewApi : public RNJsi::JsiHostObject,
auto name = arguments[1].asString(runtime).utf8(runtime);
info->props.insert_or_assign(
arguments[1].asString(runtime).utf8(runtime),
RNJsi::ViewProperty(runtime, arguments[2]));
RNJsi::ViewProperty(runtime, arguments[2], _platformContext,
nativeId));
// Now let's see if we have a view that we can update
if (info->view != nullptr) {
// Update view!
Expand Down
5 changes: 4 additions & 1 deletion packages/skia/cpp/rnskia/RNSkPictureView.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
#include "RNSkView.h"
#include "jsi/ViewProperty.h"

#include "RNSkPlatformContext.h"
#include "api/JsiSkPicture.h"
#include "utils/RNSkLog.h"
#include "RNSkPlatformContext.h"
#include "utils/RNSkTimingInfo.h"

#pragma clang diagnostic push
Expand Down Expand Up @@ -105,6 +105,9 @@ class RNSkPictureView : public RNSkView {
// Save picture
std::static_pointer_cast<RNSkPictureRenderer>(getRenderer())
->setPicture(prop.second.getPicture());
} else if (prop.first == "onFramePresented") {
setOnFramePresented(prop.second.isNull() ? nullptr
: prop.second.getFunction());
}
}
}
Expand Down
Loading
Loading