diff --git a/src/resources/four-colors.avif b/src/resources/four-colors.avif new file mode 100644 index 00000000000..06394f43827 Binary files /dev/null and b/src/resources/four-colors.avif differ diff --git a/src/resources/four-colors.bmp b/src/resources/four-colors.bmp new file mode 100644 index 00000000000..d61f80398ed Binary files /dev/null and b/src/resources/four-colors.bmp differ diff --git a/src/resources/four-colors.gif b/src/resources/four-colors.gif new file mode 100644 index 00000000000..4d4642f7e0c Binary files /dev/null and b/src/resources/four-colors.gif differ diff --git a/src/resources/four-colors.ico b/src/resources/four-colors.ico new file mode 100644 index 00000000000..8ebb5d74799 Binary files /dev/null and b/src/resources/four-colors.ico differ diff --git a/src/resources/four-colors.svg b/src/resources/four-colors.svg new file mode 100644 index 00000000000..a3ba74a5640 --- /dev/null +++ b/src/resources/four-colors.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/resources/four-colors.webp b/src/resources/four-colors.webp new file mode 100644 index 00000000000..f7dd40bee9a Binary files /dev/null and b/src/resources/four-colors.webp differ diff --git a/src/webgpu/web_platform/copyToTexture/image_file.spec.ts b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts index aa9a16e16dc..682e2339d45 100644 --- a/src/webgpu/web_platform/copyToTexture/image_file.spec.ts +++ b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts @@ -6,11 +6,14 @@ import { makeTestGroup } from '../../../common/framework/test_group.js'; import { TextureUploadingUtils } from '../../util/copy_to_texture.js'; import { convertToUnorm8, - GetSourceFromImageFile, + GetSourceFromEXIFImageFile, kImageNames, kImageInfo, kImageExpectedColors, kObjectTypeFromFiles, + kEXIFImageNames, + kEXIFImageInfo, + loadImageFileAndRun, } from '../util.js'; export const g = makeTestGroup(TextureUploadingUtils); @@ -38,7 +41,7 @@ g.test('from_orientation_metadata_file') ) .params(u => u // - .combine('imageName', kImageNames) + .combine('imageName', kEXIFImageNames) .combine('objectTypeFromFile', kObjectTypeFromFiles) .combine('srcDoFlipYDuringCopy', [true, false]) ) @@ -47,7 +50,7 @@ g.test('from_orientation_metadata_file') const kColorFormat = 'rgba8unorm'; // Load image file. - const source = await GetSourceFromImageFile(t, imageName, objectTypeFromFile); + const source = await GetSourceFromEXIFImageFile(t, imageName, objectTypeFromFile); const width = source.width; const height = source.height; @@ -72,7 +75,7 @@ g.test('from_orientation_metadata_file') } ); - const expect = kImageInfo[imageName].display; + const expect = kEXIFImageInfo[imageName].display; const presentColors = kImageExpectedColors.srgb; if (srcDoFlipYDuringCopy) { @@ -123,3 +126,111 @@ g.test('from_orientation_metadata_file') ]); } }); + +g.test('from_multiple_formats') + .desc( + ` + Test HTMLImageElements which loaded multiple image file formats. Including + *.jpg, *.png, *.bmp, *.webp, *.avif, *.svg, *.ico and *.gif. + + It creates an HTMLImageElement using images in the 'resources' folder. + + Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel + of dst texture, and read one pixel out to compare with the manually documented expected color. + + If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result + is flipped. + + The tests covers: + - Image with multiple image file format + - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases) + - TODO: partial copy tests should be added + - TODO: all valid dstColorFormat tests should be added. + - TODO(#4108): Make this work in service workers (see GetSourceFromImageFile) + ` + ) + .params(u => + u // + .combine('imageName', kImageNames) + .combine('srcDoFlipYDuringCopy', [true, false]) + ) + .fn(async t => { + const { imageName, srcDoFlipYDuringCopy } = t.params; + const kColorFormat = 'rgba8unorm'; + await loadImageFileAndRun(t, imageName, (source: HTMLImageElement) => { + const width = source.width; + const height = source.height; + + const dstTexture = t.createTextureTracked({ + size: { width, height }, + format: kColorFormat, + usage: + GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, + }); + + t.device.queue.copyExternalImageToTexture( + { + source, + flipY: srcDoFlipYDuringCopy, + }, + { + texture: dstTexture, + }, + { + width, + height, + } + ); + + const expect = kImageInfo[imageName].display; + const presentColors = kImageExpectedColors.srgb; + + if (srcDoFlipYDuringCopy) { + t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ + // Flipped top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Flipped top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, + // Flipped bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Flipped bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + ]); + } else { + t.expectSinglePixelComparisonsAreOkInTexture({ texture: dstTexture }, [ + // Top-left. + { + coord: { x: width * 0.25, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topLeftColor]), + }, + // Top-right. + { + coord: { x: width * 0.75, y: height * 0.25 }, + exp: convertToUnorm8(presentColors[expect.topRightColor]), + }, + // Bottom-left. + { + coord: { x: width * 0.25, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomLeftColor]), + }, + // Bottom-right. + { + coord: { x: width * 0.75, y: height * 0.75 }, + exp: convertToUnorm8(presentColors[expect.bottomRightColor]), + }, + ]); + } + }); + }); diff --git a/src/webgpu/web_platform/util.ts b/src/webgpu/web_platform/util.ts index 66a2163b738..7b5131d4ba8 100644 --- a/src/webgpu/web_platform/util.ts +++ b/src/webgpu/web_platform/util.ts @@ -626,7 +626,7 @@ const kFourColorsInfo = { }, } as const; -export const kImageInfo = makeTable({ +export const kEXIFImageInfo = makeTable({ table: { 'four-colors.jpg': kFourColorsInfo, 'four-colors-rotate-90-cw.jpg': kFourColorsInfo, @@ -635,9 +635,25 @@ export const kImageInfo = makeTable({ }, } as const); +export const kImageInfo = makeTable({ + table: { + 'four-colors.jpg': kFourColorsInfo, + 'four-colors.png': kFourColorsInfo, + 'four-colors.bmp': kFourColorsInfo, + 'four-colors.webp': kFourColorsInfo, + 'four-colors.gif': kFourColorsInfo, + 'four-colors.avif': kFourColorsInfo, + 'four-colors.ico': kFourColorsInfo, + 'four-colors.svg': kFourColorsInfo, + }, +} as const); + type ImageName = keyof typeof kImageInfo; export const kImageNames: readonly ImageName[] = keysOf(kImageInfo); +type EXIFImageName = keyof typeof kEXIFImageInfo; +export const kEXIFImageNames: readonly EXIFImageName[] = keysOf(kEXIFImageInfo); + type ObjectTypeFromFile = (typeof kObjectTypeFromFiles)[number]; export const kObjectTypeFromFiles = [ 'ImageBitmap-from-Blob', @@ -649,12 +665,12 @@ export const kObjectTypeFromFiles = [ * Load image file(e.g. *.jpg) from ImageBitmap, blob or HTMLImageElement. And * convert the result to valid source that GPUCopyExternalImageSource supported. */ -export async function GetSourceFromImageFile( +export async function GetSourceFromEXIFImageFile( test: GPUTest, - imageName: ImageName, + exifImageName: EXIFImageName, objectTypeFromFile: ObjectTypeFromFile ): Promise { - const imageUrl = getResourcePath(imageName); + const imageUrl = getResourcePath(exifImageName); switch (objectTypeFromFile) { case 'ImageBitmap-from-Blob': { @@ -689,3 +705,44 @@ export async function GetSourceFromImageFile( } } } + +/** + * Create HTMLImageElement and load image file and waits for it to be loaded. + * Returns a promise which resolves after `callback` (which may be async) completes. + * + * @param imageName An valid imageName in kkImageInfo table . + * @param callback Function to call when HTMLImageElement is loaded. + * + */ +export function loadImageFileAndRun( + test: GPUTest, + imageName: ImageName, + callback: (image: HTMLImageElement) => unknown | Promise +): Promise { + return raceWithRejectOnTimeout( + new Promise((resolve, reject) => { + const callbackAndResolve = (image: HTMLImageElement) => + void (async () => { + try { + await callback(image); + resolve(); + } catch (ex) { + reject(ex); + } + })(); + // Skip test if HTMLImageElement is not available, e.g. in worker. + if (typeof HTMLImageElement === 'undefined') { + test.skip( + 'Try to use HTMLImage do image file decoding but HTMLImageElement not available.' + ); + } + const image = new Image(); + image.src = getResourcePath(imageName); + image.onload = () => { + callbackAndResolve(image); + }; + }), + 2000, + 'Video never became ready' + ); +}