[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:
parent
7e1779311b
commit
cb94b57ad2
@ -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.
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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);
|
||||
|
21
modules/canvaskit/npm_build/types/index.d.ts
vendored
21
modules/canvaskit/npm_build/types/index.d.ts
vendored
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user