@@ -7,6 +7,8 @@ Tests for external textures from HTMLVideoElement (and other video-type sources?
77TODO: consider whether external_texture and copyToTexture video tests should be in the same file
88TODO(#3193): Test video in BT.2020 color space
99TODO(#4364): Test camera capture with copyExternalImageToTexture (not necessarily in this file)
10+ TODO(#4605): Test importExternalTexture with video frame display size different with coded size from
11+ a video file
1012` ;
1113
1214import { makeTestGroup } from '../../../common/framework/test_group.js' ;
@@ -162,6 +164,69 @@ function checkNonStandardIsZeroCopyIfAvailable(): { checkNonStandardIsZeroCopy?:
162164 }
163165}
164166
167+ function createVideoFrameWithDisplayScale (
168+ t : GPUTest ,
169+ displayScale : 'smaller' | 'same' | 'larger'
170+ ) : { frame : VideoFrame ; displayWidth : number ; displayHeight : number } {
171+ const canvas = createCanvas ( t , 'onscreen' , kWidth , kHeight ) ;
172+ const canvasContext = canvas . getContext ( '2d' ) ;
173+
174+ if ( canvasContext === null ) {
175+ t . skip ( ' onscreen canvas 2d context not available' ) ;
176+ }
177+
178+ const ctx = canvasContext ;
179+
180+ const rectWidth = Math . floor ( kWidth / 2 ) ;
181+ const rectHeight = Math . floor ( kHeight / 2 ) ;
182+
183+ // Red
184+ ctx . fillStyle = `rgba(255, 0, 0, 1.0)` ;
185+ ctx . fillRect ( 0 , 0 , rectWidth , rectHeight ) ;
186+ // Lime
187+ ctx . fillStyle = `rgba(0, 255, 0, 1.0)` ;
188+ ctx . fillRect ( rectWidth , 0 , kWidth - rectWidth , rectHeight ) ;
189+ // Blue
190+ ctx . fillStyle = `rgba(0, 0, 255, 1.0)` ;
191+ ctx . fillRect ( 0 , rectHeight , rectWidth , kHeight - rectHeight ) ;
192+ // Fuchsia
193+ ctx . fillStyle = `rgba(255, 0, 255, 1.0)` ;
194+ ctx . fillRect ( rectWidth , rectHeight , kWidth - rectWidth , kHeight - rectHeight ) ;
195+
196+ const imageData = ctx . getImageData ( 0 , 0 , kWidth , kHeight ) ;
197+
198+ let displayWidth = kWidth ;
199+ let displayHeight = kHeight ;
200+ switch ( displayScale ) {
201+ case 'smaller' :
202+ displayWidth = Math . floor ( kWidth / 2 ) ;
203+ displayHeight = Math . floor ( kHeight / 2 ) ;
204+ break ;
205+ case 'same' :
206+ displayWidth = kWidth ;
207+ displayHeight = kHeight ;
208+ break ;
209+ case 'larger' :
210+ displayWidth = kWidth * 2 ;
211+ displayHeight = kHeight * 2 ;
212+ break ;
213+ default :
214+ unreachable ( ) ;
215+ }
216+
217+ const frameInit : VideoFrameBufferInit = {
218+ format : 'RGBA' ,
219+ codedWidth : kWidth ,
220+ codedHeight : kHeight ,
221+ displayWidth,
222+ displayHeight,
223+ timestamp : 0 ,
224+ } ;
225+
226+ const frame = new VideoFrame ( imageData . data . buffer , frameInit ) ;
227+ return { frame, displayWidth, displayHeight } ;
228+ }
229+
165230g . test ( 'importExternalTexture,sample' )
166231 . desc (
167232 `
@@ -381,6 +446,159 @@ Tests that we can import an VideoFrame with non-YUV pixel format into a GPUExter
381446 ] ) ;
382447 } ) ;
383448
449+ g . test ( 'importExternalTexture,video_frame_display_size_diff_with_coded_size' )
450+ . desc (
451+ `
452+ Tests that we can import a VideoFrame with display size different with its coded size, and
453+ sampling works without validation errors.
454+ `
455+ )
456+ . params ( u =>
457+ u //
458+ . combine ( 'displayScale' , [ 'smaller' , 'same' , 'larger' ] as const )
459+ )
460+ . fn ( t => {
461+ if ( typeof VideoFrame === 'undefined' ) {
462+ t . skip ( 'WebCodec is not supported' ) ;
463+ }
464+
465+ const { frame } = createVideoFrameWithDisplayScale ( t , t . params . displayScale ) ;
466+
467+ const colorAttachment = t . createTextureTracked ( {
468+ format : kFormat ,
469+ size : { width : kWidth , height : kHeight , depthOrArrayLayers : 1 } ,
470+ usage : GPUTextureUsage . COPY_SRC | GPUTextureUsage . RENDER_ATTACHMENT ,
471+ } ) ;
472+
473+ const pipeline = createExternalTextureSamplingTestPipeline ( t , kFormat ) ;
474+ const bindGroup = createExternalTextureSamplingTestBindGroup (
475+ t ,
476+ undefined /* checkNonStandardIsZeroCopy */ ,
477+ frame ,
478+ pipeline ,
479+ 'srgb'
480+ ) ;
481+
482+ const commandEncoder = t . device . createCommandEncoder ( ) ;
483+ const passEncoder = commandEncoder . beginRenderPass ( {
484+ colorAttachments : [
485+ {
486+ view : colorAttachment . createView ( ) ,
487+ clearValue : { r : 0.0 , g : 0.0 , b : 0.0 , a : 1.0 } ,
488+ loadOp : 'clear' ,
489+ storeOp : 'store' ,
490+ } ,
491+ ] ,
492+ } ) ;
493+ passEncoder . setPipeline ( pipeline ) ;
494+ passEncoder . setBindGroup ( 0 , bindGroup ) ;
495+ passEncoder . draw ( 6 ) ;
496+ passEncoder . end ( ) ;
497+ t . device . queue . submit ( [ commandEncoder . finish ( ) ] ) ;
498+
499+ const expected = {
500+ topLeft : new Uint8Array ( [ 255 , 0 , 0 , 255 ] ) ,
501+ topRight : new Uint8Array ( [ 0 , 255 , 0 , 255 ] ) ,
502+ bottomLeft : new Uint8Array ( [ 0 , 0 , 255 , 255 ] ) ,
503+ bottomRight : new Uint8Array ( [ 255 , 0 , 255 , 255 ] ) ,
504+ } ;
505+
506+ ttu . expectSinglePixelComparisonsAreOkInTexture ( t , { texture : colorAttachment } , [
507+ // Top-left.
508+ {
509+ coord : { x : kWidth * 0.25 , y : kHeight * 0.25 } ,
510+ exp : expected . topLeft ,
511+ } ,
512+ // Top-right.
513+ {
514+ coord : { x : kWidth * 0.75 , y : kHeight * 0.25 } ,
515+ exp : expected . topRight ,
516+ } ,
517+ // Bottom-left.
518+ {
519+ coord : { x : kWidth * 0.25 , y : kHeight * 0.75 } ,
520+ exp : expected . bottomLeft ,
521+ } ,
522+ // Bottom-right.
523+ {
524+ coord : { x : kWidth * 0.75 , y : kHeight * 0.75 } ,
525+ exp : expected . bottomRight ,
526+ } ,
527+ ] ) ;
528+
529+ frame . close ( ) ;
530+ } ) ;
531+
532+ g . test ( 'importExternalTexture,video_frame_display_size_from_textureDimensions' )
533+ . desc (
534+ `
535+ Tests that textureDimensions() for texture_external matches VideoFrame display size.
536+ `
537+ )
538+ . params ( u =>
539+ u //
540+ . combine ( 'displayScale' , [ 'smaller' , 'same' , 'larger' ] as const )
541+ )
542+ . fn ( t => {
543+ if ( typeof VideoFrame === 'undefined' ) {
544+ t . skip ( 'WebCodec is not supported' ) ;
545+ }
546+
547+ const { frame, displayWidth, displayHeight } = createVideoFrameWithDisplayScale (
548+ t ,
549+ t . params . displayScale
550+ ) ;
551+
552+ const externalTexture = t . device . importExternalTexture ( {
553+ source : frame ,
554+ colorSpace : 'srgb' ,
555+ } ) ;
556+
557+ const storageBuffer = t . createBufferTracked ( {
558+ size : 8 ,
559+ usage : GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ,
560+ } ) ;
561+
562+ const pipeline = t . device . createComputePipeline ( {
563+ layout : 'auto' ,
564+ compute : {
565+ module : t . device . createShaderModule ( {
566+ code : `
567+ @group(0) @binding(0) var t : texture_external;
568+ @group(0) @binding(1) var<storage, read_write> outDims : array<u32>;
569+
570+ @compute @workgroup_size(1) fn main() {
571+ let d = textureDimensions(t);
572+ outDims[0] = d.x;
573+ outDims[1] = d.y;
574+ }
575+ ` ,
576+ } ) ,
577+ entryPoint : 'main' ,
578+ } ,
579+ } ) ;
580+
581+ const bindGroup = t . device . createBindGroup ( {
582+ layout : pipeline . getBindGroupLayout ( 0 ) ,
583+ entries : [
584+ { binding : 0 , resource : externalTexture } ,
585+ { binding : 1 , resource : { buffer : storageBuffer } } ,
586+ ] ,
587+ } ) ;
588+
589+ const encoder = t . device . createCommandEncoder ( ) ;
590+ const pass = encoder . beginComputePass ( ) ;
591+ pass . setPipeline ( pipeline ) ;
592+ pass . setBindGroup ( 0 , bindGroup ) ;
593+ pass . dispatchWorkgroups ( 1 ) ;
594+ pass . end ( ) ;
595+ t . device . queue . submit ( [ encoder . finish ( ) ] ) ;
596+
597+ t . expectGPUBufferValuesEqual ( storageBuffer , new Uint32Array ( [ displayWidth , displayHeight ] ) ) ;
598+
599+ frame . close ( ) ;
600+ } ) ;
601+
384602g . test ( 'importExternalTexture,sampleWithVideoFrameWithVisibleRectParam' )
385603 . desc (
386604 `
0 commit comments