Revert "Don't use createImageBitmap to load bitmap costumes"

Reverts 7abbcdbb10

I am unable to reproduce the claim that ImageBitmap uses more memory
than <img>. Instead I've observed the following:

<img> uses a base64 data: URI which is slower than creating a Blob
directly from the binary data. I also experimented using a blob: URI
with <img> and did find it to perform better. A future commit may use
this when createImageBitmap() is not available.

<img> can't be reliably decoded in parallel. Chrome in particular will
very often wait until texImage2d() to actually decode the image, which
turns image decoding into a single-threaded process. Calling decode()
does not affect this. createImageBitmap is reliably parallelized.

<img>'s only benefit is that projects with a lot of copies of the same
exact costume will usually have their image decoding happen only once
whereas createImageBitmap() will decode each one individually (does not
apply when using blob: URIs).

Chrome:
<img> with data:    48.5MB  4160ms
<img> with blob:    48.4MB  2162ms
createImageBitmap() 48.4MB  1261ms
Equivalent SVGs     60.8MB  3857ms

Firefox:
<img> with data:    88.3MB  3704ms
<img> with blob:    88.9MB  2459ms
createImageBitmap() 92.5MB  2380ms
Equivalent SVGs     483.3MB 5429ms

Using a test project with 100 480x360 unique costumes.
Equivalent SVGs is the same project with each costume converted to SVG,
just for curiosity. Note that SVGs may not actually be rendered or
decoded until the project starts.
This commit is contained in:
GarboMuffin committed 2022-07-31 01:51:23 -05:00
1 parent 4325486bef
commit 125f20b9ce
1 file changed
+17 -4
+17 -4
View File
@@ -84,7 +84,11 @@ const canvasPool = (function () {
return new CanvasPool();
}());
const readImage = src => new Promise((resolve, reject) => {
/**
* @param {string} src URL of image
* @returns {Promise<HTMLImageElement>}
*/
const readAsImageElement = src => new Promise((resolve, reject) => {
const image = new Image();
image.onload = function () {
resolve(image);
@@ -99,14 +103,23 @@ const readImage = src => new Promise((resolve, reject) => {
image.src = src;
});
const persistentReadImage = async src => {
/**
* @param {Asset} asset scratch-storage asset
* @returns {Promise<HTMLImageElement|ImageBitmap>}
*/
const persistentReadImage = async asset => {
// Sometimes, when a lot of images are loaded at once, especially in Chrome, reading an image
// can throw an error even on valid images. To mitigate this, we'll retry image reading a few
// time with delays.
let firstError;
for (let i = 0; i < 3; i++) {
try {
return await readImage(src);
if (typeof createImageBitmap === 'function') {
return await createImageBitmap(
new Blob([asset.data.buffer], {type: asset.assetType.contentType})
);
}
return await readAsImageElement(asset.encodeDataURI());
} catch (e) {
if (!firstError) {
firstError = e;
@@ -145,7 +158,7 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
return null;
}
return persistentReadImage(asset.encodeDataURI());
return persistentReadImage(asset);
}))
.then(([baseImageElement, textImageElement]) => {
if (!baseImageElement) {