[canvaskit] Add ability to make texture image across surfaces.

Change-Id: Ie82e8257c9f0990d3be99a2429e034ac400b9580
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/469759
Reviewed-by: Brian Salomon <bsalomon@google.com>
This commit is contained in:
Kevin Lubick 2021-11-11 11:20:49 -05:00
parent 7e1779311b
commit cb94b57ad2
8 changed files with 249 additions and 18 deletions

View File

@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `CanvasKit.MakeLazyImageFromTextureSource`, which is similar to
`Surface.makeImageFromTextureSource`, but can be re-used across different WebGL contexts.
### Fixed
- Some `Surface` methods would not properly switch to the right WebGL context.

View File

@ -17,6 +17,7 @@
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkImageGenerator.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkM44.h"
#include "include/core/SkMaskFilter.h"
@ -57,12 +58,15 @@
#include "modules/canvaskit/WasmCommon.h"
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/html5.h>
#ifdef SK_GL
#include "include/gpu/GrBackendSurface.h"
#include "include/gpu/GrDirectContext.h"
#include "include/gpu/gl/GrGLInterface.h"
#include "include/gpu/gl/GrGLTypes.h"
#include "src/gpu/GrProxyProvider.h"
#include "src/gpu/GrRecordingContextPriv.h"
#include "src/gpu/gl/GrGLDefines.h"
#include "webgl/webgl1.h"
@ -665,23 +669,23 @@ void castUniforms(void* data, size_t dataLen, const SkRuntimeEffect& effect) {
namespace emscripten {
namespace internal {
template<typename ClassType>
void raw_destructor(ClassType *);
void raw_destructor(ClassType*);
template<>
void raw_destructor<SkContourMeasure>(SkContourMeasure *ptr) {
void raw_destructor<SkContourMeasure>(SkContourMeasure* ptr) {
}
template<>
void raw_destructor<SkVertices>(SkVertices *ptr) {
void raw_destructor<SkVertices>(SkVertices* ptr) {
}
#ifndef SK_NO_FONTS
template<>
void raw_destructor<SkTextBlob>(SkTextBlob *ptr) {
void raw_destructor<SkTextBlob>(SkTextBlob* ptr) {
}
template<>
void raw_destructor<SkTypeface>(SkTypeface *ptr) {
void raw_destructor<SkTypeface>(SkTypeface* ptr) {
}
#endif
}
@ -697,6 +701,7 @@ Uint8Array toBytes(sk_sp<SkData> data) {
).call<Uint8Array>("slice"); // slice with no args makes a copy of the memory view.
}
#ifdef SK_GL
// We need to call into the JS side of things to free webGL contexts. This object will be called
// with _setTextureCleanup after CanvasKit loads. The object will have one attribute,
// a function called deleteTexture that takes two ints.
@ -716,6 +721,76 @@ void deleteJSTexture(SkImage::ReleaseContext rc) {
delete ctx;
}
class WebGLTextureImageGenerator : public SkImageGenerator {
public:
WebGLTextureImageGenerator(int width, int height, JSObject callbackObj):
SkImageGenerator(SkImageInfo::MakeN32Premul(width, height)),
fCallback(callbackObj) {}
~WebGLTextureImageGenerator() {
// This cleans up the associated TextureSource at is used to make the texture
// (i.e. "makeTexture" below). We expect this destructor to be called when the
// SkImage that this Generator belongs to is destroyed.
fCallback.call<void>("freeSrc");
}
protected:
GrSurfaceProxyView onGenerateTexture(GrRecordingContext* ctx,
const SkImageInfo& info,
const SkIPoint& origin,
GrMipmapped mipMapped,
GrImageTexGenPolicy texGenPolicy) {
if (ctx->backend() != GrBackendApi::kOpenGL) {
return {};
}
GrGLTextureInfo glInfo;
glInfo.fID = fCallback.call<uint32_t>("makeTexture");
// The format and target should match how we make the texture on the JS side
// See the implementation of the makeTexture function.
glInfo.fFormat = GR_GL_RGBA8;
glInfo.fTarget = GR_GL_TEXTURE_2D;
static constexpr auto kMipmapped = GrMipmapped::kNo;
GrBackendTexture backendTexture(info.width(), info.height(), kMipmapped, glInfo);
const GrBackendFormat& format = backendTexture.getBackendFormat();
const GrColorType colorType = SkColorTypeToGrColorType(info.colorType());
if (!ctx->priv().caps()->areColorTypeAndFormatCompatible(colorType, format)) {
return {};
}
uint32_t webGLCtx = emscripten_webgl_get_current_context();
auto releaseCtx = new TextureReleaseContext{webGLCtx, glInfo.fID};
auto cleanupCallback = GrRefCntedCallback::Make(deleteJSTexture, releaseCtx);
sk_sp<GrSurfaceProxy> proxy = ctx->priv().proxyProvider()->wrapBackendTexture(
backendTexture,
kBorrow_GrWrapOwnership,
GrWrapCacheable::kYes,
kRead_GrIOType,
std::move(cleanupCallback));
if (!proxy) {
return {};
}
static constexpr auto kOrigin = kTopLeft_GrSurfaceOrigin;
GrSwizzle swizzle = ctx->priv().caps()->getReadSwizzle(format, colorType);
return GrSurfaceProxyView(std::move(proxy), kOrigin, swizzle);
}
private:
JSObject fCallback;
};
// callbackObj has two functions in it, one to create a texture "makeTexture" and one to clean up
// the underlying texture source "freeSrc". This way, we can create WebGL textures for each
// surface/WebGLContext that the image is used on (we cannot share WebGLTextures across contexts).
sk_sp<SkImage> MakeImageFromGenerator(int width, int height, JSObject callbackObj) {
auto gen = std::make_unique<WebGLTextureImageGenerator>(width, height, callbackObj);
return SkImage::MakeFromGenerator(std::move(gen));
}
#endif // SK_GL
EMSCRIPTEN_BINDINGS(Skia) {
#ifdef SK_GL
function("_MakeGrContext", &MakeGrContext);
@ -1272,6 +1347,9 @@ EMSCRIPTEN_BINDINGS(Skia) {
class_<SkImage>("Image")
.smart_ptr<sk_sp<SkImage>>("sk_sp<Image>")
#if SK_GL
.class_function("_makeFromGenerator", &MakeImageFromGenerator)
#endif
// Note that this needs to be cleaned up with delete().
.function("getColorSpace", optional_override([](sk_sp<SkImage> self)->sk_sp<SkColorSpace> {
return self->imageInfo().refColorSpace();

View File

@ -58,6 +58,7 @@ var CanvasKit = {
MakeWebGLCanvasSurface: function() {},
Malloc: function() {},
MallocGlyphIDs: function() {},
MakeLazyImageFromTextureSource: function() {},
Free: function() {},
computeTonalColors: function() {},
deleteContext: function() {},
@ -414,6 +415,7 @@ var CanvasKit = {
// private API
_makeShaderCubic: function() {},
_makeShaderOptions: function() {},
_makeFromGenerator: function() {},
},
ImageFilter: {

View File

@ -55,7 +55,7 @@
};
CanvasKit._setTextureCleanup({
"deleteTexture": function(webglHandle, texHandle) {
'deleteTexture': function(webglHandle, texHandle) {
var tex = GL.textures[texHandle];
if (tex) {
GL.getContext(webglHandle).GLctx.deleteTexture(tex);
@ -154,11 +154,7 @@
// Default to trying WebGL first.
CanvasKit.MakeCanvasSurface = CanvasKit.MakeWebGLCanvasSurface;
CanvasKit.Surface.prototype.makeImageFromTexture = function(tex, info) {
CanvasKit.setCurrentContext(this._context);
if (!info['colorSpace']) {
info['colorSpace'] = CanvasKit.ColorSpace.SRGB;
}
function pushTexture(tex) {
// GL is an emscripten object that holds onto WebGL state. One item in that state is
// an array of textures, of which the index is the handle/id.
var texHandle = GL.textures.length;
@ -169,15 +165,36 @@
texHandle = 1;
}
GL.textures.push(tex);
return texHandle
}
CanvasKit.Surface.prototype.makeImageFromTexture = function(tex, info) {
CanvasKit.setCurrentContext(this._context);
if (!info['colorSpace']) {
info['colorSpace'] = CanvasKit.ColorSpace.SRGB;
}
var texHandle = pushTexture(tex);
return this._makeImageFromTexture(this._context, texHandle, info);
};
// If the user specified a height or width in the image info, we use that. Otherwise,
// we try to find the natural media type (for <img> and <video>), display* for
// https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame and then fall back to
// the height and width (to cover <canvas>, ImageBitmap or ImageData).
function getHeight(src, h) {
return h || src['naturalHeight'] || src['videoHeight'] ||
src['displayHeight'] || src['height'];
}
function getWidth(src, w) {
return w || src['naturalWidth'] || src['videoWidth'] ||
src['displayWidth'] || src['width'];
}
CanvasKit.Surface.prototype.makeImageFromTextureSource = function(src, w, h) {
// If the user specified a height or width in the image info, we use that. Otherwise,
// we try to find the natural media type (for <img> and <video>) and then fall back to
// the height and width (to cover <canvas>, ImageBitmap or ImageData).
var height = h || src.naturalHeight || src.videoHeight || src.height;
var width = w || src.naturalWidth || src.videoWidth || src.width;
var height = getHeight(src, h);
var width = getWidth(src, w);
// We want to be pointing at the context associated with this surface.
CanvasKit.setCurrentContext(this._context);
var glCtx = GL.currentContext.GLctx;
@ -199,6 +216,44 @@
return this.makeImageFromTexture(newTex, info);
};
CanvasKit.MakeLazyImageFromTextureSource = function(src, w, h) {
var height = getHeight(src, h);
var width = getWidth(src, w);
var callbackObj = {
'makeTexture': function() {
// This callback function will make a texture on the current drawing surface (i.e.
// the current WebGL context). It assumes that Skia is just about to draw the texture
// to the desired surface, and thus the currentContext is the correct one.
// This is a lot easier than needing to pass the surface handle from the C++ side here.
var ctx = GL.currentContext;
var glCtx = ctx.GLctx;
var newTex = glCtx.createTexture();
glCtx.bindTexture(glCtx.TEXTURE_2D, newTex);
if (ctx.version === 2) {
glCtx.texImage2D(glCtx.TEXTURE_2D, 0, glCtx.RGBA, width, height, 0, glCtx.RGBA, glCtx.UNSIGNED_BYTE, src);
} else {
glCtx.texImage2D(glCtx.TEXTURE_2D, 0, glCtx.RGBA, glCtx.RGBA, glCtx.UNSIGNED_BYTE, src);
}
glCtx.bindTexture(glCtx.TEXTURE_2D, null);
return pushTexture(newTex);
},
'freeSrc': function() {
// This callback will be executed whenever the returned image is deleted. This gives
// us a chance to free up the src (which we now own). Generally, there's nothing
// we need to do (we can let JS garbage collection do its thing). The one exception
// is for https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame, which we should
// close when we are done.
},
}
if (src.constructor.name === 'VideoFrame') {
callbackObj['freeSrc'] = function() {
src.close();
}
}
return CanvasKit.Image._makeFromGenerator(width, height, callbackObj);
}
CanvasKit.setCurrentContext = function(ctx) {
if (!ctx) {
return false;

View File

@ -15,14 +15,27 @@
<canvas id=api2 width=300 height=300></canvas>
<canvas id=api3 width=300 height=300></canvas>
<br>
<img id="src" src="https://storage.googleapis.com/skia-cdn/misc/test.png"
width=40 height=40 crossorigin="anonymous">
<canvas id=api4 width=300 height=300></canvas>
<canvas id=api5 width=300 height=300></canvas>
<canvas id=api6 width=300 height=300></canvas>
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
<script type="text/javascript" charset="utf-8">
const cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
const ckLoaded = CanvasKitInit({locateFile: (file) => '/node_modules/canvaskit/bin/'+file});
const loadTestImage = fetch(cdn + 'test.png').then((response) => response.arrayBuffer());
const imageEle = document.getElementById("src");
Promise.all([ckLoaded, loadTestImage]).then((results) => {MultiCanvasExample(...results)});
Promise.all([ckLoaded, loadTestImage, imageEle.decode()]).then((results) => {
ContextSharingExample(results[0]);
MultiCanvasExample(...results);
});
// This example shows how CanvasKit can automatically switch between multiple canvases
// with different WebGL contexts.
@ -77,9 +90,41 @@
canvasThree.drawImageCubic(img, 100, 100, 0.3, 0.3, null);
surfThree.flush();
img.delete();
}
window.requestAnimationFrame(firstFrame);
}
function ContextSharingExample(CanvasKit) {
const img = CanvasKit.MakeLazyImageFromTextureSource(imageEle);
const surfOne = CanvasKit.MakeWebGLCanvasSurface("api4");
const surfTwo = CanvasKit.MakeWebGLCanvasSurface("api5");
const surfThree = CanvasKit.MakeWebGLCanvasSurface("api6");
let i = 0;
function drawFrame(canvas) {
canvas.drawImageCubic(img, 5+i, 5+i, 0.3, 0.3, null);
i += 1
if (i >= 3) {
if (i > 60) {
img.delete();
return;
}
if (i % 2) {
surfOne.requestAnimationFrame(drawFrame);
} else {
surfTwo.requestAnimationFrame(drawFrame);
}
}
}
surfOne.requestAnimationFrame(drawFrame);
surfTwo.requestAnimationFrame(drawFrame);
surfThree.requestAnimationFrame(drawFrame);
}
</script>

View File

@ -260,6 +260,7 @@ function imageTests(CK: CanvasKit, imgElement?: HTMLImageElement) {
colorType: CK.ColorType.RGBA_8888,
colorSpace: CK.ColorSpace.SRGB
}, Uint8Array.of(255, 0, 0, 250), 4);
const img4 = CK.MakeLazyImageFromTextureSource(imgElement); // $ExpectType Image
if (!img) return;
const dOne = img.encodeToBytes(); // $ExpectType Uint8Array | null
const dTwo = img.encodeToBytes(CK.ImageFormat.JPEG, 97);

View File

@ -267,6 +267,20 @@ export interface CanvasKit {
*/
MakeRenderTarget(ctx: GrDirectContext, info: ImageInfo): Surface | null;
/**
* Returns a texture-backed image based on the content in src. It assumes the image is
* RGBA_8888, unpremul and SRGB. This image can be re-used across multiple surfaces.
*
* Not available for software-backed surfaces.
* @param src - CanvasKit will take ownership of the TextureSource and clean it up when
* the image is destroyed.
* @param width - If provided, will be used as the width of src. Otherwise, the natural
* width of src (if available) will be used.
* @param height - If provided, will be used as the height of src. Otherwise, the natural
* height of src (if available) will be used.
*/
MakeLazyImageFromTextureSource(src: TextureSource, width?: number, height?: number): Image;
/**
* Deletes the associated WebGLContext. Function not available on the CPU version.
* @param ctx
@ -2620,6 +2634,12 @@ export interface Surface extends EmbindObject<Surface> {
* Returns a texture-backed image based on the content in src. It uses RGBA_8888, unpremul
* and SRGB - for more control, use makeImageFromTexture.
*
* The underlying texture for this image will be created immediately from src, so
* it can be disposed of after this call. This image will *only* be usable for this
* surface (because WebGL textures are not transferable to other WebGL contexts).
* For an image that can be used across multiple surfaces, at the cost of being lazily
* loaded, see MakeLazyImageFromTextureSource.
*
* Not available for software-backed surfaces.
* @param src
* @param width - If provided, will be used as the width of src. Otherwise, the natural
@ -3775,6 +3795,7 @@ export type InputFlattenedRSXFormArray = MallocObj | Float32Array | number[];
export type InputVector3 = MallocObj | Vector3 | Float32Array;
/**
* These are the types that webGL's texImage2D supports as a way to get data from as a texture.
* Not listed, but also supported are https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame
*/
export type TextureSource = TypedArray | HTMLImageElement | HTMLVideoElement | ImageData | ImageBitmap;

View File

@ -1339,4 +1339,29 @@ describe('Core canvas behavior', () => {
img.delete();
});
});
gm('MakeLazyImageFromTextureSource_imgElement', (canvas) => {
if (!CanvasKit.gpu) {
return;
}
// This makes an offscreen <img> with the provided source.
const imageEle = new Image();
imageEle.src = '/assets/mandrill_512.png';
// We need to wait until the image is loaded before the texture can use it. For good
// measure, we also wait for it to be decoded.
return imageEle.decode().then(() => {
const img = CanvasKit.MakeLazyImageFromTextureSource(imageEle);
canvas.drawImage(img, 5, 5, null);
const info = img.getImageInfo();
expect(info).toEqual({
'width': 512, // width and height should be derived from the image.
'height': 512,
'alphaType': CanvasKit.AlphaType.Unpremul,
'colorType': CanvasKit.ColorType.RGBA_8888,
});
img.delete();
});
});
});