[canvaskit] Add API to re-use Image textures and reduce flickering

This creates a new SkImage with the same texture and changes it
out without the user noticing (or needing to delete the old Image).

Change-Id: I3a1ce6d4a335873f2b7670d56dadfccdc7881c38
Bug: skia:12723
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/495556
Reviewed-by: Nathaniel Nifong <nifong@google.com>
This commit is contained in:
Kevin Lubick 2022-01-26 15:49:20 -05:00
parent 6e1afd3bfe
commit 994c946bd3
10 changed files with 93 additions and 8 deletions

1
demos.skia.org/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

View File

@ -1,4 +1,6 @@
.PHONY: local
local:
echo "Go check out http://localhost:8123/demos/hello_world/index.html"
rm -f build
ln -s ../modules/canvaskit/build build
python2 -m SimpleHTTPServer 8123

View File

@ -24,7 +24,7 @@
<canvas id=draw width=500 height=500></canvas>
<img id="sourceImage" src="/demo/textures/testimg.png">
<img id="sourceImage" src="/demos/textures/testimg.png">
<video id="sourceVideo" autoplay muted></video>
</body>
@ -62,21 +62,22 @@
throw 'Could not make surface';
}
const paint = new CanvasKit.Paint();
// This image will have its underlying texture re-used once per frame.
const img = surface.makeImageFromTextureSource(srcEle);
// This example creates a new texture, loads it into an image, and then deletes the image
// (which should delete the texture automatically).
let lastTS = Date.now();
function drawFrame(canvas) {
const now = Date.now();
canvas.rotate(10 * (now - lastTS) / 1000, 250, 250);
lastTS = now;
const img = surface.makeImageFromTextureSource(srcEle);
// Re-use the image's underlying texture, but replace the contents of the old texture
// with the contents of srcEle
surface.updateTextureFromSource(img, srcEle);
canvas.clear(CanvasKit.Color(200, 200, 200));
canvas.drawImage(img, 5, 5, paint);
img && img.delete();
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
});
</script>
</script>

View File

@ -6,13 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `Surface.updateTextureFromSource` prevents flickering on some platforms by re-using the texture
for a given `Image` instead of needing to always create a new one via
`Surface.makeImageFromTextureSource`. (skbug.com/12723)
### Changed
- Surface factories always produce a surface with an attached color space. Specifying `null` to
`CanvasKit.MakeWebGLCanvasSurface` or calling any factory that does not take a color space
will now create a surface with a color space of `CanvasKit.ColorSpace.SRGB`.
### Fixed
- Supplying textures via `CanvasKit.makeImageFromTextureSource` should not cause issues with
- Supplying textures via `Surface.makeImageFromTextureSource` should not cause issues with
Mipmaps or other places where Skia needs to create textures (skbug.com/12797)
## [0.32.0] - 2021-12-15

View File

@ -1922,6 +1922,9 @@ EMSCRIPTEN_BINDINGS(Skia) {
auto backendRT = self.getBackendRenderTarget(SkSurface::kFlushRead_BackendHandleAccess);
return (backendRT.isValid()) ? backendRT.sampleCnt() : 0;
}))
.function("_resetContext",optional_override([](SkSurface& self)->void {
GrAsDirectContext(self.recordingContext())->resetContext(kTextureBinding_GrGLBackendState);
}))
#else
.function("reportBackendTypeIsGPU", optional_override([](SkSurface& self) -> bool {
return false;

View File

@ -674,6 +674,7 @@ var CanvasKit = {
/** @return {CanvasKit.Image} */
makeImageSnapshot: function() {},
makeSurface: function() {},
updateTextureFromSource: function() {},
},
// private API
@ -683,6 +684,7 @@ var CanvasKit = {
_makeImageSnapshot: function() {},
_makeSurface: function() {},
_makeRasterDirect: function() {},
_resetContext: function() {},
delete: function() {},
},

View File

@ -167,7 +167,11 @@
CanvasKit.Surface.prototype.makeImageFromTexture = function(tex, info) {
CanvasKit.setCurrentContext(this._context);
var texHandle = pushTexture(tex);
return this._makeImageFromTexture(this._context, texHandle, info);
var img = this._makeImageFromTexture(this._context, texHandle, info);
if (img) {
img._tex = texHandle;
}
return img;
};
// We try to find the natural media type (for <img> and <video>), display* for
@ -211,6 +215,54 @@
return this.makeImageFromTexture(newTex, info);
};
CanvasKit.Surface.prototype.updateTextureFromSource = function(img, src) {
if (!img._tex) {
Debug('Image is not backed by a user-provided texture');
return;
}
CanvasKit.setCurrentContext(this._context);
var glCtx = GL.currentContext.GLctx;
// Copy the contents of src over the texture associated with this image.
var tex = GL.textures[img._tex];
glCtx.bindTexture(glCtx.TEXTURE_2D, tex);
if (GL.currentContext.version === 2) {
glCtx.texImage2D(glCtx.TEXTURE_2D, 0, glCtx.RGBA, getWidth(src), getHeight(src), 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);
// Tell Skia we messed with the currently bound texture.
this._resetContext();
// Create a new texture entry and put null into the old slot. This keeps our texture alive,
// otherwise it will be deleted when we delete the old Image.
GL.textures[img._tex] = null;
img._tex = pushTexture(tex);
var ii = img.getImageInfo();
ii['colorSpace'] = img.getColorSpace();
// Skia may cache parts of the image, and some places assume images are immutable. In order
// to make things work, we create a new SkImage based on the same texture as the old image.
var newImg = this._makeImageFromTexture(this._context, img._tex, ii);
// To make things more ergonomic for the user, we change passed in img object to refer
// to the new image and clean up the old SkImage object. This has the effect of updating
// the Image (from the user's side of things), because they shouldn't be caring about what
// part of WASM memory we are pointing to.
// The $$ part is provided by emscripten's embind, so this could break if they change
// things on us.
// https://github.com/emscripten-core/emscripten/blob/a65d70c809f077542649c60097787e1c7460ced6/src/embind/embind.js
// They do not do anything special to keep closure from minifying things and neither do we.
var oldPtr = img.$$.ptr;
var oldSmartPtr = img.$$.smartPtr;
img.$$.ptr = newImg.$$.ptr;
img.$$.smartPtr = newImg.$$.smartPtr;
// We want to clean up the previous image, so we swap out the pointers and call delete on it
// which should have that effect.
newImg.$$.ptr = oldPtr;
newImg.$$.smartPtr = oldSmartPtr;
newImg.delete();
// Clean up the colorspace that we used.
ii['colorSpace'].delete();
}
CanvasKit.MakeLazyImageFromTextureSource = function(src, info) {
if (!info) {
info = {

View File

@ -895,6 +895,8 @@ function surfaceTests(CK: CanvasKit, gl?: WebGLRenderingContext) {
};
surfaceFour.requestAnimationFrame(drawFrame);
surfaceFour.drawOnce(drawFrame);
surfaceFour.updateTextureFromSource(img5!, videoEle);
}
function textBlobTests(CK: CanvasKit, font?: Font, path?: Path) {

View File

@ -2674,6 +2674,19 @@ export interface Surface extends EmbindObject<Surface> {
*/
sampleCnt(): number;
/**
* Updates the underlying GPU texture of the image to be the contents of the provided
* TextureSource. Has no effect on CPU backend or if img was not created with either
* makeImageFromTextureSource or makeImageFromTexture.
* If the provided TextureSource is of different dimensions than the Image, the contents
* will be deformed (e.g. squished). The ColorType, AlphaType, and ColorSpace of src should
* match the original settings used to create the Image or it may draw strange.
*
* @param img - A texture-backed Image.
* @param src - A valid texture source of any dimensions.
*/
updateTextureFromSource(img: Image, src: TextureSource): void;
/**
* Returns the width of this surface in pixels.
*/

View File

@ -1390,6 +1390,10 @@ describe('Core canvas behavior', () => {
// measure, we also wait for it to be decoded.
return imageEle.decode().then(() => {
const img = surface.makeImageFromTextureSource(imageEle);
// Make sure the texture is properly written to and Skia does not draw over it by
// by accident.
canvas.clear(CanvasKit.RED);
surface.updateTextureFromSource(img, imageEle);
canvas.drawImage(img, 0, 0, null);
const info = img.getImageInfo();