Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use createImageBitmap when available #7579

Merged
merged 20 commits into from
Mar 10, 2019
Merged
Show file tree
Hide file tree
Changes from 18 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
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Change Log
==========

### 1.56 - 2019-04-01

##### Breaking Changes :mega:
* `Resource.fetchImage` now returns an `ImageBitmap` instead of `Image` when supported. This allows for decoding images while fetching using `createImageBitmap` to greatly speed up texture upload and decrease frame drops when loading models with large textures. [#7579](https://github.com/AnalyticalGraphicsInc/cesium/pull/7579)

##### Deprecated :hourglass_flowing_sand:
* `Resource.fetchImage` now takes an options object. Use `resource.fetchImage({ preferBlob: true })` instead of `resource.fetchImage(true)`. The previous function definition will no longer work in 1.56. [#7579](https://github.com/AnalyticalGraphicsInc/cesium/pull/7579)

##### Additions :tada:
* `Resource.fetchImage` now has a `flipY` option to vertically flip an image during fetch & decode. It is only valid when `ImageBitmapOptions` is supported by the browser. [#7579](https://github.com/AnalyticalGraphicsInc/cesium/pull/7579)

### 1.55 - 2019-03-01

##### Breaking Changes :mega:
Expand Down
7 changes: 5 additions & 2 deletions Source/Core/FeatureDetection.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ define([
'./defineProperties',
'./DeveloperError',
'./Fullscreen',
'./RuntimeError',
'../ThirdParty/when'
], function(
defaultValue,
defined,
defineProperties,
DeveloperError,
Fullscreen,
RuntimeError,
when) {
'use strict';
/*global CanvasPixelArray*/
Expand Down Expand Up @@ -269,6 +267,10 @@ define([
}
}

function supportsCreateImageBitmap() {
return typeof createImageBitmap === 'function';
}

/**
* A set of functions to detect whether the current browser supports
* various features.
Expand All @@ -293,6 +295,7 @@ define([
supportsPointerEvents : supportsPointerEvents,
supportsImageRenderingPixelated: supportsImageRenderingPixelated,
supportsWebP: supportsWebP,
supportsCreateImageBitmap: supportsCreateImageBitmap,
imageRenderingValue: imageRenderingValue,
typedArrayTypes: typedArrayTypes
};
Expand Down
14 changes: 12 additions & 2 deletions Source/Core/IonResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,18 @@ define([
return result;
};

IonResource.prototype.fetchImage = function (preferBlob, allowCrossOrigin) {
return Resource.prototype.fetchImage.call(this, this._isExternal ? preferBlob : true, allowCrossOrigin);
IonResource.prototype.fetchImage = function (options) {
if (!this._isExternal) {
var userOptions = options;
options = {
preferBlob : true
};
if (defined(userOptions)) {
options.flipY = userOptions.flipY;
}
}

return Resource.prototype.fetchImage.call(this, options);
};

IonResource.prototype._makeRequest = function(options) {
Expand Down
152 changes: 139 additions & 13 deletions Source/Core/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ define([
'./deprecationWarning',
'./DeveloperError',
'./freezeObject',
'./FeatureDetection',
'./getAbsoluteUri',
'./getBaseUri',
'./getExtensionFromUri',
Expand Down Expand Up @@ -39,6 +40,7 @@ define([
deprecationWarning,
DeveloperError,
freezeObject,
FeatureDetection,
getAbsoluteUri,
getBaseUri,
getExtensionFromUri,
Expand Down Expand Up @@ -374,6 +376,66 @@ define([
});
};

var supportsImageBitmapOptionsResult;
var supportsImageBitmapOptionsPromise;
/**
* A helper function to check whether createImageBitmap supports passing ImageBitmapOptions.
*
* @returns {Promise<Boolean>} A promise that resolves to true if this browser supports creating an ImageBitmap with options.
*
* @private
*/
Resource.supportsImageBitmapOptions = function() {
// Until the HTML folks figure out what to do about this, we need to actually try loading an image to
// know if this browser supports passing options to the createImageBitmap function.
// https://github.com/whatwg/html/pull/4248
if (defined(supportsImageBitmapOptionsPromise)) {
return supportsImageBitmapOptionsPromise;
}

if (!FeatureDetection.supportsCreateImageBitmap()) {
supportsImageBitmapOptionsResult = false;
supportsImageBitmapOptionsPromise = when.resolve(supportsImageBitmapOptionsResult);
return supportsImageBitmapOptionsPromise;
}

var imageDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWP4////fwAJ+wP9CNHoHgAAAABJRU5ErkJggg==';

supportsImageBitmapOptionsPromise = Resource.fetchBlob({
url : imageDataUri
})
.then(function(blob) {
return createImageBitmap(blob, {
imageOrientation: 'flipY'
});
})
.then(function(imageBitmap) {
supportsImageBitmapOptionsResult = true;
return supportsImageBitmapOptionsResult;
})
.otherwise(function() {
supportsImageBitmapOptionsResult = false;
return supportsImageBitmapOptionsResult;
});

return supportsImageBitmapOptionsPromise;
};

/**
* Same as Resource.supportsImageBitmapOptions but synchronous. If the result is not ready, returns undefined.
*
* @returns {Boolean} True if this browser supports creating an ImageBitmap with options.
*
* @private
*/
Resource.supportsImageBitmapOptionsSync = function() {
if (!defined(supportsImageBitmapOptionsPromise)) {
Resource.supportsImageBitmapOptions();
}

return supportsImageBitmapOptionsResult;
};

defineProperties(Resource, {
/**
* Returns true if blobs are supported.
Expand Down Expand Up @@ -827,10 +889,13 @@ define([

/**
* Asynchronously loads the given image resource. Returns a promise that will resolve to
* an {@link Image} once loaded, or reject if the image failed to load.
* an {@link https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap|ImageBitmap} if the browser supports `createImageBitmap` or otherwise an
* {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement|Image} once loaded, or reject if the image failed to load.
*
* @param {Boolean} [preferBlob = false] If true, we will load the image via a blob.
* @returns {Promise.<Image>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority.
* @param {Object} [options] An object with the following properties.
* @param {Boolean} [options.preferBlob=false] If true, we will load the image via a blob.
* @param {Boolean} [options.flipY=true] If true, image will be vertially flipped during decode. Only applies if the browser supports `createImageBitmap`.
* @returns {Promise.<ImageBitmap>|Promise.<Image>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority.
*
*
* @example
Expand All @@ -849,8 +914,16 @@ define([
* @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing}
* @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A}
*/
Resource.prototype.fetchImage = function (preferBlob) {
preferBlob = defaultValue(preferBlob, false);
Resource.prototype.fetchImage = function (options) {
if (typeof options === 'boolean') {
deprecationWarning('fetchImage-parameter-change', 'fetchImage now takes an options object in CesiumJS 1.55. Use resource.fetchImage({ preferBlob: true }) instead of resource.fetchImage(true).');
options = {
preferBlob : options
};
}
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
var preferBlob = defaultValue(options.preferBlob, false);
var flipY = defaultValue(options.flipY, true);

checkAndResetRequest(this.request);

Expand All @@ -860,28 +933,36 @@ define([
// 3. It's a blob URI
// 4. It doesn't have request headers and we preferBlob is false
if (!xhrBlobSupported || this.isDataUri || this.isBlobUri || (!this.hasHeaders && !preferBlob)) {
return fetchImage(this, true);
return fetchImage(this, flipY);
}

var blobPromise = this.fetchBlob();
if (!defined(blobPromise)) {
return;
}

if (FeatureDetection.supportsCreateImageBitmap() && Resource.supportsImageBitmapOptionsSync()) {
return blobPromise
.then(function(blob) {
return Resource._Implementations.createImageBitmapFromBlob(blob, flipY);
});
}

var generatedBlobResource;
var generatedBlob;
return blobPromise
.then(function(blob) {
if (!defined(blob)) {
return;
}

generatedBlob = blob;
var blobUrl = window.URL.createObjectURL(blob);
generatedBlobResource = new Resource({
url: blobUrl
});

return fetchImage(generatedBlobResource);
return fetchImage(generatedBlobResource, flipY);
})
.then(function(image) {
if (!defined(image)) {
Expand All @@ -903,7 +984,7 @@ define([
});
};

function fetchImage(resource) {
function fetchImage(resource, flipY) {
var request = resource.request;
request.url = resource.url;
request.requestFunction = function() {
Expand All @@ -917,7 +998,7 @@ define([

var deferred = when.defer();

Resource._Implementations.createImage(url, crossOrigin, deferred);
Resource._Implementations.createImage(url, crossOrigin, deferred, flipY);

return deferred.promise;
};
Expand All @@ -941,7 +1022,7 @@ define([
request.state = RequestState.UNISSUED;
request.deferred = undefined;

return fetchImage(resource);
return fetchImage(resource, flipY);
}

return when.reject(e);
Expand All @@ -958,15 +1039,19 @@ define([
* @param {Object} [options.templateValues] Key/Value pairs that are used to replace template values (eg. {x}).
* @param {Object} [options.headers={}] Additional HTTP headers that will be sent.
* @param {DefaultProxy} [options.proxy] A proxy to be used when loading the resource.
* @param {Boolean} [options.flipY = true] Whether to vertically flip the image during fetch and decode. Only applies when requesting an image and the browser supports createImageBitmap.
* @param {Resource~RetryCallback} [options.retryCallback] The Function to call when a request for this resource fails. If it returns true, the request will be retried.
* @param {Number} [options.retryAttempts=0] The number of times the retryCallback should be called before giving up.
* @param {Request} [options.request] A Request object that will be used. Intended for internal use only.
* @param {Boolean} [options.preferBlob = false] If true, we will load the image via a blob.
* @returns {Promise.<Image>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority.
* @returns {Promise.<ImageBitmap>|Promise.<Image>|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if <code>request.throttle</code> is true and the request does not have high enough priority.
*/
Resource.fetchImage = function (options) {
var resource = new Resource(options);
return resource.fetchImage(options.preferBlob);
return resource.fetchImage({
flipY: options.flipY,
preferBlob: options.preferBlob
});
};

/**
Expand Down Expand Up @@ -1755,7 +1840,7 @@ define([
*/
Resource._Implementations = {};

Resource._Implementations.createImage = function(url, crossOrigin, deferred) {
function loadImageElement(url, crossOrigin, deferred) {
var image = new Image();

image.onload = function() {
Expand All @@ -1775,6 +1860,47 @@ define([
}

image.src = url;
}

Resource._Implementations.createImage = function(url, crossOrigin, deferred, flipY) {
if (!FeatureDetection.supportsCreateImageBitmap()) {
loadImageElement(url, crossOrigin, deferred);
return;
}

// Passing an Image to createImageBitmap will force it to run on the main thread
// since DOM elements don't exist on workers. We convert it to a blob so it's non-blocking.
// See:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1044102#c38
// https://bugs.chromium.org/p/chromium/issues/detail?id=580202#c10
Resource.supportsImageBitmapOptions()
.then(function(result) {
// We can only use ImageBitmap if we can flip on decode.
// See: https://github.com/AnalyticalGraphicsInc/cesium/pull/7579#issuecomment-466146898
if (!result) {
loadImageElement(url, crossOrigin, deferred);
return;
}

Resource.fetchBlob({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a return statement and then you should move all of the following then calls up a level.

Copy link
Contributor Author

@OmarShehata OmarShehata Mar 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why? I'm assuming this is what you mean:

Resource.supportsImageBitmapOptions()
    .then(function(result) {
        if (!result) {
            loadImageElement(url, crossOrigin, deferred);
            return;
        }

        return Resource.fetchBlob({
            url: url
        });
    })
    .then(function(blob) {
        return Resource._Implementations.createImageBitmapFromBlob(blob, flipY);
    })
    .then(deferred.resolve).otherwise(deferred.reject);

This will run the .then(function(blob) anyway even if we loadImageElement and don't fetch the blob, and then blob will be undefined, which we don't want, right?

I can just move the .then(deferred.resolve).otherwise(deferred.reject); outside and I think that should work fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see Resource.prototype.fetchImage checks if the result is defined at each step in the promise chain to make sure it doesn't run when it terminates early like that. I can follow that pattern.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if for some reason supportsImageBitmapOptions rejected, you would never call deferred.reject (or deferred.resolve).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see Resource.prototype.fetchImage checks if the result is defined at each step in the promise chain to make sure it doesn't run when it terminates early like that. I can follow that pattern.

I'm not sure what you mean by this, please stop by if you still have questions.

Copy link
Contributor Author

@OmarShehata OmarShehata Mar 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I'm pretty sure I've got it locked down now. What I meant was, Resource has a similar promise chain when fetching an image. When we return undefined, all the subsequent then's are still going to run. So you need to add a check at each subsequent then() that the result is defined.

Here is where Resource is doing this:

https://github.com/AnalyticalGraphicsInc/cesium/blob/8bc364ce5eca3c42d84fe21386b0d5a50be82c2d/Source/Core/Resource.js#L886-L889

I just followed a similar pattern in createImage:

https://github.com/AnalyticalGraphicsInc/cesium/blob/a6324e2bcf8b3547b4adb589e7ee789321997678/Source/Core/Resource.js#L1855-L1882

Notice that deferred.resolve is only called in the ImageBitmap codepath. Since loadImageElement calls it on the Image on onload.

Will bump when CI passes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole file could probably use some clean-up. Some of the (old) if checks make reading the code pretty confusing. No action on your part, and this is nothing you did; just mentioning it because I may open a small clean-up PR to take a pass (after merging this).

url: url
}).then(function(blob) {
return Resource._Implementations.createImageBitmapFromBlob(blob, flipY);
}).then(deferred.resolve).otherwise(deferred.reject);
});
};

Resource._Implementations.createImageBitmapFromBlob = function(blob, flipY) {
return Resource.supportsImageBitmapOptions()
.then(function(supportsBitmapOptions) {
if (!supportsBitmapOptions) {
return createImageBitmap(blob);
}

return createImageBitmap(blob, {
imageOrientation: flipY ? 'flipY' : 'none'
});
});
};

function decodeResponse(loadWithHttpResponse, responseType) {
Expand Down
20 changes: 19 additions & 1 deletion Source/Core/loadImageFromTypedArray.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
define([
'../ThirdParty/when',
'./Check',
'./defined',
'./defaultValue',
'./FeatureDetection',
'./Resource'
], function(
when,
Check,
defined,
defaultValue,
FeatureDetection,
Resource) {
'use strict';

/**
* @private
*/
function loadImageFromTypedArray(uint8Array, format, request) {
function loadImageFromTypedArray(options) {
var uint8Array = options.uint8Array;
var format = options.format;
var request = options.request;
var flipY = defaultValue(options.flipY, true);
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object('uint8Array', uint8Array);
Check.typeOf.string('format', format);
Expand All @@ -21,6 +31,14 @@ define([
type : format
});

// Avoid an extra fetch by just calling createImageBitmap here directly on the blob
// instead of sending it to Resource as a blob URL.
if (FeatureDetection.supportsCreateImageBitmap() && Resource.supportsImageBitmapOptionsSync()) {
return when(createImageBitmap(blob, {
imageOrientation: flipY ? 'flipY' : 'none'
}));
}

var blobUrl = window.URL.createObjectURL(blob);
var resource = new Resource({
url: blobUrl,
Expand Down
4 changes: 3 additions & 1 deletion Source/Scene/DiscardMissingTileImagePolicy.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ define([
that._isReady = true;
}

when(resource.fetchImage(true), success, failure);
resource.fetchImage({
preferBlob : true
}).then(success).otherwise(failure);
}

/**
Expand Down
Loading