blob: 64b00460ed8993820bfb51521d203185fdafc61e [file] [log] [blame]
export const description = `
copyExternalImageToTexture from ImageBitmaps created from various sources.
TODO: Test ImageBitmap generated from all possible ImageBitmapSource, relevant ImageBitmapOptions
(https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#images-2)
and various source filetypes and metadata (weird dimensions, EXIF orientations, video rotations
and visible/crop rectangles, etc. (In theory these things are handled inside createImageBitmap,
but in theory could affect the internal representation of the ImageBitmap.)
TODO: Test zero-sized copies from all sources (just make sure params cover it) (e.g. 0x0, 0x4, 4x0).
`;
import { makeTestGroup } from '../../../common/framework/test_group.js';
import {
getBaseFormatForRegularTextureFormat,
isTextureFormatPossiblyUsableWithCopyExternalImageToTexture,
kRegularTextureFormats,
} from '../../format_info.js';
import { TextureUploadingUtils, kCopySubrectInfo } from '../../util/copy_to_texture.js';
import { kTestColorsAll, kTestColorsOpaque, makeTestColorsTexelView } from './util.js';
export const g = makeTestGroup(TextureUploadingUtils);
g.test('from_ImageData')
.desc(
`
Test ImageBitmap generated from ImageData can be copied to WebGPU
texture correctly. These imageBitmaps are highly possible living
in CPU back resource.
It generates pixels in ImageData one by one based on a color list:
[Red, Green, Blue, Black, White, SemitransparentWhite].
Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
of dst texture, and read the contents out to compare with the ImageBitmap 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 dstFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcFlipYInCopy' in cases)
And the expected results are all passed.
`
)
.params(u =>
u
.combine('alpha', ['none', 'premultiply'] as const)
.combine('orientation', ['none', 'flipY'] as const)
.combine('colorSpaceConversion', ['none', 'default'] as const)
.combine('srcFlipYInCopy', [true, false])
.combine('dstFormat', kRegularTextureFormats)
.filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstFormat))
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('width', [1, 2, 4, 15, 255, 256])
.combine('height', [1, 2, 4, 15, 255, 256])
)
.beforeAllSubcases(t => {
t.skipIf(typeof ImageData === 'undefined', 'ImageData does not exist in this environment');
})
.fn(async t => {
const {
width,
height,
alpha,
orientation,
colorSpaceConversion,
dstFormat,
dstPremultiplied,
srcFlipYInCopy,
} = t.params;
t.skipIfTextureFormatNotSupported(dstFormat);
t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstFormat);
const testColors = kTestColorsAll;
// Generate correct expected values
const texelViewSource = makeTestColorsTexelView({
testColors,
format: 'rgba8unorm', // ImageData is always in rgba8unorm format.
width,
height,
flipY: false,
premultiplied: false,
});
const imageData = new ImageData(width, height);
texelViewSource.writeTextureData(imageData.data, {
bytesPerRow: width * 4,
rowsPerImage: height,
subrectOrigin: [0, 0],
subrectSize: { width, height },
});
const imageBitmap = await createImageBitmap(imageData, {
premultiplyAlpha: alpha,
imageOrientation: orientation,
colorSpaceConversion,
});
const dst = t.createTextureTracked({
size: { width, height },
format: dstFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const expFormat = getBaseFormatForRegularTextureFormat(dstFormat) ?? dstFormat;
const flipSrcBeforeCopy = orientation === 'flipY';
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: srcFlipYInCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{ source: imageBitmap, origin: { x: 0, y: 0 }, flipY: srcFlipYInCopy },
{
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_canvas')
.desc(
`
Test ImageBitmap generated from canvas/offscreenCanvas can be copied to WebGPU
texture correctly. These imageBitmaps 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 ImageBitmap 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 dstFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcFlipYInCopy' in cases)
And the expected results are all passed.
`
)
.params(u =>
u
.combine('orientation', ['none', 'flipY'] as const)
.combine('colorSpaceConversion', ['none', 'default'] as const)
.combine('srcFlipYInCopy', [true, false])
.combine('dstFormat', kRegularTextureFormats)
.filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstFormat))
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('width', [1, 2, 4, 15, 255, 256])
.combine('height', [1, 2, 4, 15, 255, 256])
)
.beforeAllSubcases(t => {
t.skipIf(typeof ImageData === 'undefined', 'ImageData does not exist in this environment');
})
.fn(async t => {
const {
width,
height,
orientation,
colorSpaceConversion,
dstFormat,
dstPremultiplied,
srcFlipYInCopy,
} = t.params;
t.skipIfTextureFormatNotSupported(dstFormat);
t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstFormat);
// CTS sometimes runs on worker threads, where document is not available.
// In this case, OffscreenCanvas can be used instead of <canvas>.
// But some browsers don't support OffscreenCanvas, and some don't
// support '2d' contexts on OffscreenCanvas.
// In this situation, the case will be skipped.
let imageCanvas;
if (typeof document !== 'undefined') {
imageCanvas = document.createElement('canvas');
imageCanvas.width = width;
imageCanvas.height = height;
} else if (typeof OffscreenCanvas === 'undefined') {
t.skip('OffscreenCanvas is not supported');
return;
} else {
imageCanvas = new OffscreenCanvas(width, height);
}
const imageCanvasContext = imageCanvas.getContext('2d') as OffscreenCanvasRenderingContext2D;
if (imageCanvasContext === null) {
t.skip('OffscreenCanvas "2d" context not available');
return;
}
// 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 },
});
// Use putImageData to prevent color space conversion.
imageCanvasContext.putImageData(imageData, 0, 0);
// MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
// `createImageBitmap` that takes `ImageBitmapOptions`.
const imageBitmap = await createImageBitmap(imageCanvas as HTMLCanvasElement, {
premultiplyAlpha: 'premultiply',
imageOrientation: orientation,
colorSpaceConversion,
});
const dst = t.createTextureTracked({
size: { width, height },
format: dstFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const expFormat = getBaseFormatForRegularTextureFormat(dstFormat) ?? dstFormat;
const flipSrcBeforeCopy = orientation === 'flipY';
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: srcFlipYInCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{ source: imageBitmap, origin: { x: 0, y: 0 }, flipY: srcFlipYInCopy },
{
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('copy_subrect_from_ImageData')
.desc(
`
Test ImageBitmap generated from ImageData can be copied to WebGPU
texture correctly. These imageBitmaps are highly possible living in CPU 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 ImageBitmap 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 dstFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcFlipYInCopy' in cases)
- Valid subrect copies.
And the expected results are all passed.
`
)
.params(u =>
u
.combine('alpha', ['none', 'premultiply'] as const)
.combine('orientation', ['none', 'flipY'] as const)
.combine('colorSpaceConversion', ['none', 'default'] as const)
.combine('srcFlipYInCopy', [true, false])
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('copySubRectInfo', kCopySubrectInfo)
)
.beforeAllSubcases(t => {
t.skipIf(typeof ImageData === 'undefined', 'ImageData does not exist in this environment');
})
.fn(async t => {
const {
copySubRectInfo,
alpha,
orientation,
colorSpaceConversion,
dstPremultiplied,
srcFlipYInCopy,
} = t.params;
const testColors = kTestColorsAll;
const { srcOrigin, dstOrigin, srcSize, dstSize, copyExtent } = copySubRectInfo;
const kColorFormat = 'rgba8unorm';
// Generate correct expected values
const texelViewSource = makeTestColorsTexelView({
testColors,
format: kColorFormat, // ImageData is always in rgba8unorm format.
width: srcSize.width,
height: srcSize.height,
flipY: false,
premultiplied: false,
});
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 imageBitmap = await createImageBitmap(imageData, {
premultiplyAlpha: alpha,
imageOrientation: orientation,
colorSpaceConversion,
});
const dst = t.createTextureTracked({
size: dstSize,
format: kColorFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const flipSrcBeforeCopy = orientation === 'flipY';
const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
srcPixels: imageData.data,
srcOrigin,
srcSize,
dstOrigin,
dstSize,
subRectSize: copyExtent,
format: kColorFormat,
flipSrcBeforeCopy,
srcDoFlipYDuringCopy: srcFlipYInCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{ source: imageBitmap, origin: srcOrigin, flipY: srcFlipYInCopy },
{
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 }
);
});
g.test('copy_subrect_from_2D_Canvas')
.desc(
`
Test ImageBitmap generated from canvas/offscreenCanvas can be copied to WebGPU
texture correctly. These imageBitmaps 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 ImageBitmap 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 dstFormat of copyExternalImageToTexture()
- Valid source image alphaMode
- Valid dest alphaMode
- Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcFlipYInCopy' in cases)
- Valid subrect copies.
And the expected results are all passed.
`
)
.params(u =>
u
.combine('orientation', ['none', 'flipY'] as const)
.combine('colorSpaceConversion', ['none', 'default'] as const)
.combine('srcFlipYInCopy', [true, false])
.combine('dstPremultiplied', [true, false])
.beginSubcases()
.combine('copySubRectInfo', kCopySubrectInfo)
)
.fn(async t => {
const { copySubRectInfo, orientation, colorSpaceConversion, dstPremultiplied, srcFlipYInCopy } =
t.params;
const { srcOrigin, dstOrigin, srcSize, dstSize, copyExtent } = copySubRectInfo;
const kColorFormat = 'rgba8unorm';
// CTS sometimes runs on worker threads, where document is not available.
// In this case, OffscreenCanvas can be used instead of <canvas>.
// But some browsers don't support OffscreenCanvas, and some don't
// support '2d' contexts on OffscreenCanvas.
// In this situation, the case will be skipped.
let imageCanvas;
if (typeof document !== 'undefined') {
imageCanvas = document.createElement('canvas');
imageCanvas.width = srcSize.width;
imageCanvas.height = srcSize.height;
} else if (typeof OffscreenCanvas === 'undefined') {
t.skip('OffscreenCanvas is not supported');
return;
} else {
imageCanvas = new OffscreenCanvas(srcSize.width, srcSize.height);
}
const imageCanvasContext = imageCanvas.getContext('2d') as CanvasRenderingContext2D;
if (imageCanvasContext === null) {
t.skip('OffscreenCanvas "2d" context not available');
return;
}
// 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,
});
// Use putImageData to prevent color space conversion.
imageCanvasContext.putImageData(imageData, 0, 0);
// MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
// `createImageBitmap` that takes `ImageBitmapOptions`.
const imageBitmap = await createImageBitmap(imageCanvas as HTMLCanvasElement, {
premultiplyAlpha: 'premultiply',
imageOrientation: orientation,
colorSpaceConversion,
});
const dst = t.createTextureTracked({
size: dstSize,
format: kColorFormat,
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const flipSrcBeforeCopy = orientation === 'flipY';
const texelViewExpected = t.getExpectedDstPixelsFromSrcPixels({
srcPixels: imageData.data,
srcOrigin,
srcSize,
dstOrigin,
dstSize,
subRectSize: copyExtent,
format: kColorFormat,
flipSrcBeforeCopy,
srcDoFlipYDuringCopy: srcFlipYInCopy,
conversion: {
srcPremultiplied: false,
dstPremultiplied,
},
});
t.doTestAndCheckResult(
{ source: imageBitmap, origin: srcOrigin, flipY: srcFlipYInCopy },
{
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 }
);
});