blob: b48be79492cec10ce2fa0ff9a83b267a1f6ff86c [file] [log] [blame]
export const description = `
copyExternalImageToTexture from HTMLImageElement source.
`;
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { raceWithRejectOnTimeout } from '../../../common/util/util.js';
import {
getBaseFormatForRegularTextureFormat,
isTextureFormatPossiblyUsableWithCopyExternalImageToTexture,
kRegularTextureFormats,
} from '../../format_info.js';
import * as ttu from '../../texture_test_utils.js';
import { TextureUploadingUtils, kCopySubrectInfo } from '../../util/copy_to_texture.js';
import { kTestColorsOpaque, makeTestColorsTexelView } from './util.js';
async function decodeImageFromCanvas(canvas: HTMLCanvasElement): Promise<HTMLImageElement> {
const blobFromCanvas = new Promise(resolve => {
canvas.toBlob(blob => resolve(blob));
});
const blob = (await blobFromCanvas) as Blob;
const url = URL.createObjectURL(blob);
const image = new Image(canvas.width, canvas.height);
image.src = url;
await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout');
return image;
}
export const g = makeTestGroup(TextureUploadingUtils);
g.test('from_image')
.desc(
`
Test HTMLImageElement can be copied to WebGPU texture correctly.
These images are highly possible living in GPU back resource.
It generates pixels in ImageData one by one based on a color list:
[Red, Green, Blue, Black, White].
Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
of dst texture, and read the contents out to compare with the HTMLImageElement contents.
Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
is set to 'true' and do unpremultiply alpha if it is set to 'false'.
If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
is flipped.
The tests covers:
- Valid 2D canvas
- Valid dstColorFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases)
And the expected results are all passed.
`
)
.params(u =>
u
.combine('srcDoFlipYDuringCopy', [true, false])
.combine('dstColorFormat', kRegularTextureFormats)
.filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('width', [1, 2, 4, 15, 255, 256])
.combine('height', [1, 2, 4, 15, 255, 256])
)
.beforeAllSubcases(t => {
if (typeof HTMLImageElement === 'undefined') t.skip('HTMLImageElement not available');
})
.fn(async t => {
const { width, height, dstColorFormat, dstPremultiplied, srcDoFlipYDuringCopy } = t.params;
t.skipIfTextureFormatNotSupported(dstColorFormat);
t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);
const imageCanvas = document.createElement('canvas');
imageCanvas.width = width;
imageCanvas.height = height;
// Generate non-transparent pixel data to avoid canvas
// different opt behaviour on putImageData()
// from browsers.
const texelViewSource = makeTestColorsTexelView({
testColors: kTestColorsOpaque,
format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
width,
height,
flipY: false,
premultiplied: false,
});
// Generate correct expected values
const imageData = new ImageData(width, height);
texelViewSource.writeTextureData(imageData.data, {
bytesPerRow: width * 4,
rowsPerImage: height,
subrectOrigin: [0, 0],
subrectSize: { width, height },
});
const imageCanvasContext = imageCanvas.getContext('2d') as CanvasRenderingContext2D;
if (imageCanvasContext === null) {
t.skip('canvas cannot get 2d context');
return;
}
// Use putImageData to prevent color space conversion.
imageCanvasContext.putImageData(imageData, 0, 0);
const image = await decodeImageFromCanvas(imageCanvas);
const dst = t.createTextureTracked({
size: { width, height },
format: dstColorFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const expFormat = getBaseFormatForRegularTextureFormat(dstColorFormat) ?? dstColorFormat;
const flipSrcBeforeCopy = false;
const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
srcPixels: imageData.data,
srcOrigin: [0, 0],
srcSize: [width, height],
dstOrigin: [0, 0],
dstSize: [width, height],
subRectSize: [width, height],
format: expFormat,
flipSrcBeforeCopy,
srcDoFlipYDuringCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{
source: image,
origin: { x: 0, y: 0 },
flipY: srcDoFlipYDuringCopy,
},
{
texture: dst,
origin: { x: 0, y: 0 },
colorSpace: 'srgb',
premultipliedAlpha: dstPremultiplied,
},
texelViewExpected,
{ width, height, depthOrArrayLayers: 1 },
// 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
// allow diffs of 1ULP since that's the generally-appropriate threshold.
{ maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
);
});
g.test('from_fully_transparent_image')
.desc(
`
Test HTMLImageElement with alpha 0 can be copied to WebGPU texture correctly.
Use a prebaked 2x2 fully transparent image as source.
Then call copyExternalImageToTexture() to do a copy to the 0 mipLevel of dst texture,
and read the contents out to compare with the HTMLImageElement contents.
When dest alpha mode is:
- premultiplied, the content should be (0, 0, 0, 0)
- not premultiplied, the content should be the same as prebaked
pixel values (255, 102, 153, 0).
The tests covers:
- Source HTMLImageElement is fully transparent with valid dest alphaMode.
And the expected results are all passed.
`
)
.params(u => u.combine('dstPremultiplied', [true, false]))
.beforeAllSubcases(t => {
if (typeof HTMLImageElement === 'undefined') t.skip('HTMLImageElement not available');
})
.fn(async t => {
const { dstPremultiplied } = t.params;
const kColorFormat = 'rgba8unorm';
const kImageWidth = 2;
const kImageHeight = 2;
const imageCanvas = document.createElement('canvas');
imageCanvas.width = kImageWidth;
imageCanvas.height = kImageHeight;
// Prebaked fully transparent image with content (255, 102, 153, 0)
const image = new Image(kImageWidth, kImageHeight);
image.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR42mP8nzaTAQQYYQwALssD/5ca+r8AAAAASUVORK5CYII=';
await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout');
const dst = t.createTextureTracked({
size: { width: kImageWidth, height: kImageHeight },
format: kColorFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
t.device.queue.copyExternalImageToTexture(
{
source: image,
},
{
texture: dst,
premultipliedAlpha: dstPremultiplied,
},
{
width: kImageWidth,
height: kImageHeight,
}
);
const expectedPixels = dstPremultiplied
? new Uint8Array([0, 0, 0, 0])
: new Uint8Array([255, 102, 153, 0]);
ttu.expectSinglePixelComparisonsAreOkInTexture(t, { texture: dst }, [
{ coord: { x: kImageWidth * 0.3, y: kImageHeight * 0.3 }, exp: expectedPixels },
]);
});
g.test('copy_subrect_from_2D_Canvas')
.desc(
`
Test HTMLImageElement can be copied to WebGPU texture correctly.
These images are highly possible living in GPU back resource.
It generates pixels in ImageData one by one based on a color list:
[Red, Green, Blue, Black, White].
Then call copyExternalImageToTexture() to do a subrect copy, based on a predefined copy
rect info list, to the 0 mipLevel of dst texture, and read the contents out to compare
with the HTMLImageElement contents.
Do premultiply alpha during copy if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
is set to 'true' and do unpremultiply alpha if it is set to 'false'.
If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
is flipped, and origin is top-left consistantly.
The tests covers:
- Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
- Valid dstColorFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases)
- Valid subrect copies.
And the expected results are all passed.
`
)
.params(u =>
u
.combine('srcDoFlipYDuringCopy', [true, false])
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('copySubRectInfo', kCopySubrectInfo)
)
.beforeAllSubcases(t => {
if (typeof HTMLImageElement === 'undefined') t.skip('HTMLImageElement not available');
})
.fn(async t => {
const { copySubRectInfo, dstPremultiplied, srcDoFlipYDuringCopy } = t.params;
const { srcOrigin, dstOrigin, srcSize, dstSize, copyExtent } = copySubRectInfo;
const kColorFormat = 'rgba8unorm';
const imageCanvas = document.createElement('canvas');
imageCanvas.width = srcSize.width;
imageCanvas.height = srcSize.height;
// Generate non-transparent pixel data to avoid canvas
// different opt behaviour on putImageData()
// from browsers.
const texelViewSource = makeTestColorsTexelView({
testColors: kTestColorsOpaque,
format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
width: srcSize.width,
height: srcSize.height,
flipY: false,
premultiplied: false,
});
// Generate correct expected values
const imageData = new ImageData(srcSize.width, srcSize.height);
texelViewSource.writeTextureData(imageData.data, {
bytesPerRow: srcSize.width * 4,
rowsPerImage: srcSize.height,
subrectOrigin: [0, 0],
subrectSize: srcSize,
});
const imageCanvasContext = imageCanvas.getContext('2d');
if (imageCanvasContext === null) {
t.skip('canvas cannot get 2d context');
return;
}
// Use putImageData to prevent color space conversion.
imageCanvasContext.putImageData(imageData, 0, 0);
const image = await decodeImageFromCanvas(imageCanvas);
const dst = t.createTextureTracked({
size: dstSize,
format: kColorFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const flipSrcBeforeCopy = false;
const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
srcPixels: imageData.data,
srcOrigin,
srcSize,
dstOrigin,
dstSize,
subRectSize: copyExtent,
format: kColorFormat,
flipSrcBeforeCopy,
srcDoFlipYDuringCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{
source: image,
origin: srcOrigin,
flipY: srcDoFlipYDuringCopy,
},
{
texture: dst,
origin: dstOrigin,
colorSpace: 'srgb',
premultipliedAlpha: dstPremultiplied,
},
texelViewExpected,
copyExtent,
// 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
// allow diffs of 1ULP since that's the generally-appropriate threshold.
{ maxDiffULPsForFloatFormat: 1, maxDiffULPsForNormFormat: 1 }
);
});