Bug 495499. Speed up PutImageData for cases when the array contains doubles. r=brendan,vlad,jorendorff

This commit is contained in:
Boris Zbarsky 2009-06-11 10:35:41 -04:00
parent 9927adf1d1
commit 7b0ee90b96
5 changed files with 229 additions and 242 deletions

View File

@ -3740,8 +3740,38 @@ nsCanvasRenderingContext2D::GetImageData()
extern "C" {
#include "jstypes.h"
JS_FRIEND_API(JSBool)
js_ArrayToJSUint8Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest);
js_CoerceArrayToCanvasImageData(JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest);
}
static inline PRUint8 ToUint8(PRInt32 aInput)
{
if (PRUint32(aInput) > 255)
return (aInput < 0) ? 0 : 255;
return PRUint8(aInput);
}
static inline PRUint8 ToUint8(double aInput)
{
if (!(aInput >= 0)) /* Not < so that NaN coerces to 0 */
return 0;
if (aInput > 255)
return 255;
double toTruncate = aInput + 0.5;
PRUint8 retval = PRUint8(toTruncate);
// now retval is rounded to nearest, ties rounded up. We want
// rounded to nearest ties to even, so check whether we had a tie.
if (retval == toTruncate) {
// It was a tie (since adding 0.5 gave us the exact integer we want).
// Since we rounded up, we either already have an even number or we
// have an odd number but the number we want is one less. So just
// unconditionally masking out the ones bit should do the trick to get
// us the value we want.
return (retval & ~1);
}
return retval;
}
// void putImageData (in ImageData d, in float x, in float y);
@ -3815,34 +3845,42 @@ nsCanvasRenderingContext2D::PutImageData()
PRUint8 *imgPtr = imageBuffer.get();
JSBool ok = js_ArrayToJSUint8Buffer(ctx, dataArray, 0, w*h*4, imageBuffer);
JSBool canFastPath =
js_CoerceArrayToCanvasImageData(dataArray, 0, w*h*4, imageBuffer);
// no fast path? go slow.
if (!ok) {
// no fast path? go slow. We sadly need this for now, instead of just
// throwing, because dataArray might not be dense in case someone stuck
// their own array on the imageData.
// FIXME: it'd be awfully nice if we could prevent such modification of
// imageData objects, since it's likely the spec won't allow it anyway.
// Bug 497110 covers this.
if (!canFastPath) {
jsval vr, vg, vb, va;
PRUint8 ir, ig, ib, ia;
for (int32 j = 0; j < h; j++) {
int32 lineOffset = (j*w*4);
for (int32 i = 0; i < w; i++) {
if (!JS_GetElement(ctx, dataArray, (j*w*4) + i*4 + 0, &vr) ||
!JS_GetElement(ctx, dataArray, (j*w*4) + i*4 + 1, &vg) ||
!JS_GetElement(ctx, dataArray, (j*w*4) + i*4 + 2, &vb) ||
!JS_GetElement(ctx, dataArray, (j*w*4) + i*4 + 3, &va))
int32 pixelOffset = lineOffset + i*4;
if (!JS_GetElement(ctx, dataArray, pixelOffset + 0, &vr) ||
!JS_GetElement(ctx, dataArray, pixelOffset + 1, &vg) ||
!JS_GetElement(ctx, dataArray, pixelOffset + 2, &vb) ||
!JS_GetElement(ctx, dataArray, pixelOffset + 3, &va))
return NS_ERROR_DOM_SYNTAX_ERR;
if (JSVAL_IS_INT(vr)) ir = (PRUint8) JSVAL_TO_INT(vr);
else if (JSVAL_IS_DOUBLE(vr)) ir = (PRUint8) (*JSVAL_TO_DOUBLE(vr));
if (JSVAL_IS_INT(vr)) ir = ToUint8(JSVAL_TO_INT(vr));
else if (JSVAL_IS_DOUBLE(vr)) ir = ToUint8(*JSVAL_TO_DOUBLE(vr));
else return NS_ERROR_DOM_SYNTAX_ERR;
if (JSVAL_IS_INT(vg)) ig = (PRUint8) JSVAL_TO_INT(vg);
else if (JSVAL_IS_DOUBLE(vg)) ig = (PRUint8) (*JSVAL_TO_DOUBLE(vg));
if (JSVAL_IS_INT(vg)) ig = ToUint8(JSVAL_TO_INT(vg));
else if (JSVAL_IS_DOUBLE(vg)) ig = ToUint8(*JSVAL_TO_DOUBLE(vg));
else return NS_ERROR_DOM_SYNTAX_ERR;
if (JSVAL_IS_INT(vb)) ib = (PRUint8) JSVAL_TO_INT(vb);
else if (JSVAL_IS_DOUBLE(vb)) ib = (PRUint8) (*JSVAL_TO_DOUBLE(vb));
if (JSVAL_IS_INT(vb)) ib = ToUint8(JSVAL_TO_INT(vb));
else if (JSVAL_IS_DOUBLE(vb)) ib = ToUint8(*JSVAL_TO_DOUBLE(vb));
else return NS_ERROR_DOM_SYNTAX_ERR;
if (JSVAL_IS_INT(va)) ia = (PRUint8) JSVAL_TO_INT(va);
else if (JSVAL_IS_DOUBLE(va)) ia = (PRUint8) (*JSVAL_TO_DOUBLE(va));
if (JSVAL_IS_INT(va)) ia = ToUint8(JSVAL_TO_INT(va));
else if (JSVAL_IS_DOUBLE(va)) ia = ToUint8(*JSVAL_TO_DOUBLE(va));
else return NS_ERROR_DOM_SYNTAX_ERR;
// Convert to premultiplied color (losslessly if the input came from getImageData)

View File

@ -714,6 +714,7 @@ _TEST_FILES_6 = \
image_green.png \
image_green-redirect \
image_green-redirect^headers^ \
test_2d.imagedata_coercion.html \
$(NULL)
# xor and lighter aren't well handled by cairo; they mostly work, but we don't want

View File

@ -0,0 +1,110 @@
<!DOCTYPE HTML>
<title>Canvas test: 2d.imagedata_coercion</title>
<!-- Testing: imagedata coerced correctly on set -->
<script src="/MochiKit/packed.js"></script>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
<body>
<canvas id="c" width="100" height="1"><p class="fallback">FAIL (fallback content)</p></canvas>
<script>
/* NOTE: Due to round-tripping through premultiplied values and the rounding
that ensues, values of alpha < 255 will tend to do weird things. In
particular, the premultiplied color values are computed by multiplying by a,
dividing by 255, then always rounding up. The conversion the other way is done
by multiplying by 255/a and rounding down. So if
255/a * (amount added when rounding) > 1
we will get a change in value when we go through a putImageData/getImageData cycle. Therefore, to make sure we don't have to worry about our color
channels, our alpha channel should never be < 250, unless it's 0. And when it's 0, all our color channels will come back as 0 too. */
/* Our tests. Each test has two arrays: the array of values to set and the
array of values that should read back as a result. */
var tests = [
[
[ 0, 1, 3, 250 ], [ 0, 1, 3, 250 ]
],
[
[ 0, 1, 2, 250, 4, 5, 6, 250 ], [ 0, 1, 2, 250, 4, 5, 6, 250 ]
],
[
[ 0, 1000, 2, 300, 400, 5, 600, 250 ], [ 0, 255, 2, 255, 255, 5, 255, 250 ]
],
[
[ -10, -5, NaN, 250, 4, 5, 6, -250 ], [ 0, 0, 0, 250, 0, 0, 0, 0 ]
],
[
[ 0.5, 12.2, 12.8, 251.5, 12.5, 13.5, 13.2, 250.5 ],
[ 0, 12, 13, 252, 12, 14, 13, 250 ]
],
];
function doTest(type, idx) {
var testPair = tests[idx];
var test = testPair[0];
var ref = testPair[1];
var descSuffix = " for " + type + " test #" + (idx+1);
function myIs(a, b, str) {
is(a, b, str + descSuffix);
}
myIs(test.length, ref.length, "Length mismatch");
myIs(test.length % 4, 0, "Length not a multiple of 4");
var pixels = test.length / 4;
var imageData = ctx.createImageData(pixels, 1);
myIs(imageData.width, pixels, "Incorrect created data width");
myIs(imageData.height, 1, "Incorrect created data height");
myIs(imageData.data.length, test.length,
"Incorrect length in created image data");
ctx.putImageData(imageData, 0, 0);
var testImageData = ctx.getImageData(0, 0, pixels, 1);
myIs(testImageData.data.length, test.length,
"Incorrect length in test image data after clearing pixels");
var j;
for (j = 0; j < testImageData.data.length; ++j) {
myIs(testImageData.data[j], 0,
"Nonzero value at position " + j + " in test image data " +
"after clearing pixels");
}
for (j = 0; j < imageData.data.length; ++j) {
imageData.data[j] = test[j];
}
if (type == "slow") {
// convert to a non-dense array so we can test that codepath
imageData.data.makeMeSlow = 1;
}
ctx.putImageData(imageData, 0, 0);
testImageData = ctx.getImageData(0, 0, pixels, 1);
myIs(testImageData.data.length, test.length,
"Incorrect length in test image data after putting our imagedata");
for (j = 0; j < testImageData.data.length; ++j) {
myIs(testImageData.data[j], ref[j],
"Incorrect value at position " + j + " in test image data " +
"after putting our imagedata");
}
}
function doTests(type) {
for (var i = 0; i < tests.length; ++i) {
doTest(type, i);
}
}
var canvas;
var ctx;
SimpleTest.waitForExplicitFinish();
addLoadEvent(function () {
canvas = document.getElementById('c');
ctx = canvas.getContext('2d');
doTests("fast");
doTests("slow")
SimpleTest.finish();
});
</script>

View File

@ -1171,7 +1171,7 @@ array_trace(JSTracer *trc, JSObject *obj)
size_t i;
jsval v;
JS_ASSERT(OBJ_IS_DENSE_ARRAY(cx, obj));
JS_ASSERT(js_IsDenseArray(obj));
capacity = js_DenseArrayCapacity(obj);
for (i = 0; i < capacity; i++) {
@ -3465,192 +3465,59 @@ js_ArrayInfo(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
#endif
JS_FRIEND_API(JSBool)
js_ArrayToJSUint8Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest)
js_CoerceArrayToCanvasImageData(JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
if (!obj || !js_IsDenseArray(obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
jsint vi;
JSUint8 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v) || (vi = JSVAL_TO_INT(v)) < 0)
return JS_FALSE;
jsval v = obj->dslots[i];
if (JSVAL_IS_INT(v)) {
jsint vi = JSVAL_TO_INT(v);
if (jsuint(vi) > 255)
vi = (vi < 0) ? 0 : 255;
*dp++ = JSUint8(vi);
} else if (JSVAL_IS_DOUBLE(v)) {
jsdouble vd = *JSVAL_TO_DOUBLE(v);
if (!(vd >= 0)) /* Not < so that NaN coerces to 0 */
*dp++ = 0;
else if (vd > 255)
*dp++ = 255;
else {
jsdouble toTruncate = vd + 0.5;
JSUint8 val = JSUint8(toTruncate);
*dp++ = (JSUint8) vi;
/*
* now val is rounded to nearest, ties rounded up. We want
* rounded to nearest ties to even, so check whether we had a
* tie.
*/
if (val == toTruncate) {
/*
* It was a tie (since adding 0.5 gave us the exact integer
* we want). Since we rounded up, we either already have an
* even number or we have an odd number but the number we
* want is one less. So just unconditionally masking out the
* ones bit should do the trick to get us the value we
* want.
*/
*dp++ = (val & ~1);
} else {
*dp++ = val;
}
}
} else {
return JS_FALSE;
}
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSUint16Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint16 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
jsint vi;
JSUint16 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v) || (vi = JSVAL_TO_INT(v)) < 0)
return JS_FALSE;
*dp++ = (JSUint16) vi;
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSUint32Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint32 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
jsint vi;
JSUint32 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v) || (vi = JSVAL_TO_INT(v)) < 0)
return JS_FALSE;
*dp++ = (JSUint32) vi;
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSInt8Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt8 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
JSInt8 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v))
return JS_FALSE;
*dp++ = (JSInt8) JSVAL_TO_INT(v);
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSInt16Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt16 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
JSInt16 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v))
return JS_FALSE;
*dp++ = (JSInt16) JSVAL_TO_INT(v);
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSInt32Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt32 *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
JSInt32 *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (!JSVAL_IS_INT(v))
return JS_FALSE;
*dp++ = (JSInt32) JSVAL_TO_INT(v);
}
return JS_TRUE;
}
JS_FRIEND_API(JSBool)
js_ArrayToJSDoubleBuffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
jsdouble *dest)
{
uint32 length;
if (!obj || !OBJ_IS_DENSE_ARRAY(cx, obj))
return JS_FALSE;
length = obj->fslots[JSSLOT_ARRAY_LENGTH];
if (length < offset + count)
return JS_FALSE;
jsval v;
jsdouble *dp = dest;
for (uintN i = offset; i < offset+count; i++) {
v = obj->dslots[i];
if (JSVAL_IS_INT(v))
*dp++ = (jsdouble) JSVAL_TO_INT(v);
else if (JSVAL_IS_DOUBLE(v))
*dp++ = *(JSVAL_TO_DOUBLE(v));
else
return JS_FALSE;
}
return JS_TRUE;
}

View File

@ -58,7 +58,13 @@ js_IdIsIndex(jsval id, jsuint *indexp);
extern JSClass js_ArrayClass, js_SlowArrayClass;
#define OBJ_IS_DENSE_ARRAY(cx,obj) (OBJ_GET_CLASS(cx, obj) == &js_ArrayClass)
static JS_INLINE JSBool
js_IsDenseArray(JSObject *obj)
{
return STOBJ_GET_CLASS(obj) == &js_ArrayClass;
}
#define OBJ_IS_DENSE_ARRAY(cx, obj) js_IsDenseArray(obj)
#define OBJ_IS_ARRAY(cx,obj) (OBJ_IS_DENSE_ARRAY(cx, obj) || \
OBJ_GET_CLASS(cx, obj) == &js_SlowArrayClass)
@ -109,14 +115,14 @@ js_MakeArraySlow(JSContext *cx, JSObject *obj);
static JS_INLINE uint32
js_DenseArrayCapacity(JSObject *obj)
{
JS_ASSERT(OBJ_IS_DENSE_ARRAY(BOGUS_CX, obj));
JS_ASSERT(js_IsDenseArray(obj));
return obj->dslots ? (uint32) obj->dslots[-1] : 0;
}
static JS_INLINE void
js_SetDenseArrayCapacity(JSObject *obj, uint32 capacity)
{
JS_ASSERT(OBJ_IS_DENSE_ARRAY(BOGUS_CX, obj));
JS_ASSERT(js_IsDenseArray(obj));
JS_ASSERT(obj->dslots);
obj->dslots[-1] = (jsval) capacity;
}
@ -169,59 +175,24 @@ extern JSBool JS_FASTCALL
js_ArrayCompPush(JSContext *cx, JSObject *obj, jsval v);
/*
* Fast dense-array-to-buffer conversions.
* Fast dense-array-to-buffer conversion for use by canvas.
*
* If the array is a dense array, fill [offset..offset+count] values
* into destination, assuming that types are consistent. Return
* JS_TRUE if successful, otherwise JS_FALSE -- note that the
* destination buffer may be modified even if JS_FALSE is returned
* (e.g. due to finding an inappropriate type later on in the array).
* If JS_FALSE is returned, no error conditions or exceptions are set
* on the context.
* If the array is a dense array, fill [offset..offset+count] values into
* destination, assuming that types are consistent. Return JS_TRUE if
* successful, otherwise JS_FALSE -- note that the destination buffer may be
* modified even if JS_FALSE is returned (e.g. due to finding an inappropriate
* type later on in the array). If JS_FALSE is returned, no error conditions
* or exceptions are set on the context.
*
* For ArrayToJSUint8, ArrayToJSUint16, and ArrayToJSUint32, each element
* in the array a) must be an integer; b) must be >= 0. Integers
* are clamped to fit in the destination size. Only JSVAL_IS_INT values
* are considered to be valid, so for JSUint32, the maximum value that
* can be fast-converted is less than the full unsigned 32-bit range.
*
* For ArrayToJSInt8, ArrayToJSInt16, ArrayToJSInt32, each element in
* the array must be an integer. Integers are clamped to fit in the
* destination size. Only JSVAL_IS_INT values are considered to be
* valid, so for JSInt32, the maximum value that can be
* fast-converted is less than the full signed 32-bit range.
*
* For ArrayToJSDouble, each element in the array must be an
* integer -or- a double (JSVAL_IS_NUMBER).
* This method succeeds if each element of the array is an integer or a double.
* Values outside the 0-255 range are clamped to that range. Double values are
* converted to integers in this range by clamping and then rounding to
* nearest, ties to even.
*/
JS_FRIEND_API(JSBool)
js_ArrayToJSUint8Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSUint16Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint16 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSUint32Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSUint32 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSInt8Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt8 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSInt16Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt16 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSInt32Buffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
JSInt32 *dest);
JS_FRIEND_API(JSBool)
js_ArrayToJSDoubleBuffer(JSContext *cx, JSObject *obj, jsuint offset, jsuint count,
jsdouble *dest);
js_CoerceArrayToCanvasImageData(JSObject *obj, jsuint offset, jsuint count,
JSUint8 *dest);
JSBool
js_PrototypeHasIndexedProperties(JSContext *cx, JSObject *obj);