diff --git a/src/resources/README.md b/src/resources/README.md index a1ed0604170c..2bc89869a501 100644 --- a/src/resources/README.md +++ b/src/resources/README.md @@ -91,3 +91,25 @@ ffmpeg -display_vflip -i temp.mp4 -c copy four-colors-vp9-bt601-vflip.mp4 rm temp.mp4 ``` + +The test jpg files were generated with by exiftool cmds and other image tools in below steps: +``` +// Generate four-colors.jpg with no orientation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors.jpg from four-colors.png and check with exiftool to ensure no orientation metadata been set. + +// Generate jpg picture with 90 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-90-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=6 four-colors-hard-rotate-90-ccw.jpg -o four-colors-rotate-90-cw.jpg +rm four-colors-hard-rotate-90-ccw.jpg + +// Generate jpg picture with 180 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-180-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=3 four-colors-hard-rotate-180-ccw.jpg -o four-colors-rotate-180-cw.jpg +rm four-colors-hard-rotate-180-ccw.jpg + +// Generate jpg picture with 270 cw rotation metadata +Use a image tool (e.g. "Paint" app on Windows) to create four-colors-hard-rotate-270-ccw.jpg and check with exiftool to ensure no orientation metadata been set. +exiftool -Orientation#=8 four-colors-hard-rotate-270-ccw.jpg -o four-colors-rotate-270-cw.jpg +rm four-colors-hard-rotate-270-ccw.jpg + +``` diff --git a/src/resources/four-colors-rotate-180-cw.jpg b/src/resources/four-colors-rotate-180-cw.jpg new file mode 100644 index 000000000000..5be3efab39b1 Binary files /dev/null and b/src/resources/four-colors-rotate-180-cw.jpg differ diff --git a/src/resources/four-colors-rotate-270-cw.jpg b/src/resources/four-colors-rotate-270-cw.jpg new file mode 100644 index 000000000000..62ae07beb092 Binary files /dev/null and b/src/resources/four-colors-rotate-270-cw.jpg differ diff --git a/src/resources/four-colors-rotate-90-cw.jpg b/src/resources/four-colors-rotate-90-cw.jpg new file mode 100644 index 000000000000..2e2beed8e491 Binary files /dev/null and b/src/resources/four-colors-rotate-90-cw.jpg differ diff --git a/src/resources/four-colors.jpg b/src/resources/four-colors.jpg new file mode 100644 index 000000000000..c04c1f6cd301 Binary files /dev/null and b/src/resources/four-colors.jpg differ diff --git a/src/webgpu/web_platform/copyToTexture/image_file.spec.ts b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts new file mode 100644 index 000000000000..aa9a16e16dce --- /dev/null +++ b/src/webgpu/web_platform/copyToTexture/image_file.spec.ts @@ -0,0 +1,125 @@ +export const description = ` +copyExternalImageToTexture from ImageFiles like *.png, *.jpg source. +`; + +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { TextureUploadingUtils } from '../../util/copy_to_texture.js'; +import { + convertToUnorm8, + GetSourceFromImageFile, + kImageNames, + kImageInfo, + kImageExpectedColors, + kObjectTypeFromFiles, +} from '../util.js'; + +export const g = makeTestGroup(TextureUploadingUtils); + +g.test('from_orientation_metadata_file') + .desc( + ` + Test HTMLImageElements with rotation metadata can be copied to WebGPU texture correctly. + + It creates an ImageBitmap or 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 rotation metadata + - 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('objectTypeFromFile', kObjectTypeFromFiles) + .combine('srcDoFlipYDuringCopy', [true, false]) + ) + .fn(async t => { + const { imageName, objectTypeFromFile, srcDoFlipYDuringCopy } = t.params; + const kColorFormat = 'rgba8unorm'; + + // Load image file. + const source = await GetSourceFromImageFile(t, imageName, objectTypeFromFile); + 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 f000d80e56b9..66a2163b7388 100644 --- a/src/webgpu/web_platform/util.ts +++ b/src/webgpu/web_platform/util.ts @@ -101,6 +101,15 @@ export const kVideoExpectedColors = makeTable({ }, } as const); +export const kImageExpectedColors = { + srgb: { + red: { R: 1.0, G: 0.0, B: 0.0, A: 1.0 }, + green: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 }, + blue: { R: 0.0, G: 0.0, B: 1.0, A: 1.0 }, + yellow: { R: 1.0, G: 1.0, B: 0.0, A: 1.0 }, + }, +} as const; + // MAINTENANCE_TODO: Add BT.2020 video in table. // Video container and codec defines several transform ops to apply to raw decoded frame to display. // Our test cases covers 'visible rect' and 'rotation'. @@ -350,6 +359,7 @@ type VideoName = keyof typeof kVideoInfo; export const kVideoNames: readonly VideoName[] = keysOf(kVideoInfo); export const kPredefinedColorSpace = ['display-p3', 'srgb'] as const; + /** * Starts playing a video and waits for it to be consumable. * Returns a promise which resolves after `callback` (which may be async) completes. @@ -606,3 +616,76 @@ export async function captureCameraFrame(test: GPUTest): Promise { return frame; } + +const kFourColorsInfo = { + display: { + topLeftColor: 'yellow', + topRightColor: 'red', + bottomLeftColor: 'blue', + bottomRightColor: 'green', + }, +} as const; + +export const kImageInfo = makeTable({ + table: { + 'four-colors.jpg': kFourColorsInfo, + 'four-colors-rotate-90-cw.jpg': kFourColorsInfo, + 'four-colors-rotate-180-cw.jpg': kFourColorsInfo, + 'four-colors-rotate-270-cw.jpg': kFourColorsInfo, + }, +} as const); + +type ImageName = keyof typeof kImageInfo; +export const kImageNames: readonly ImageName[] = keysOf(kImageInfo); + +type ObjectTypeFromFile = (typeof kObjectTypeFromFiles)[number]; +export const kObjectTypeFromFiles = [ + 'ImageBitmap-from-Blob', + 'ImageBitmap-from-Image', + 'Image', +] as const; + +/** + * 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( + test: GPUTest, + imageName: ImageName, + objectTypeFromFile: ObjectTypeFromFile +): Promise { + const imageUrl = getResourcePath(imageName); + + switch (objectTypeFromFile) { + case 'ImageBitmap-from-Blob': { + // MAINTENANCE_TODO: resource folder path when using service worker is not correct. Return + // the correct path to load resource in correct place. + // The wrong path: /out/webgpu/webworker/web_platform/copyToTexture/resources + if (globalThis.constructor.name === 'ServiceWorkerGlobalScope') { + test.skip('Try to load image resource from serivce worker but the path is not correct.'); + } + // Load image file through fetch. + const response = await fetch(imageUrl); + return createImageBitmap(await response.blob()); + } + case 'ImageBitmap-from-Image': + case 'Image': { + // 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.' + ); + } + + // Load image file through HTMLImageElement. + const image = new Image(); + image.src = imageUrl; + await raceWithRejectOnTimeout(image.decode(), 5000, 'decode image timeout'); + if (objectTypeFromFile === 'Image') { + return image; + } + + return createImageBitmap(image); + } + } +}