diff options
author | bors-servo <servo-ops@mozilla.com> | 2020-08-27 13:12:27 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-27 13:12:27 -0400 |
commit | 9e6da58d7793a4576fef38446457e1073a19cd5e (patch) | |
tree | 9413f55965530db94a2c82d68bbb33a4f3d765a9 | |
parent | 84185eb1daf1420597f38a77e40ed3baedb5d521 (diff) | |
parent | 85b6bbb33ace8b433ed279d2cb73ed8d96bb9690 (diff) | |
download | servo-9e6da58d7793a4576fef38446457e1073a19cd5e.tar.gz servo-9e6da58d7793a4576fef38446457e1073a19cd5e.zip |
Auto merge of #27614 - kunalmohan:webgpu-cts, r=kvark
Minor fixes and update cts
<!-- Please describe your changes on the following line: -->
- Prevent redundant buffer and texture destroy calls.
- More subtests for B2B copy pass now.
- All tests under `setViewport()` and `setScissorRect()` pass now.
- Tests for `createTexture()` do not crash. More than 50% of them pass now.
r?@kvark
---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [ ] These changes fix #___ (GitHub issue number if applicable)
<!-- Either: -->
- [X] There are tests for these changes OR
- [ ] These changes do not require tests because ___
<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->
<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
17 files changed, 1390 insertions, 81 deletions
diff --git a/Cargo.lock b/Cargo.lock index b4bc8fe7f39..bebc8f83d36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7028,7 +7028,7 @@ dependencies = [ [[package]] name = "wgpu-core" version = "0.6.0" -source = "git+https://github.com/gfx-rs/wgpu#1d0e0ce37ede5ec53000ab252c27b8cf856865b2" +source = "git+https://github.com/gfx-rs/wgpu#59f0996eabd43e882d4bfc73ee5b4ed912617abf" dependencies = [ "arrayvec 0.5.1", "bitflags", @@ -7055,7 +7055,7 @@ dependencies = [ [[package]] name = "wgpu-types" version = "0.6.0" -source = "git+https://github.com/gfx-rs/wgpu#1d0e0ce37ede5ec53000ab252c27b8cf856865b2" +source = "git+https://github.com/gfx-rs/wgpu#59f0996eabd43e882d4bfc73ee5b4ed912617abf" dependencies = [ "bitflags", "serde", diff --git a/components/script/dom/gpubuffer.rs b/components/script/dom/gpubuffer.rs index 2d721465e68..60a6075cb7f 100644 --- a/components/script/dom/gpubuffer.rs +++ b/components/script/dom/gpubuffer.rs @@ -184,6 +184,7 @@ impl GPUBufferMethods for GPUBuffer { GPUBufferState::Mapped | GPUBufferState::MappedAtCreation => { self.Unmap(); }, + GPUBufferState::Destroyed => return, _ => {}, }; if let Err(e) = self diff --git a/components/script/dom/gputexture.rs b/components/script/dom/gputexture.rs index 175ec7c90cc..b473e52eaae 100644 --- a/components/script/dom/gputexture.rs +++ b/components/script/dom/gputexture.rs @@ -18,6 +18,7 @@ use crate::dom::gpudevice::{ }; use crate::dom::gputextureview::GPUTextureView; use dom_struct::dom_struct; +use std::cell::Cell; use std::num::NonZeroU32; use std::string::String; use webgpu::{ @@ -40,6 +41,7 @@ pub struct GPUTexture { dimension: GPUTextureDimension, format: GPUTextureFormat, texture_usage: u32, + destroyed: Cell<bool>, } impl GPUTexture { @@ -67,6 +69,7 @@ impl GPUTexture { dimension, format, texture_usage, + destroyed: Cell::new(false), } } @@ -197,6 +200,9 @@ impl GPUTextureMethods for GPUTexture { /// https://gpuweb.github.io/gpuweb/#dom-gputexture-destroy fn Destroy(&self) { + if self.destroyed.get() { + return; + } if let Err(e) = self .channel .0 @@ -207,5 +213,6 @@ impl GPUTextureMethods for GPUTexture { self.texture.0, e ); }; + self.destroyed.set(true); } } diff --git a/components/webgpu/lib.rs b/components/webgpu/lib.rs index 7d4dbd3e808..0d6ece51273 100644 --- a/components/webgpu/lib.rs +++ b/components/webgpu/lib.rs @@ -21,7 +21,6 @@ use servo_config::pref; use smallvec::SmallVec; use std::borrow::Cow; use std::cell::RefCell; -use std::collections::hash_map::Entry; use std::collections::HashMap; use std::num::NonZeroU64; use std::rc::Rc; @@ -475,9 +474,7 @@ impl<'a> WGPU<'a> { )) .map_err(|e| format!("{:?}", e)) }; - if result.is_err() { - self.encoder_record_error(command_encoder_id, result.clone()); - } + self.encoder_record_error(command_encoder_id, &result); self.send_result(device_id, scope_id, result); }, WebGPURequest::CopyBufferToBuffer { @@ -497,7 +494,7 @@ impl<'a> WGPU<'a> { destination_offset, size )); - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::CopyBufferToTexture { command_encoder_id, @@ -512,7 +509,7 @@ impl<'a> WGPU<'a> { &destination, ©_size )); - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::CopyTextureToBuffer { command_encoder_id, @@ -527,7 +524,7 @@ impl<'a> WGPU<'a> { &destination, ©_size )); - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::CopyTextureToTexture { command_encoder_id, @@ -542,7 +539,7 @@ impl<'a> WGPU<'a> { &destination, ©_size )); - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::CreateBindGroup { device_id, @@ -985,7 +982,7 @@ impl<'a> WGPU<'a> { } else { Err(String::from("Invalid ComputePass")) }; - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::RunRenderPass { command_encoder_id, @@ -1000,7 +997,7 @@ impl<'a> WGPU<'a> { } else { Err(String::from("Invalid RenderPass")) }; - self.encoder_record_error(command_encoder_id, result); + self.encoder_record_error(command_encoder_id, &result); }, WebGPURequest::Submit { queue_id, @@ -1279,12 +1276,13 @@ impl<'a> WGPU<'a> { fn encoder_record_error<U, T: std::fmt::Debug>( &self, encoder_id: id::CommandEncoderId, - result: Result<U, T>, + result: &Result<U, T>, ) { - if let Err(e) = result { - if let Entry::Vacant(v) = self.error_command_encoders.borrow_mut().entry(encoder_id) { - v.insert(format!("{:?}", e)); - } + if let Err(ref e) = result { + self.error_command_encoders + .borrow_mut() + .entry(encoder_id) + .or_insert_with(|| format!("{:?}", e)); } } } diff --git a/tests/wpt/webgpu/meta/MANIFEST.json b/tests/wpt/webgpu/meta/MANIFEST.json index 28666d5d9c1..f45fcb38480 100644 --- a/tests/wpt/webgpu/meta/MANIFEST.json +++ b/tests/wpt/webgpu/meta/MANIFEST.json @@ -89,7 +89,7 @@ [] ], "params_utils.js": [ - "a95d01b9c8058076af5cf49d4ab82b5c74367a5b", + "d4ffd25372d2d3e975c683ca8a17c2c82d5c8687", [] ], "query": { @@ -98,7 +98,7 @@ [] ], "encode_selectively.js": [ - "62cb55ee039b7cc04dfdf751ce2241ba5864cac6", + "cb365deb4bb688e8ae5eebdef24a2ffcb3d857ef", [] ], "json_param_value.js": [ @@ -106,7 +106,7 @@ [] ], "parseQuery.js": [ - "6c10baab0b2f97dc09e633201dae6618842fbd69", + "8bfd88bc9b66a41e08b3283b13b65bb3da0c10fe", [] ], "query.js": [ @@ -161,7 +161,7 @@ ] }, "version.js": [ - "794e3cdef44214801f15234a61aaf5af338f97fc", + "74eef63b8a1fa6042d1d8352b25eaf95d4b25985", [] ] }, @@ -223,6 +223,10 @@ ] } }, + "copyBetweenLinearDataAndTexture.spec.js": [ + "d2b89189e2c65d50f4f6f0c20f32a7f6512c5b35", + [] + ], "fences.spec.js": [ "98f913008b8af33e1dc866f5714388d4ec9e050d", [] @@ -311,7 +315,7 @@ ], "resource_usages": { "textureUsageInRender.spec.js": [ - "12efa65ce2d9e9ed67cb2be398a0466424d38015", + "b036245663df8d684067eff7c79861253205dbca", [] ] }, @@ -324,7 +328,7 @@ [] ], "setScissorRect.spec.js": [ - "7934c007286c8c4e9702eaaadb14b288a78e8fb1", + "c049e92e70e3ad9a5f71f88a863e5a86cc74fc4e", [] ], "setStencilReference.spec.js": [ @@ -350,7 +354,7 @@ [] ], "gpu_test.js": [ - "f350131af3babe9729dec35261f7c445e2bf41d2", + "21cb10f1429e495b2f32b1d51a09b8584f63707d", [] ], "idl": { @@ -366,7 +370,7 @@ ] }, "listing.js": [ - "3134cf0dd5693705ed82f0828f4dbd93fedf358e", + "cbb23b30ec894c103a957f944b18be1e019ed571", [] ], "util": { @@ -380,7 +384,7 @@ ], "texture": { "layout.js": [ - "c3c610bf0d93ebe66abf81d8f2017deba62641a3", + "927798985fc772a4c9e9ea7a62335ab693b43991", [] ], "subresource.js": [ @@ -435,7 +439,7 @@ "testharness": { "webgpu": { "cts.html": [ - "28d69b38d20367b4c61e72e834f518a11e8de411", + "63357f7e996ecadde32f6816dc131b94e9ab976c", [ "webgpu/cts.html?q=webgpu:api,operation,buffers,map_detach:*", {} @@ -457,6 +461,10 @@ {} ], [ + "webgpu/cts.html?q=webgpu:api,operation,copyBetweenLinearDataAndTexture:*", + {} + ], + [ "webgpu/cts.html?q=webgpu:api,operation,fences:*", {} ], @@ -485,10 +493,6 @@ {} ], [ - "webgpu/cts.html?q=webgpu:api,validation,createTexture:*", - {} - ], - [ "webgpu/cts.html?q=webgpu:api,validation,error_scope:*", {} ], diff --git a/tests/wpt/webgpu/meta/webgpu/cts.html.ini b/tests/wpt/webgpu/meta/webgpu/cts.html.ini index 4f1bc0ef2b3..10c16d8a7f5 100644 --- a/tests/wpt/webgpu/meta/webgpu/cts.html.ini +++ b/tests/wpt/webgpu/meta/webgpu/cts.html.ini @@ -30,36 +30,95 @@ [cts.html?q=webgpu:api,validation,createTexture:*] - expected: CRASH + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc2-rgba-unorm"] + expected: FAIL -[cts.html?q=webgpu:api,validation,setViewport:*] - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=-1;minDepth=0;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc3-rgba-unorm"] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_mipLevelCount:width=32;height=31;mipLevelCount=7] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="r8snorm"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_submit_a_destroyed_texture_before_and_after_encode:destroyBeforeEncode=false;destroyAfterEncode=true] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="rg11b10float"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_submit_a_destroyed_texture_before_and_after_encode:destroyBeforeEncode=true;destroyAfterEncode=false] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_sampleCount:sampleCount=2] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc4-r-unorm"] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_sampleCount:sampleCount=8] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_sampleCount:sampleCount=16] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_sampleCount:sampleCount=4;arrayLayerCount=2] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_mipLevelCount:width=32;height=32;mipLevelCount=0] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc1-rgba-unorm-srgb"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc6h-rgb-ufloat"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc7-rgba-unorm-srgb"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=1;minDepth=10;maxDepth=1] + [webgpu:api,validation,createTexture:validation_of_mipLevelCount:width=32;height=32;mipLevelCount=100] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=0;minDepth=0;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc5-rg-unorm"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=1;minDepth=-1;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc6h-rgb-sfloat"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=0;height=1;minDepth=0;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc7-rgba-unorm"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=1;minDepth=0;maxDepth=-1] + [webgpu:api,validation,createTexture:validation_of_sampleCount:sampleCount=4;mipLevelCount=2] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=0;height=0;minDepth=0;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="rg8snorm"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=-1;height=1;minDepth=0;maxDepth=1] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc1-rgba-unorm"] expected: FAIL - [webgpu:api,validation,setViewport:use_of_setViewport:x=0;y=0;width=1;height=1;minDepth=0;maxDepth=10] + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc4-r-snorm"] expected: FAIL + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc3-rgba-unorm-srgb"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="rgba8snorm"] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc5-rg-snorm"] + expected: FAIL + + [webgpu:api,validation,createTexture:validation_of_mipLevelCount:width=31;height=32;mipLevelCount=7] + expected: FAIL + + [webgpu:api,validation,createTexture:it_is_invalid_to_have_an_output_attachment_texture_with_non_renderable_format:format="bc2-rgba-unorm-srgb"] + expected: FAIL + + +[cts.html?q=webgpu:api,validation,setViewport:*] [cts.html?q=webgpu:web-platform,copyImageBitmapToTexture:*] expected: TIMEOUT @@ -183,15 +242,6 @@ [cts.html?q=webgpu:api,validation,setScissorRect:*] - [webgpu:api,validation,setScissorRect:use_of_setScissorRect:x=0;y=0;width=0;height=1] - expected: FAIL - - [webgpu:api,validation,setScissorRect:use_of_setScissorRect:x=0;y=0;width=0;height=0] - expected: FAIL - - [webgpu:api,validation,setScissorRect:use_of_setScissorRect:x=0;y=0;width=1;height=0] - expected: FAIL - [cts.html?q=webgpu:web-platform,canvas,context_creation:*] @@ -222,15 +272,9 @@ [cts.html?q=webgpu:api,operation,command_buffer,render,basic:*] [cts.html?q=webgpu:api,validation,copyBufferToBuffer:*] - [webgpu:api,validation,copyBufferToBuffer:copy_within_same_buffer:srcOffset=4;dstOffset=0;copySize=8] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=64;dstUsage=512] expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:copy_out_of_bounds:srcOffset=0;dstOffset=36;copySize=0] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=512;dstUsage=8] expected: FAIL @@ -243,9 +287,6 @@ [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=512;dstUsage=1] expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:copy_out_of_bounds:srcOffset=36;dstOffset=0;copySize=0] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=8;dstUsage=512] expected: FAIL @@ -255,15 +296,9 @@ [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=1;dstUsage=512] expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:copy_within_same_buffer:srcOffset=0;dstOffset=4;copySize=8] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=2;dstUsage=512] expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:copy_within_same_buffer:srcOffset=0;dstOffset=8;copySize=4] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=4;dstUsage=512] expected: FAIL @@ -279,9 +314,6 @@ [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=16;dstUsage=512] expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:copy_within_same_buffer:srcOffset=8;dstOffset=0;copySize=4] - expected: FAIL - [webgpu:api,validation,copyBufferToBuffer:buffer_usage:srcUsage=512;dstUsage=64] expected: FAIL @@ -303,3 +335,6 @@ [cts.html?q=webgpu:api,validation,copy_between_linear_data_and_texture,copyBetweenLinearDataAndTexture_dataRelated:*] expected: CRASH + +[cts.html?q=webgpu:api,operation,copyBetweenLinearDataAndTexture:*] + expected: TIMEOUT diff --git a/tests/wpt/webgpu/tests/webgpu/common/framework/params_utils.js b/tests/wpt/webgpu/tests/webgpu/common/framework/params_utils.js index a95d01b9c80..d4ffd25372d 100644 --- a/tests/wpt/webgpu/tests/webgpu/common/framework/params_utils.js +++ b/tests/wpt/webgpu/tests/webgpu/common/framework/params_utils.js @@ -3,6 +3,9 @@ **/ import { comparePublicParamsPaths, Ordering } from './query/compare.js'; import { kWildcard, kParamSeparator, kParamKVSeparator } from './query/separators.js'; // Consider adding more types here if needed +// +// TODO: This type isn't actually used to constrain what you're allowed to do in `.params()`, so +// it's not really serving its purpose. Figure out how to fix that? export function paramKeyIsPublic(key) { return !key.startsWith('_'); diff --git a/tests/wpt/webgpu/tests/webgpu/common/framework/query/encode_selectively.js b/tests/wpt/webgpu/tests/webgpu/common/framework/query/encode_selectively.js index 62cb55ee039..cb365deb4bb 100644 --- a/tests/wpt/webgpu/tests/webgpu/common/framework/query/encode_selectively.js +++ b/tests/wpt/webgpu/tests/webgpu/common/framework/query/encode_selectively.js @@ -17,6 +17,8 @@ ret = ret.replace(/%3D/g, '='); // for params (k=v) ret = ret.replace(/%5B/g, '['); // for JSON arrays ret = ret.replace(/%5D/g, ']'); // for JSON arrays + ret = ret.replace(/%7B/g, '{'); // for JSON objects + ret = ret.replace(/%7D/g, '}'); // for JSON objects ret = ret.replace(/%E2%9C%97/g, '✗'); // for jsUndefinedMagicValue return ret; } diff --git a/tests/wpt/webgpu/tests/webgpu/common/framework/query/parseQuery.js b/tests/wpt/webgpu/tests/webgpu/common/framework/query/parseQuery.js index 6c10baab0b2..8bfd88bc9b6 100644 --- a/tests/wpt/webgpu/tests/webgpu/common/framework/query/parseQuery.js +++ b/tests/wpt/webgpu/tests/webgpu/common/framework/query/parseQuery.js @@ -26,9 +26,29 @@ function parseQueryImpl(s) { // Undo encodeURIComponentSelectively s = decodeURIComponent(s); - // bigParts are: suite, group, test, params (note kBigSeparator could appear in params) - const [suite, fileString, testString, paramsString] = s.split(kBigSeparator, 4); - assert(fileString !== undefined, `filter string must have at least one ${kBigSeparator}`); + // bigParts are: suite, file, test, params (note kBigSeparator could appear in params) + let suite; + let fileString; + let testString; + let paramsString; + { + const i1 = s.indexOf(kBigSeparator); + assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`); + suite = s.substring(0, i1); + const i2 = s.indexOf(kBigSeparator, i1 + 1); + if (i2 === -1) { + fileString = s.substring(i1 + 1); + } else { + fileString = s.substring(i1 + 1, i2); + const i3 = s.indexOf(kBigSeparator, i2 + 1); + if (i3 === -1) { + testString = s.substring(i2 + 1); + } else { + testString = s.substring(i2 + 1, i3); + paramsString = s.substring(i3 + 1); + } + } + } const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator); diff --git a/tests/wpt/webgpu/tests/webgpu/common/framework/version.js b/tests/wpt/webgpu/tests/webgpu/common/framework/version.js index 794e3cdef44..74eef63b8a1 100644 --- a/tests/wpt/webgpu/tests/webgpu/common/framework/version.js +++ b/tests/wpt/webgpu/tests/webgpu/common/framework/version.js @@ -1,3 +1,3 @@ // AUTO-GENERATED - DO NOT EDIT. See tools/gen_version. -export const version = 'fa4873f0a303566ca6f34744a253d96f5e462d3d'; +export const version = 'c1df7f4ff1adcde985384633e7cffa52d53e3535'; diff --git a/tests/wpt/webgpu/tests/webgpu/cts.html b/tests/wpt/webgpu/tests/webgpu/cts.html index 28d69b38d20..63357f7e996 100644 --- a/tests/wpt/webgpu/tests/webgpu/cts.html +++ b/tests/wpt/webgpu/tests/webgpu/cts.html @@ -33,6 +33,7 @@ <meta name=variant content='?q=webgpu:api,operation,command_buffer,basic:*'> <meta name=variant content='?q=webgpu:api,operation,command_buffer,copies:*'> <meta name=variant content='?q=webgpu:api,operation,command_buffer,render,basic:*'> +<meta name=variant content='?q=webgpu:api,operation,copyBetweenLinearDataAndTexture:*'> <meta name=variant content='?q=webgpu:api,operation,fences:*'> <meta name=variant content='?q=webgpu:api,operation,render_pass,storeOp:*'> <!--<meta name=variant content='?q=webgpu:api,operation,resource_init,copied_texture_clear:*'>--> @@ -42,7 +43,7 @@ <meta name=variant content='?q=webgpu:api,validation,createBindGroup:*'> <!--<meta name=variant content='?q=webgpu:api,validation,createBindGroupLayout:*'>--> <meta name=variant content='?q=webgpu:api,validation,createPipelineLayout:*'> -<meta name=variant content='?q=webgpu:api,validation,createTexture:*'> +<!--<meta name=variant content='?q=webgpu:api,validation,createTexture:*'>--> <!--<meta name=variant content='?q=webgpu:api,validation,createView:*'>--> <meta name=variant content='?q=webgpu:api,validation,error_scope:*'> <meta name=variant content='?q=webgpu:api,validation,fences:*'> diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/api/operation/copyBetweenLinearDataAndTexture.spec.js b/tests/wpt/webgpu/tests/webgpu/webgpu/api/operation/copyBetweenLinearDataAndTexture.spec.js new file mode 100644 index 00000000000..d2b89189e2c --- /dev/null +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/api/operation/copyBetweenLinearDataAndTexture.spec.js @@ -0,0 +1,1026 @@ +/** + * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts + **/ export const description = `writeTexture + copyBufferToTexture + copyTextureToBuffer operation tests. + +* copy_with_various_rows_per_image_and_bytes_per_row: test that copying data with various bytesPerRow (including { ==, > } bytesInACompleteRow) and\ + rowsPerImage (including { ==, > } copyExtent.height) values and minimum required bytes in copy works for every format. Also covers special code paths: + - bufferSize - offset < bytesPerImage * copyExtent.depth + - when bytesPerRow is not a multiple of 512 and copyExtent.depth > 1: copyExtent.depth % 2 == { 0, 1 } + - bytesPerRow == bytesInACompleteCopyImage + +* copy_with_various_offsets_and_data_sizes: test that copying data with various offset (including { ==, > } 0 and is/isn't power of 2) values and additional\ + data paddings works for every format with 2d and 2d-array textures. Also covers special code paths: + - offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow + - offset > bytesInACompleteCopyImage + +* copy_with_various_origins_and_copy_extents: test that copying slices of a texture works with various origin (including { origin.x, origin.y, origin.z }\ + { ==, > } 0 and is/isn't power of 2) and copyExtent (including { copyExtent.x, copyExtent.y, copyExtent.z } { ==, > } 0 and is/isn't power of 2) values\ + (also including {origin._ + copyExtent._ { ==, < } the subresource size of textureCopyView) works for all formats. origin and copyExtent values are passed\ + as [number, number, number] instead of GPUExtent3DDict. + +* copy_various_mip_levels: test that copying various mip levels works for all formats. Also covers special code paths: + - the physical size of the subresouce is not equal to the logical size + - bufferSize - offset < bytesPerImage * copyExtent.depth and copyExtent needs to be clamped + +* copy_with_no_image_or_slice_padding_and_undefined_values: test that when copying a single row we can set any bytesPerRow value and when copying a single\ + slice we can set rowsPerImage to 0. Also test setting offset, rowsPerImage, mipLevel, origin, origin.{x,y,z} to undefined. + +* TODO: + - add another initMethod which renders the texture + - because of expectContests 4-bytes alignment we don't test CopyT2B with buffer size not divisible by 4 + - add tests for 1d / 3d textures +`; +import { params, poptions } from '../../../common/framework/params_builder.js'; +import { makeTestGroup } from '../../../common/framework/test_group.js'; +import { assert, unreachable } from '../../../common/framework/util/util.js'; +import { kSizedTextureFormatInfo, kSizedTextureFormats } from '../../capability_info.js'; +import { GPUTest } from '../../gpu_test.js'; +import { align } from '../../util/math.js'; +import { getTextureCopyLayout } from '../../util/texture/layout.js'; + +/** Each combination of methods assume that the ones before it were tested and work correctly. */ +const kMethodsToTest = [ + // We make sure that CopyT2B works when copying the whole texture for renderable formats: + // TODO + // Then we make sure that WriteTexture works for all formats: + { initMethod: 'WriteTexture', checkMethod: 'FullCopyT2B' }, + // Then we make sure that CopyB2T works for all formats: + { initMethod: 'CopyB2T', checkMethod: 'FullCopyT2B' }, + // Then we make sure that CopyT2B works for all formats: + { initMethod: 'WriteTexture', checkMethod: 'PartialCopyT2B' }, +]; + +class CopyBetweenLinearDataAndTextureTest extends GPUTest { + bytesInACompleteRow(copyWidth, format) { + const blockWidth = kSizedTextureFormatInfo[format].blockWidth; + assert(copyWidth % blockWidth === 0); + const copyWidthInBlocks = copyWidth / blockWidth; + return kSizedTextureFormatInfo[format].bytesPerBlock * copyWidthInBlocks; + } + + requiredBytesInCopy(layout, format, copyExtent) { + assert(layout.rowsPerImage % kSizedTextureFormatInfo[format].blockHeight === 0); + assert(copyExtent.height % kSizedTextureFormatInfo[format].blockHeight === 0); + assert(copyExtent.width % kSizedTextureFormatInfo[format].blockWidth === 0); + if (copyExtent.width === 0 || copyExtent.height === 0 || copyExtent.depth === 0) { + return 0; + } else { + const texelBlockRowsPerImage = + layout.rowsPerImage / kSizedTextureFormatInfo[format].blockHeight; + const bytesPerImage = layout.bytesPerRow * texelBlockRowsPerImage; + const bytesInLastSlice = + layout.bytesPerRow * (copyExtent.height / kSizedTextureFormatInfo[format].blockHeight - 1) + + (copyExtent.width / kSizedTextureFormatInfo[format].blockWidth) * + kSizedTextureFormatInfo[format].bytesPerBlock; + return bytesPerImage * (copyExtent.depth - 1) + bytesInLastSlice; + } + } + + /** Offset for a particular texel in the linear texture data */ + getTexelOffsetInBytes(textureDataLayout, format, texel, origin = { x: 0, y: 0, z: 0 }) { + const { offset, bytesPerRow, rowsPerImage } = textureDataLayout; + const info = kSizedTextureFormatInfo[format]; + + assert(texel.x >= origin.x && texel.y >= origin.y && texel.z >= origin.z); + assert(rowsPerImage % info.blockHeight === 0); + assert(texel.x % info.blockWidth === 0); + assert(texel.y % info.blockHeight === 0); + assert(origin.x % info.blockWidth === 0); + assert(origin.y % info.blockHeight === 0); + + const bytesPerImage = (rowsPerImage / info.blockHeight) * bytesPerRow; + + return ( + offset + + (texel.z - origin.z) * bytesPerImage + + ((texel.y - origin.y) / info.blockHeight) * bytesPerRow + + ((texel.x - origin.x) / info.blockWidth) * info.bytesPerBlock + ); + } + + *iterateBlockRows(size, origin, format) { + if (size.width === 0 || size.height === 0 || size.depth === 0) { + // do not iterate anything for an empty region + return; + } + const info = kSizedTextureFormatInfo[format]; + assert(size.height % info.blockHeight === 0); + for (let y = 0; y < size.height / info.blockHeight; ++y) { + for (let z = 0; z < size.depth; ++z) { + yield { + x: origin.x, + y: origin.y + y * info.blockHeight, + z: origin.z + z, + }; + } + } + } + + generateData(byteSize, start = 0) { + const arr = new Uint8Array(byteSize); + for (let i = 0; i < byteSize; ++i) { + arr[i] = (i ** 3 + i + start) % 251; + } + return arr; + } + + /** + * This is used for testing passing undefined members of `GPUTextureDataLayout` instead of actual + * values where possible. Passing arguments as values and not as objects so that they are passed + * by copy and not by reference. + */ + undefDataLayoutIfNeeded(offset, rowsPerImage, bytesPerRow, changeBeforePass) { + if (changeBeforePass === 'undefined') { + if (offset === 0) { + offset = undefined; + } + if (rowsPerImage === 0) { + rowsPerImage = undefined; + } + } + return { offset, bytesPerRow, rowsPerImage }; + } + + /** + * This is used for testing passing undefined members of `GPUTextureCopyView` instead of actual + * values where possible and also for testing passing the origin as `[number, number, number]`. + * Passing arguments as values and not as objects so that they are passed by copy and not by + * reference. + */ + undefOrArrayCopyViewIfNeeded(texture, origin_x, origin_y, origin_z, mipLevel, changeBeforePass) { + let origin = { x: origin_x, y: origin_y, z: origin_z }; + + if (changeBeforePass === 'undefined') { + if (origin_x === 0 && origin_y === 0 && origin_z === 0) { + origin = undefined; + } else { + if (origin_x === 0) { + origin_x = undefined; + } + if (origin_y === 0) { + origin_y = undefined; + } + if (origin_z === 0) { + origin_z = undefined; + } + origin = { x: origin_x, y: origin_y, z: origin_z }; + } + + if (mipLevel === 0) { + mipLevel = undefined; + } + } + + if (changeBeforePass === 'arrays') { + origin = [origin_x, origin_y, origin_z]; + } + + return { texture, origin, mipLevel }; + } + + /** + * This is used for testing passing `GPUExtent3D` as `[number, number, number]` instead of + * `GPUExtent3DDict`. Passing arguments as values and not as objects so that they are passed by + * copy and not by reference. + */ + arrayCopySizeIfNeeded(width, height, depth, changeBeforePass) { + if (changeBeforePass === 'arrays') { + return [width, height, depth]; + } else { + return { width, height, depth }; + } + } + + /** Run a CopyT2B command with appropriate arguments corresponding to `ChangeBeforePass` */ + copyTextureToBufferWithAppliedArguments( + buffer, + { offset, rowsPerImage, bytesPerRow }, + { width, height, depth }, + { texture, mipLevel, origin }, + changeBeforePass + ) { + const { x, y, z } = origin; + + const appliedCopyView = this.undefOrArrayCopyViewIfNeeded( + texture, + x, + y, + z, + mipLevel, + changeBeforePass + ); + + const appliedDataLayout = this.undefDataLayoutIfNeeded( + offset, + rowsPerImage, + bytesPerRow, + changeBeforePass + ); + + const appliedCheckSize = this.arrayCopySizeIfNeeded(width, height, depth, changeBeforePass); + + const encoder = this.device.createCommandEncoder(); + encoder.copyTextureToBuffer( + appliedCopyView, + { buffer, ...appliedDataLayout }, + appliedCheckSize + ); + + this.device.defaultQueue.submit([encoder.finish()]); + } + + /** Put data into a part of the texture with an appropriate method. */ + uploadLinearTextureDataToTextureSubBox( + textureCopyView, + textureDataLayout, + copySize, + partialData, + method, + changeBeforePass + ) { + const { texture, mipLevel, origin } = textureCopyView; + const { offset, rowsPerImage, bytesPerRow } = textureDataLayout; + const { x, y, z } = origin; + const { width, height, depth } = copySize; + + const appliedCopyView = this.undefOrArrayCopyViewIfNeeded( + texture, + x, + y, + z, + mipLevel, + changeBeforePass + ); + + const appliedDataLayout = this.undefDataLayoutIfNeeded( + offset, + rowsPerImage, + bytesPerRow, + changeBeforePass + ); + + const appliedCopySize = this.arrayCopySizeIfNeeded(width, height, depth, changeBeforePass); + + switch (method) { + case 'WriteTexture': { + this.device.defaultQueue.writeTexture( + appliedCopyView, + partialData, + appliedDataLayout, + appliedCopySize + ); + + break; + } + case 'CopyB2T': { + const buffer = this.device.createBuffer({ + mappedAtCreation: true, + size: align(partialData.byteLength, 4), + usage: GPUBufferUsage.COPY_SRC, + }); + + new Uint8Array(buffer.getMappedRange()).set(partialData); + buffer.unmap(); + + const encoder = this.device.createCommandEncoder(); + encoder.copyBufferToTexture( + { buffer, ...appliedDataLayout }, + appliedCopyView, + appliedCopySize + ); + + this.device.defaultQueue.submit([encoder.finish()]); + + break; + } + default: + unreachable(); + } + } + + /** + * We check an appropriate part of the texture against the given data. + * Used directly with PartialCopyT2B check method (for a subpart of the texture) + * and by `copyWholeTextureToBufferAndCheckContentsWithUpdatedData` with FullCopyT2B check method + * (for the whole texture). We also ensure that CopyT2B doesn't overwrite bytes it's not supposed + * to if validateOtherBytesInBuffer is set to true. + */ + copyPartialTextureToBufferAndCheckContents( + { texture, mipLevel, origin }, + checkSize, + format, + expected, + expectedDataLayout, + changeBeforePass = 'none' + ) { + // The alignment is necessary because we need to copy and map data from this buffer. + const bufferSize = align(expected.byteLength, 4); + // The start value ensures generated data here doesn't match the expected data. + const bufferData = this.generateData(bufferSize, 17); + + const buffer = this.device.createBuffer({ + mappedAtCreation: true, + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + new Uint8Array(buffer.getMappedRange()).set(bufferData); + buffer.unmap(); + + this.copyTextureToBufferWithAppliedArguments( + buffer, + expectedDataLayout, + checkSize, + { texture, mipLevel, origin }, + changeBeforePass + ); + + this.updateLinearTextureDataSubBox( + expectedDataLayout, + expectedDataLayout, + checkSize, + origin, + format, + bufferData, + expected + ); + + this.expectContents(buffer, bufferData); + } + + /** + * Copies the whole texture into linear data stored in a buffer for further checks. + * + * Used for `copyWholeTextureToBufferAndCheckContentsWithUpdatedData`. + */ + copyWholeTextureToNewBuffer({ texture, mipLevel }, resultDataLayout) { + const { mipSize, byteLength, bytesPerRow, rowsPerImage } = resultDataLayout; + const buffer = this.device.createBuffer({ + size: align(byteLength, 4), // this is necessary because we need to copy and map data from this buffer + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + const encoder = this.device.createCommandEncoder(); + encoder.copyTextureToBuffer( + { texture, mipLevel }, + { buffer, bytesPerRow, rowsPerImage }, + mipSize + ); + + this.device.defaultQueue.submit([encoder.finish()]); + + return buffer; + } + + copyFromArrayToArray(src, srcOffset, dst, dstOffset, size) { + dst.set(src.subarray(srcOffset, srcOffset + size), dstOffset); + } + + /** + * Takes the data returned by `copyWholeTextureToNewBuffer` and updates it after a copy operation + * on the texture by emulating the copy behaviour here directly. + */ + updateLinearTextureDataSubBox( + { bytesPerRow, rowsPerImage }, + sourceDataLayout, + copySize, + origin, + format, + destination, + source + ) { + for (const texel of this.iterateBlockRows(copySize, origin, format)) { + const sourceOffset = this.getTexelOffsetInBytes(sourceDataLayout, format, texel, origin); + const destinationOffset = this.getTexelOffsetInBytes( + { bytesPerRow, rowsPerImage, offset: 0 }, + format, + texel + ); + + const rowLength = this.bytesInACompleteRow(copySize.width, format); + this.copyFromArrayToArray(source, sourceOffset, destination, destinationOffset, rowLength); + } + } + + /** + * Used for checking whether the whole texture was updated correctly by + * `uploadLinearTextureDataToTextureSubpart`. Takes fullData returned by + * `copyWholeTextureToNewBuffer` before the copy operation which is the original texture data, + * then updates it with `updateLinearTextureDataSubpart` and checks the texture against the + * updated data after the copy operation. + */ + copyWholeTextureToBufferAndCheckContentsWithUpdatedData( + { texture, mipLevel, origin }, + fullTextureCopyLayout, + texturePartialDataLayout, + copySize, + format, + fullData, + partialData + ) { + const { mipSize, bytesPerRow, rowsPerImage, byteLength } = fullTextureCopyLayout; + const { dst, begin, end } = this.createAlignedCopyForMapRead(fullData, byteLength, 0); + + // We add an eventual async expectation which will update the full data and then add + // other eventual async expectations to ensure it will be correct. + this.eventualAsyncExpectation(async () => { + await dst.mapAsync(GPUMapMode.READ); + const actual = new Uint8Array(dst.getMappedRange()).subarray(begin, end); + this.updateLinearTextureDataSubBox( + fullTextureCopyLayout, + texturePartialDataLayout, + copySize, + origin, + format, + actual, + partialData + ); + + this.copyPartialTextureToBufferAndCheckContents( + { texture, mipLevel, origin: { x: 0, y: 0, z: 0 } }, + { width: mipSize[0], height: mipSize[1], depth: mipSize[2] }, + format, + actual, + { bytesPerRow, rowsPerImage, offset: 0 } + ); + + dst.destroy(); + }); + } + + /** + * Tests copy between linear data and texture by creating a texture, putting some data into it + * with WriteTexture/CopyB2T, then getting data for the whole texture/for a part of it back and + * comparing it with the expectation. + */ + uploadTextureAndVerifyCopy({ + textureDataLayout, + copySize, + dataSize, + mipLevel = 0, + origin = { x: 0, y: 0, z: 0 }, + textureSize, + format, + dimension = '2d', + initMethod, + checkMethod, + changeBeforePass = 'none', + }) { + const texture = this.device.createTexture({ + size: textureSize, + format, + dimension, + mipLevelCount: mipLevel + 1, + usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST, + }); + + const data = this.generateData(dataSize); + + switch (checkMethod) { + case 'PartialCopyT2B': { + this.uploadLinearTextureDataToTextureSubBox( + { texture, mipLevel, origin }, + textureDataLayout, + copySize, + data, + initMethod, + changeBeforePass + ); + + this.copyPartialTextureToBufferAndCheckContents( + { texture, mipLevel, origin }, + copySize, + format, + data, + textureDataLayout, + changeBeforePass + ); + + break; + } + case 'FullCopyT2B': { + const fullTextureCopyLayout = getTextureCopyLayout(format, dimension, textureSize, { + mipLevel, + }); + + const fullData = this.copyWholeTextureToNewBuffer( + { texture, mipLevel }, + fullTextureCopyLayout + ); + + this.uploadLinearTextureDataToTextureSubBox( + { texture, mipLevel, origin }, + textureDataLayout, + copySize, + data, + initMethod, + changeBeforePass + ); + + this.copyWholeTextureToBufferAndCheckContentsWithUpdatedData( + { texture, mipLevel, origin }, + fullTextureCopyLayout, + textureDataLayout, + copySize, + format, + fullData, + data + ); + + break; + } + default: + unreachable(); + } + } +} + +/** + * This is a helper function used for filtering test parameters + * + * TODO: Modify this after introducing tests with rendering. + */ +function formatCanBeTested({ format }) { + return kSizedTextureFormatInfo[format].copyDst && kSizedTextureFormatInfo[format].copySrc; +} + +export const g = makeTestGroup(CopyBetweenLinearDataAndTextureTest); + +// Test that copying data with various bytesPerRow and rowsPerImage values and minimum required +// bytes in copy works for every format. +// Covers a special code path for Metal: +// bufferSize - offset < bytesPerImage * copyExtent.depth +// Covers a special code path for D3D12: +// when bytesPerRow is not a multiple of 512 and copyExtent.depth > 1: copyExtent.depth % 2 == { 0, 1 } +// bytesPerRow == bytesInACompleteCopyImage */ +g.test('copy_with_various_rows_per_image_and_bytes_per_row') + .params( + params() + .combine(kMethodsToTest) + .combine([ + { bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 0 }, // no padding + { bytesPerRowPadding: 0, rowsPerImagePaddingInBlocks: 6 }, // rowsPerImage padding + { bytesPerRowPadding: 6, rowsPerImagePaddingInBlocks: 0 }, // bytesPerRow padding + { bytesPerRowPadding: 15, rowsPerImagePaddingInBlocks: 17 }, // both paddings + ]) + .combine([ + // In the two cases below, for (WriteTexture, PartialCopyB2T) and (CopyB2T, FullCopyT2B) + // sets of methods we will have bytesPerRow = 256 and copyDepth % 2 == { 0, 1 } + // respectively. This covers a special code path for D3D12. + { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 5 }, // standard copy + { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 2 }, // standard copy + + { copyWidthInBlocks: 256, copyHeightInBlocks: 3, copyDepth: 2 }, // copyWidth is 256-aligned + { copyWidthInBlocks: 0, copyHeightInBlocks: 4, copyDepth: 5 }, // empty copy because of width + { copyWidthInBlocks: 3, copyHeightInBlocks: 0, copyDepth: 5 }, // empty copy because of height + { copyWidthInBlocks: 3, copyHeightInBlocks: 4, copyDepth: 0 }, // empty copy because of depth + { copyWidthInBlocks: 1, copyHeightInBlocks: 3, copyDepth: 5 }, // copyWidth = 1 + + // The two cases below cover another special code path for D3D12. + // - For (WriteTexture, FullCopyT2B) with r8unorm: + // bytesPerRow = 15 = 3 * 5 = bytesInACompleteCopyImage. + { copyWidthInBlocks: 32, copyHeightInBlocks: 1, copyDepth: 8 }, // copyHeight = 1 + // - For (CopyB2T, FullCopyT2B) and (WriteTexture, PartialCopyT2B) with r8unorm: + // bytesPerRow = 256 = 8 * 32 = bytesInACompleteCopyImage. + { copyWidthInBlocks: 5, copyHeightInBlocks: 4, copyDepth: 1 }, // copyDepth = 1 + + { copyWidthInBlocks: 7, copyHeightInBlocks: 1, copyDepth: 1 }, // copyHeight = 1 and copyDepth = 1 + ]) + .combine(poptions('format', kSizedTextureFormats)) + .filter(formatCanBeTested) + ) + .fn(async t => { + const { + bytesPerRowPadding, + rowsPerImagePaddingInBlocks, + copyWidthInBlocks, + copyHeightInBlocks, + copyDepth, + format, + initMethod, + checkMethod, + } = t.params; + + const info = kSizedTextureFormatInfo[format]; + + // For CopyB2T and CopyT2B we need to have bytesPerRow 256-aligned, + // to make this happen we align the bytesInACompleteRow value and multiply + // bytesPerRowPadding by 256. + const bytesPerRowAlignment = + initMethod === 'WriteTexture' && checkMethod === 'FullCopyT2B' ? 1 : 256; + + const copyWidth = copyWidthInBlocks * info.blockWidth; + const copyHeight = copyHeightInBlocks * info.blockHeight; + const rowsPerImage = copyHeight + rowsPerImagePaddingInBlocks * info.blockHeight; + const bytesPerRow = + align(t.bytesInACompleteRow(copyWidth, format), bytesPerRowAlignment) + + bytesPerRowPadding * bytesPerRowAlignment; + const copySize = { width: copyWidth, height: copyHeight, depth: copyDepth }; + + const minDataSize = t.requiredBytesInCopy( + { offset: 0, bytesPerRow, rowsPerImage }, + format, + copySize + ); + + t.uploadTextureAndVerifyCopy({ + textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage }, + copySize, + dataSize: minDataSize, + textureSize: [ + Math.max(copyWidth, info.blockWidth), + Math.max(copyHeight, info.blockHeight), + Math.max(copyDepth, 1), + ], + /* making sure the texture is non-empty */ format, + initMethod, + checkMethod, + }); + }); + +// Test that copying data with various offset values and additional data paddings +// works for every format with 2d and 2d-array textures. +// Covers two special code paths for D3D12: +// offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow +// offset > bytesInACompleteCopyImage +g.test('copy_with_various_offsets_and_data_sizes') + .params( + params() + .combine(kMethodsToTest) + .combine([ + { offsetInBlocks: 0, dataPaddingInBytes: 0 }, // no offset and no padding + { offsetInBlocks: 1, dataPaddingInBytes: 0 }, // offset = 1 + { offsetInBlocks: 2, dataPaddingInBytes: 0 }, // offset = 2 + { offsetInBlocks: 15, dataPaddingInBytes: 0 }, // offset = 15 + { offsetInBlocks: 16, dataPaddingInBytes: 0 }, // offset = 16 + { offsetInBlocks: 242, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 242 + 12 = 256 = bytesPerRow + { offsetInBlocks: 243, dataPaddingInBytes: 0 }, // for rgba8unorm format: offset + bytesInCopyExtentPerRow = 243 + 12 > 256 = bytesPerRow + { offsetInBlocks: 768, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 768 = 3 * 256 = bytesInACompleteCopyImage + { offsetInBlocks: 769, dataPaddingInBytes: 0 }, // for copyDepth = 1, blockWidth = 1 and bytesPerBlock = 1: offset = 769 > 768 = bytesInACompleteCopyImage + { offsetInBlocks: 0, dataPaddingInBytes: 1 }, // dataPaddingInBytes > 0 + { offsetInBlocks: 1, dataPaddingInBytes: 8 }, // offset > 0 and dataPaddingInBytes > 0 + ]) + .combine(poptions('copyDepth', [1, 2])) // 2d and 2d-array textures + .combine(poptions('format', kSizedTextureFormats)) + .filter(formatCanBeTested) + ) + .fn(async t => { + const { + offsetInBlocks, + dataPaddingInBytes, + copyDepth, + format, + initMethod, + checkMethod, + } = t.params; + + const info = kSizedTextureFormatInfo[format]; + + const offset = offsetInBlocks * info.bytesPerBlock; + const copySize = { + width: 3 * info.blockWidth, + height: 3 * info.blockHeight, + depth: copyDepth, + }; + + const rowsPerImage = copySize.height; + const bytesPerRow = 256; + + const dataSize = + offset + + t.requiredBytesInCopy({ offset, bytesPerRow, rowsPerImage }, format, copySize) + + dataPaddingInBytes; + + // We're copying a (3 x 3 x copyDepth) (in texel blocks) part of a (4 x 4 x copyDepth) + // (in texel blocks) texture with no origin. + t.uploadTextureAndVerifyCopy({ + textureDataLayout: { offset, bytesPerRow, rowsPerImage }, + copySize, + dataSize, + textureSize: [4 * info.blockWidth, 4 * info.blockHeight, copyDepth], + format, + initMethod, + checkMethod, + }); + }); + +// Test that copying slices of a texture works with various origin and copyExtent values +// for all formats. We pass origin and copyExtent as [number, number, number]. +g.test('copy_with_various_origins_and_copy_extents') + .params( + params() + .combine(kMethodsToTest) + .combine(poptions('originValueInBlocks', [0, 7, 8])) + .combine(poptions('copySizeValueInBlocks', [0, 7, 8])) + .combine(poptions('textureSizePaddingValueInBlocks', [0, 7, 8])) + .unless( + p => + // we can't create an empty texture + p.copySizeValueInBlocks + p.originValueInBlocks + p.textureSizePaddingValueInBlocks === 0 + ) + .combine(poptions('coordinateToTest', ['width', 'height', 'depth'])) + .combine(poptions('format', kSizedTextureFormats)) + .filter(formatCanBeTested) + ) + .fn(async t => { + const { + coordinateToTest, + originValueInBlocks, + copySizeValueInBlocks, + textureSizePaddingValueInBlocks, + format, + initMethod, + checkMethod, + } = t.params; + + const info = kSizedTextureFormatInfo[format]; + + const origin = { x: info.blockWidth, y: info.blockHeight, z: 1 }; + const copySize = { width: 2 * info.blockWidth, height: 2 * info.blockHeight, depth: 2 }; + const textureSize = [3 * info.blockWidth, 3 * info.blockHeight, 3]; + + switch (coordinateToTest) { + case 'width': { + origin.x = originValueInBlocks * info.blockWidth; + copySize.width = copySizeValueInBlocks * info.blockWidth; + textureSize[0] = + origin.x + copySize.width + textureSizePaddingValueInBlocks * info.blockWidth; + break; + } + case 'height': { + origin.y = originValueInBlocks * info.blockHeight; + copySize.height = copySizeValueInBlocks * info.blockHeight; + textureSize[1] = + origin.y + copySize.height + textureSizePaddingValueInBlocks * info.blockHeight; + break; + } + case 'depth': { + origin.z = originValueInBlocks; + copySize.depth = copySizeValueInBlocks; + textureSize[2] = origin.z + copySize.depth + textureSizePaddingValueInBlocks; + break; + } + } + + const rowsPerImage = copySize.height; + const bytesPerRow = align(copySize.width, 256); + const dataSize = t.requiredBytesInCopy( + { offset: 0, bytesPerRow, rowsPerImage }, + format, + copySize + ); + + // For testing width: we copy a (_ x 2 x 2) (in texel blocks) part of a (_ x 3 x 3) + // (in texel blocks) texture with origin (_, 1, 1) (in texel blocks). + // Similarly for other coordinates. + t.uploadTextureAndVerifyCopy({ + textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage }, + copySize, + dataSize, + origin, + textureSize, + format, + initMethod, + checkMethod, + changeBeforePass: 'arrays', + }); + }); + +/** + * Generates textureSizes which correspond to the same physicalSizeAtMipLevel including virtual + * sizes at mip level different from the physical ones. + */ +function* textureSizeExpander({ format, mipLevel, _texturePhysicalSizeAtMipLevelInBlocks }) { + const info = kSizedTextureFormatInfo[format]; + + const widthAtThisLevel = _texturePhysicalSizeAtMipLevelInBlocks.width * info.blockWidth; + const heightAtThisLevel = _texturePhysicalSizeAtMipLevelInBlocks.height * info.blockHeight; + const textureSize = [ + widthAtThisLevel << mipLevel, + heightAtThisLevel << mipLevel, + _texturePhysicalSizeAtMipLevelInBlocks.depth, + ]; + + yield { + textureSize, + }; + + // We choose width and height of the texture so that the values are divisible by blockWidth and + // blockHeight respectively and so that the virtual size at mip level corresponds to the same + // physical size. + // Virtual size at mip level with modified width has width = (physical size width) - (blockWidth / 2). + // Virtual size at mip level with modified height has height = (physical size height) - (blockHeight / 2). + const widthAtPrevLevel = widthAtThisLevel << 1; + const heightAtPrevLevel = heightAtThisLevel << 1; + assert(mipLevel > 0); + assert(widthAtPrevLevel >= info.blockWidth && heightAtPrevLevel >= info.blockHeight); + const modifiedWidth = (widthAtPrevLevel - info.blockWidth) << (mipLevel - 1); + const modifiedHeight = (heightAtPrevLevel - info.blockHeight) << (mipLevel - 1); + + const modifyWidth = info.blockWidth > 1 && modifiedWidth !== textureSize[0]; + const modifyHeight = info.blockHeight > 1 && modifiedHeight !== textureSize[1]; + + if (modifyWidth) { + yield { + textureSize: [modifiedWidth, textureSize[1], textureSize[2]], + }; + } + if (modifyHeight) { + yield { + textureSize: [textureSize[0], modifiedHeight, textureSize[2]], + }; + } + if (modifyWidth && modifyHeight) { + yield { + textureSize: [modifiedWidth, modifiedHeight, textureSize[2]], + }; + } +} + +// Test that copying various mip levels works. +// Covers two special code paths: +// - the physical size of the subresouce is not equal to the logical size +// - bufferSize - offset < bytesPerImage * copyExtent.depth and copyExtent needs to be clamped for all block formats */ +g.test('copy_various_mip_levels') + .params( + params() + .combine(kMethodsToTest) + .combine([ + // origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d texture */ + { + copySizeInBlocks: { width: 5, height: 4, depth: 1 }, + originInBlocks: { x: 3, y: 2, z: 0 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 6, depth: 1 }, + mipLevel: 1, + }, + + // origin + copySize = texturePhysicalSizeAtMipLevel for all coordinates, 2d-array texture + { + copySizeInBlocks: { width: 5, height: 4, depth: 2 }, + originInBlocks: { x: 3, y: 2, z: 1 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 6, depth: 3 }, + mipLevel: 2, + }, + + // origin.x + copySize.width = texturePhysicalSizeAtMipLevel.width + { + copySizeInBlocks: { width: 5, height: 4, depth: 2 }, + originInBlocks: { x: 3, y: 2, z: 1 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 8, height: 7, depth: 4 }, + mipLevel: 3, + }, + + // origin.y + copySize.height = texturePhysicalSizeAtMipLevel.height + { + copySizeInBlocks: { width: 5, height: 4, depth: 2 }, + originInBlocks: { x: 3, y: 2, z: 1 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 6, depth: 4 }, + mipLevel: 4, + }, + + // origin.z + copySize.depth = texturePhysicalSizeAtMipLevel.depth + { + copySizeInBlocks: { width: 5, height: 4, depth: 2 }, + originInBlocks: { x: 3, y: 2, z: 1 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 7, depth: 3 }, + mipLevel: 5, + }, + + // origin + copySize < texturePhysicalSizeAtMipLevel for all coordinates + { + copySizeInBlocks: { width: 5, height: 4, depth: 2 }, + originInBlocks: { x: 3, y: 2, z: 1 }, + _texturePhysicalSizeAtMipLevelInBlocks: { width: 9, height: 7, depth: 4 }, + mipLevel: 6, + }, + ]) + .combine(poptions('format', kSizedTextureFormats)) + .filter(formatCanBeTested) + .expand(textureSizeExpander) + ) + .fn(async t => { + const { + copySizeInBlocks, + originInBlocks, + textureSize, + mipLevel, + format, + initMethod, + checkMethod, + } = t.params; + + const info = kSizedTextureFormatInfo[format]; + + const origin = { + x: originInBlocks.x * info.blockWidth, + y: originInBlocks.y * info.blockHeight, + z: originInBlocks.z, + }; + + const copySize = { + width: copySizeInBlocks.width * info.blockWidth, + height: copySizeInBlocks.height * info.blockHeight, + depth: copySizeInBlocks.depth, + }; + + const rowsPerImage = copySize.height + info.blockHeight; + const bytesPerRow = align(copySize.width, 256); + const dataSize = t.requiredBytesInCopy( + { offset: 0, bytesPerRow, rowsPerImage }, + format, + copySize + ); + + t.uploadTextureAndVerifyCopy({ + textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage }, + copySize, + dataSize, + origin, + mipLevel, + textureSize, + format, + initMethod, + checkMethod, + }); + }); + +// Test that when copying a single row we can set any bytesPerRow value and when copying a single +// slice we can set rowsPerImage to 0. Origin, offset, mipLevel and rowsPerImage values will be set +// to undefined when appropriate. +g.test('copy_with_no_image_or_slice_padding_and_undefined_values') + .params( + params() + .combine(kMethodsToTest) + .combine([ + // copying one row: bytesPerRow and rowsPerImage can be set to 0 + { + bytesPerRow: 0, + rowsPerImage: 0, + copySize: { width: 3, height: 1, depth: 1 }, + origin: { x: 0, y: 0, z: 0 }, + }, + + // copying one row: bytesPerRow can be < bytesInACompleteRow = 400 + { + bytesPerRow: 256, + rowsPerImage: 0, + copySize: { width: 100, height: 1, depth: 1 }, + origin: { x: 0, y: 0, z: 0 }, + }, + + // copying one slice: rowsPerImage = 0 will be set to undefined + { + bytesPerRow: 256, + rowsPerImage: 0, + copySize: { width: 3, height: 3, depth: 1 }, + origin: { x: 0, y: 0, z: 0 }, + }, + + // copying one slice: rowsPerImage = 2 is valid + { + bytesPerRow: 256, + rowsPerImage: 2, + copySize: { width: 3, height: 3, depth: 1 }, + origin: { x: 0, y: 0, z: 0 }, + }, + + // origin.x = 0 will be set to undefined + { + bytesPerRow: 0, + rowsPerImage: 0, + copySize: { width: 1, height: 1, depth: 1 }, + origin: { x: 0, y: 1, z: 1 }, + }, + + // origin.y = 0 will be set to undefined + { + bytesPerRow: 0, + rowsPerImage: 0, + copySize: { width: 1, height: 1, depth: 1 }, + origin: { x: 1, y: 0, z: 1 }, + }, + + // origin.z = 0 will be set to undefined + { + bytesPerRow: 0, + rowsPerImage: 0, + copySize: { width: 1, height: 1, depth: 1 }, + origin: { x: 1, y: 1, z: 0 }, + }, + ]) + ) + .fn(async t => { + const { bytesPerRow, rowsPerImage, copySize, origin, initMethod, checkMethod } = t.params; + + t.uploadTextureAndVerifyCopy({ + textureDataLayout: { offset: 0, bytesPerRow, rowsPerImage }, + copySize, + dataSize: 100 * 3 * 4, + textureSize: [100, 3, 2], + origin, + format: 'rgba8unorm', + initMethod, + checkMethod, + changeBeforePass: 'undefined', + }); + }); diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/resource_usages/textureUsageInRender.spec.js b/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/resource_usages/textureUsageInRender.spec.js index 12efa65ce2d..b036245663d 100644 --- a/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/resource_usages/textureUsageInRender.spec.js +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/resource_usages/textureUsageInRender.spec.js @@ -21,13 +21,22 @@ Test Coverage: - Test combinations of two shader stages: - Texture usages in bindings with invisible shader stages should be tracked. Invisible shader stages include shader stage with visibility none and compute shader stage in render pass. + + - Tests replaced bindings: + - Texture usages via bindings replaced by another setBindGroup() upon the same bindGroup index + in current scope should be tracked. + + - Test texture usages in bundle: + - Texture usages in bundle should be tracked if that bundle is executed in the current scope. `; -import { poptions, params } from '../../../../common/framework/params_builder.js'; +import { pbool, poptions, params } from '../../../../common/framework/params_builder.js'; import { makeTestGroup } from '../../../../common/framework/test_group.js'; import { - kShaderStages, kDepthStencilFormats, kDepthStencilFormatInfo, + kTextureBindingTypes, + kTextureBindingTypeInfo, + kShaderStages, } from '../../../capability_info.js'; import { ValidationTest } from '../validation_test.js'; @@ -52,6 +61,22 @@ class TextureUsageTracking extends ValidationTest { usage, }); } + + createBindGroup(index, view, bindingType, bindingTexFormat) { + return this.device.createBindGroup({ + entries: [{ binding: index, resource: view }], + layout: this.device.createBindGroupLayout({ + entries: [ + { + binding: index, + visibility: GPUShaderStage.FRAGMENT, + type: bindingType, + storageTextureFormat: bindingTexFormat, + }, + ], + }), + }); + } } export const g = makeTestGroup(TextureUsageTracking); @@ -400,3 +425,158 @@ g.test('shader_stages_and_visibility') encoder.finish(); }); }); + +// We should track the texture usages in bindings which are replaced by another setBindGroup() +// call site upon the same index in the same render pass. +g.test('replaced_binding') + .params(poptions('bindingType', kTextureBindingTypes)) + .fn(async t => { + const { bindingType } = t.params; + const info = kTextureBindingTypeInfo[bindingType]; + const bindingTexFormat = info.resource === 'storageTex' ? 'rgba8unorm' : undefined; + + const sampledView = t.createTexture().createView(); + const sampledStorageView = t + .createTexture({ usage: GPUTextureUsage.STORAGE | GPUTextureUsage.SAMPLED }) + .createView(); + + // Create bindGroup0. It has two bindings. These two bindings use different views/subresources. + const bglEntries0 = [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, type: 'sampled-texture' }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + type: bindingType, + storageTextureFormat: bindingTexFormat, + }, + ]; + + const bgEntries0 = [ + { binding: 0, resource: sampledView }, + { binding: 1, resource: sampledStorageView }, + ]; + + const bindGroup0 = t.device.createBindGroup({ + entries: bgEntries0, + layout: t.device.createBindGroupLayout({ entries: bglEntries0 }), + }); + + // Create bindGroup1. It has one binding, which use the same view/subresoure of a binding in + // bindGroup0. So it may or may not conflicts with that binding in bindGroup0. + const bindGroup1 = t.createBindGroup(0, sampledStorageView, 'sampled-texture', undefined); + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + attachment: t.createTexture().createView(), + loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + storeOp: 'store', + }, + ], + }); + + // Set bindGroup0 and bindGroup1. bindGroup0 is replaced by bindGroup1 in the current pass. + // But bindings in bindGroup0 should be tracked too. + pass.setBindGroup(0, bindGroup0); + pass.setBindGroup(0, bindGroup1); + pass.endPass(); + + const success = bindingType === 'writeonly-storage-texture' ? false : true; + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); + +g.test('bindings_in_bundle') + .params( + params() + .combine(pbool('binding0InBundle')) + .combine(pbool('binding1InBundle')) + .combine(poptions('type0', ['render-target', ...kTextureBindingTypes])) + .combine(poptions('type1', ['render-target', ...kTextureBindingTypes])) + .unless( + ({ binding0InBundle, binding1InBundle, type0, type1 }) => + // We can't set 'render-target' in bundle, so we need to exclude it from bundle. + // In addition, if both bindings are non-bundle, there is no need to test it because + // we have far more comprehensive test cases for that situation in this file. + (binding0InBundle && type0 === 'render-target') || + (binding1InBundle && type1 === 'render-target') || + (!binding0InBundle && !binding1InBundle) + ) + ) + .fn(async t => { + const { binding0InBundle, binding1InBundle, type0, type1 } = t.params; + + // Two bindings are attached to the same texture view. + const view = t + .createTexture({ + usage: + GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.STORAGE | GPUTextureUsage.SAMPLED, + }) + .createView(); + + const bindGroups = []; + if (type0 !== 'render-target') { + const binding0TexFormat = type0 === 'sampled-texture' ? undefined : 'rgba8unorm'; + bindGroups[0] = t.createBindGroup(0, view, type0, binding0TexFormat); + } + if (type1 !== 'render-target') { + const binding1TexFormat = type1 === 'sampled-texture' ? undefined : 'rgba8unorm'; + bindGroups[1] = t.createBindGroup(1, view, type1, binding1TexFormat); + } + + const encoder = t.device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + attachment: + // At least one binding is in bundle, which means that its type is not 'render-target'. + // As a result, only one binding's type is 'render-target' at most. + type0 === 'render-target' || type1 === 'render-target' + ? view + : t.createTexture().createView(), + loadValue: { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + storeOp: 'store', + }, + ], + }); + + const bindingsInBundle = [binding0InBundle, binding1InBundle]; + for (let i = 0; i < 2; i++) { + // Create a bundle for each bind group if its bindings is required to be in bundle on purpose. + // Otherwise, call setBindGroup directly in pass if needed (when its binding is not + // 'render-target'). + if (bindingsInBundle[i]) { + const bundleEncoder = t.device.createRenderBundleEncoder({ + colorFormats: ['rgba8unorm'], + }); + + bundleEncoder.setBindGroup(i, bindGroups[i]); + const bundleInPass = bundleEncoder.finish(); + pass.executeBundles([bundleInPass]); + } else if (bindGroups[i] !== undefined) { + pass.setBindGroup(i, bindGroups[i]); + } + } + + pass.endPass(); + + let success = false; + if ( + (type0 === 'sampled-texture' || type0 === 'readonly-storage-texture') && + (type1 === 'sampled-texture' || type1 === 'readonly-storage-texture') + ) { + success = true; + } + + if (type0 === 'writeonly-storage-texture' && type1 === 'writeonly-storage-texture') { + success = true; + } + + // Resource usages in bundle should be tracked. And validation error should be reported + // if needed. + t.expectValidationError(() => { + encoder.finish(); + }, !success); + }); diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/setScissorRect.spec.js b/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/setScissorRect.spec.js index 7934c007286..c049e92e70e 100644 --- a/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/setScissorRect.spec.js +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/api/validation/setScissorRect.spec.js @@ -38,7 +38,7 @@ g.test('use_of_setScissorRect') { x: 0, y: 0, width: 0, height: 1, _success: false }, // Width of zero is not allowed { x: 0, y: 0, width: 1, height: 0, _success: false }, // Height of zero is not allowed { x: 0, y: 0, width: 0, height: 0, _success: false }, // Both width and height of zero are not allowed - { x: 0, y: 0, width: TEXTURE_WIDTH + 1, height: TEXTURE_HEIGHT + 1, _success: true }, // Scissor larger than the framebuffer is allowed + { x: 0, y: 0, width: TEXTURE_WIDTH + 1, height: TEXTURE_HEIGHT + 1, _success: false }, // Scissor larger than the framebuffer is not allowed ]) .fn(async t => { const { x, y, width, height, _success } = t.params; diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/gpu_test.js b/tests/wpt/webgpu/tests/webgpu/webgpu/gpu_test.js index f350131af3b..21cb10f1429 100644 --- a/tests/wpt/webgpu/tests/webgpu/webgpu/gpu_test.js +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/gpu_test.js @@ -18,6 +18,7 @@ import { DevicePool, TestOOMedShouldAttemptGC } from '../common/framework/gpu/de import { attemptGarbageCollection } from '../common/framework/util/collect_garbage.js'; import { assert } from '../common/framework/util/util.js'; +import { align } from './util/math.js'; import { fillTextureDataWithTexelValue, getTextureCopyLayout } from './util/texture/layout.js'; import { getTexelDataRepresentation } from './util/texture/texelData.js'; @@ -76,6 +77,9 @@ export class GPUTest extends Fixture { } createCopyForMapRead(src, srcOffset, size) { + assert(srcOffset % 4 === 0); + assert(size % 4 === 0); + const dst = this.device.createBuffer({ size, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, @@ -91,14 +95,31 @@ export class GPUTest extends Fixture { // TODO: add an expectContents for textures, which logs data: uris on failure + // Offset and size passed to createCopyForMapRead must be divisible by 4. For that + // we might need to copy more bytes from the buffer than we want to map. + // begin and end values represent the part of the copied buffer that stores the contents + // we initially wanted to map. + // The copy will not cause an OOB error because the buffer size must be 4-aligned. + createAlignedCopyForMapRead(src, size, offset) { + const alignedOffset = Math.floor(offset / 4) * 4; + const offsetDifference = offset - alignedOffset; + const alignedSize = align(size + offsetDifference, 4); + const dst = this.createCopyForMapRead(src, alignedOffset, alignedSize); + return { dst, begin: offsetDifference, end: offsetDifference + size }; + } + expectContents(src, expected, srcOffset = 0) { - const dst = this.createCopyForMapRead(src, srcOffset, expected.buffer.byteLength); + const { dst, begin, end } = this.createAlignedCopyForMapRead( + src, + expected.byteLength, + srcOffset + ); this.eventualAsyncExpectation(async niceStack => { const constructor = expected.constructor; await dst.mapAsync(GPUMapMode.READ); const actual = new constructor(dst.getMappedRange()); - const check = this.checkBuffer(actual, expected); + const check = this.checkBuffer(actual.subarray(begin, end), expected); if (check !== undefined) { niceStack.message = check; this.rec.expectationFailed(niceStack); diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/listing.js b/tests/wpt/webgpu/tests/webgpu/webgpu/listing.js index 3134cf0dd56..cbb23b30ec8 100644 --- a/tests/wpt/webgpu/tests/webgpu/webgpu/listing.js +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/listing.js @@ -85,6 +85,14 @@ export const listing = [ "file": [ "api", "operation", + "copyBetweenLinearDataAndTexture" + ], + "description": "writeTexture + copyBufferToTexture + copyTextureToBuffer operation tests.\n\n* copy_with_various_rows_per_image_and_bytes_per_row: test that copying data with various bytesPerRow (including { ==, > } bytesInACompleteRow) and rowsPerImage (including { ==, > } copyExtent.height) values and minimum required bytes in copy works for every format. Also covers special code paths:\n - bufferSize - offset < bytesPerImage * copyExtent.depth\n - when bytesPerRow is not a multiple of 512 and copyExtent.depth > 1: copyExtent.depth % 2 == { 0, 1 }\n - bytesPerRow == bytesInACompleteCopyImage\n\n* copy_with_various_offsets_and_data_sizes: test that copying data with various offset (including { ==, > } 0 and is/isn't power of 2) values and additional data paddings works for every format with 2d and 2d-array textures. Also covers special code paths:\n - offset + bytesInCopyExtentPerRow { ==, > } bytesPerRow\n - offset > bytesInACompleteCopyImage\n\n* copy_with_various_origins_and_copy_extents: test that copying slices of a texture works with various origin (including { origin.x, origin.y, origin.z } { ==, > } 0 and is/isn't power of 2) and copyExtent (including { copyExtent.x, copyExtent.y, copyExtent.z } { ==, > } 0 and is/isn't power of 2) values (also including {origin._ + copyExtent._ { ==, < } the subresource size of textureCopyView) works for all formats. origin and copyExtent values are passed as [number, number, number] instead of GPUExtent3DDict.\n\n* copy_various_mip_levels: test that copying various mip levels works for all formats. Also covers special code paths:\n - the physical size of the subresouce is not equal to the logical size\n - bufferSize - offset < bytesPerImage * copyExtent.depth and copyExtent needs to be clamped\n\n* copy_with_no_image_or_slice_padding_and_undefined_values: test that when copying a single row we can set any bytesPerRow value and when copying a single slice we can set rowsPerImage to 0. Also test setting offset, rowsPerImage, mipLevel, origin, origin.{x,y,z} to undefined.\n\n* TODO:\n - add another initMethod which renders the texture\n - because of expectContests 4-bytes alignment we don't test CopyT2B with buffer size not divisible by 4\n - add tests for 1d / 3d textures" + }, + { + "file": [ + "api", + "operation", "fences" ], "description": "" @@ -252,7 +260,7 @@ export const listing = [ "resource_usages", "textureUsageInRender" ], - "description": "Texture Usages Validation Tests in Render Pass.\n\nTest Coverage:\n\n - For each combination of two texture usages:\n - For various subresource ranges (different mip levels or array layers) that overlap a given\n subresources or not for color formats:\n - Check that an error is generated when read-write or write-write usages are binding to the\n same texture subresource. Otherwise, no error should be generated. One exception is race\n condition upon two writeonly-storage-texture usages, which is valid.\n\n - For each combination of two texture usages:\n - For various aspects (all, depth-only, stencil-only) that overlap a given subresources or not\n for depth/stencil formats:\n - Check that an error is generated when read-write or write-write usages are binding to the\n same aspect. Otherwise, no error should be generated.\n\n - Test combinations of two shader stages:\n - Texture usages in bindings with invisible shader stages should be tracked. Invisible shader\n stages include shader stage with visibility none and compute shader stage in render pass." + "description": "Texture Usages Validation Tests in Render Pass.\n\nTest Coverage:\n\n - For each combination of two texture usages:\n - For various subresource ranges (different mip levels or array layers) that overlap a given\n subresources or not for color formats:\n - Check that an error is generated when read-write or write-write usages are binding to the\n same texture subresource. Otherwise, no error should be generated. One exception is race\n condition upon two writeonly-storage-texture usages, which is valid.\n\n - For each combination of two texture usages:\n - For various aspects (all, depth-only, stencil-only) that overlap a given subresources or not\n for depth/stencil formats:\n - Check that an error is generated when read-write or write-write usages are binding to the\n same aspect. Otherwise, no error should be generated.\n\n - Test combinations of two shader stages:\n - Texture usages in bindings with invisible shader stages should be tracked. Invisible shader\n stages include shader stage with visibility none and compute shader stage in render pass.\n\n - Tests replaced bindings:\n - Texture usages via bindings replaced by another setBindGroup() upon the same bindGroup index\n in current scope should be tracked.\n\n - Test texture usages in bundle:\n - Texture usages in bundle should be tracked if that bundle is executed in the current scope." }, { "file": [ diff --git a/tests/wpt/webgpu/tests/webgpu/webgpu/util/texture/layout.js b/tests/wpt/webgpu/tests/webgpu/webgpu/util/texture/layout.js index c3c610bf0d9..927798985fc 100644 --- a/tests/wpt/webgpu/tests/webgpu/webgpu/util/texture/layout.js +++ b/tests/wpt/webgpu/tests/webgpu/webgpu/util/texture/layout.js @@ -32,7 +32,10 @@ export function getTextureCopyLayout(format, dimension, size, options = kDefault const { blockWidth, blockHeight, bytesPerBlock } = kSizedTextureFormatInfo[format]; - assert(isAligned(mipSize[0], blockWidth)); + // We align mipSize to be the physical size of the texture subresource. + mipSize[0] = align(mipSize[0], blockWidth); + mipSize[1] = align(mipSize[1], blockHeight); + const minBytesPerRow = (mipSize[0] / blockWidth) * bytesPerBlock; const alignedMinBytesPerRow = align(minBytesPerRow, kBytesPerRowAlignment); if (bytesPerRow !== undefined) { |