| // Copyright 2019 The Dawn Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "tests/DawnTest.h" |
| |
| #include "dawn_native/MetalBackend.h" |
| #include "utils/ComboRenderPipelineDescriptor.h" |
| #include "utils/WGPUHelpers.h" |
| |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <CoreVideo/CVPixelBuffer.h> |
| #include <IOSurface/IOSurface.h> |
| |
| namespace { |
| |
| void AddIntegerValue(CFMutableDictionaryRef dictionary, const CFStringRef key, int32_t value) { |
| CFNumberRef number = CFNumberCreate(nullptr, kCFNumberSInt32Type, &value); |
| CFDictionaryAddValue(dictionary, key, number); |
| CFRelease(number); |
| } |
| |
| class ScopedIOSurfaceRef { |
| public: |
| ScopedIOSurfaceRef() : mSurface(nullptr) { |
| } |
| explicit ScopedIOSurfaceRef(IOSurfaceRef surface) : mSurface(surface) { |
| } |
| |
| ~ScopedIOSurfaceRef() { |
| if (mSurface != nullptr) { |
| CFRelease(mSurface); |
| mSurface = nullptr; |
| } |
| } |
| |
| IOSurfaceRef get() const { |
| return mSurface; |
| } |
| |
| ScopedIOSurfaceRef(ScopedIOSurfaceRef&& other) { |
| if (mSurface != nullptr) { |
| CFRelease(mSurface); |
| } |
| mSurface = other.mSurface; |
| other.mSurface = nullptr; |
| } |
| |
| ScopedIOSurfaceRef& operator=(ScopedIOSurfaceRef&& other) { |
| if (mSurface != nullptr) { |
| CFRelease(mSurface); |
| } |
| mSurface = other.mSurface; |
| other.mSurface = nullptr; |
| |
| return *this; |
| } |
| |
| ScopedIOSurfaceRef(const ScopedIOSurfaceRef&) = delete; |
| ScopedIOSurfaceRef& operator=(const ScopedIOSurfaceRef&) = delete; |
| |
| private: |
| IOSurfaceRef mSurface = nullptr; |
| }; |
| |
| ScopedIOSurfaceRef CreateSinglePlaneIOSurface(uint32_t width, |
| uint32_t height, |
| uint32_t format, |
| uint32_t bytesPerElement) { |
| CFMutableDictionaryRef dict = |
| CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks); |
| AddIntegerValue(dict, kIOSurfaceWidth, width); |
| AddIntegerValue(dict, kIOSurfaceHeight, height); |
| AddIntegerValue(dict, kIOSurfacePixelFormat, format); |
| AddIntegerValue(dict, kIOSurfaceBytesPerElement, bytesPerElement); |
| |
| IOSurfaceRef ioSurface = IOSurfaceCreate(dict); |
| EXPECT_NE(nullptr, ioSurface); |
| CFRelease(dict); |
| |
| return ScopedIOSurfaceRef(ioSurface); |
| } |
| |
| class IOSurfaceTestBase : public DawnTest { |
| public: |
| wgpu::Texture WrapIOSurface(const wgpu::TextureDescriptor* descriptor, |
| IOSurfaceRef ioSurface, |
| uint32_t plane, |
| bool isInitialized = true) { |
| dawn_native::metal::ExternalImageDescriptorIOSurface externDesc; |
| externDesc.cTextureDescriptor = |
| reinterpret_cast<const WGPUTextureDescriptor*>(descriptor); |
| externDesc.ioSurface = ioSurface; |
| externDesc.plane = plane; |
| externDesc.isInitialized = isInitialized; |
| WGPUTexture texture = dawn_native::metal::WrapIOSurface(device.Get(), &externDesc); |
| return wgpu::Texture::Acquire(texture); |
| } |
| }; |
| |
| } // anonymous namespace |
| |
| // A small fixture used to initialize default data for the IOSurface validation tests. |
| // These tests are skipped if the harness is using the wire. |
| class IOSurfaceValidationTests : public IOSurfaceTestBase { |
| public: |
| IOSurfaceValidationTests() { |
| defaultIOSurface = CreateSinglePlaneIOSurface(10, 10, kCVPixelFormatType_32BGRA, 4); |
| |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::BGRA8Unorm; |
| descriptor.size = {10, 10, 1}; |
| descriptor.sampleCount = 1; |
| descriptor.mipLevelCount = 1; |
| descriptor.usage = wgpu::TextureUsage::RenderAttachment; |
| } |
| |
| protected: |
| wgpu::TextureDescriptor descriptor; |
| ScopedIOSurfaceRef defaultIOSurface; |
| }; |
| |
| // Test a successful wrapping of an IOSurface in a texture |
| TEST_P(IOSurfaceValidationTests, Success) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get(), 0); |
| ASSERT_NE(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the texture descriptor is invalid |
| TEST_P(IOSurfaceValidationTests, InvalidTextureDescriptor) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| |
| wgpu::ChainedStruct chainedDescriptor; |
| descriptor.nextInChain = &chainedDescriptor; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the plane is too large |
| TEST_P(IOSurfaceValidationTests, PlaneTooLarge) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 1)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor dimension isn't 2D |
| // TODO(cwallez@chromium.org): Reenable when 1D or 3D textures are implemented |
| TEST_P(IOSurfaceValidationTests, DISABLED_InvalidTextureDimension) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor mip level count isn't 1 |
| TEST_P(IOSurfaceValidationTests, InvalidMipLevelCount) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.mipLevelCount = 2; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor depth isn't 1 |
| TEST_P(IOSurfaceValidationTests, InvalidDepth) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.size.depthOrArrayLayers = 2; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor sample count isn't 1 |
| TEST_P(IOSurfaceValidationTests, InvalidSampleCount) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.sampleCount = 4; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor width doesn't match the surface's |
| TEST_P(IOSurfaceValidationTests, InvalidWidth) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.size.width = 11; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor height doesn't match the surface's |
| TEST_P(IOSurfaceValidationTests, InvalidHeight) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.size.height = 11; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Test an error occurs if the descriptor format isn't compatible with the IOSurface's |
| TEST_P(IOSurfaceValidationTests, InvalidFormat) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| descriptor.format = wgpu::TextureFormat::R8Unorm; |
| |
| ASSERT_DEVICE_ERROR(wgpu::Texture texture = |
| WrapIOSurface(&descriptor, defaultIOSurface.get(), 0)); |
| ASSERT_EQ(texture.Get(), nullptr); |
| } |
| |
| // Fixture to test using IOSurfaces through different usages. |
| // These tests are skipped if the harness is using the wire. |
| class IOSurfaceUsageTests : public IOSurfaceTestBase { |
| public: |
| // Test that sampling a 1x1 works. |
| void DoSampleTest(IOSurfaceRef ioSurface, |
| wgpu::TextureFormat format, |
| void* data, |
| size_t dataSize, |
| RGBA8 expectedColor) { |
| // Write the data to the IOSurface |
| IOSurfaceLock(ioSurface, 0, nullptr); |
| memcpy(IOSurfaceGetBaseAddress(ioSurface), data, dataSize); |
| IOSurfaceUnlock(ioSurface, 0, nullptr); |
| |
| // The simplest texture sampling pipeline. |
| wgpu::RenderPipeline pipeline; |
| { |
| wgpu::ShaderModule vs = utils::CreateShaderModule(device, R"( |
| [[builtin(vertex_index)]] var<in> VertexIndex : u32; |
| [[location(0)]] var<out> o_texCoord : vec2<f32>; |
| [[builtin(position)]] var<out> Position : vec4<f32>; |
| |
| [[stage(vertex)]] fn main() -> void { |
| const pos : array<vec2<f32>, 6> = array<vec2<f32>, 6>( |
| vec2<f32>(-2.0, -2.0), |
| vec2<f32>(-2.0, 2.0), |
| vec2<f32>( 2.0, -2.0), |
| vec2<f32>(-2.0, 2.0), |
| vec2<f32>( 2.0, -2.0), |
| vec2<f32>( 2.0, 2.0)); |
| |
| const texCoord : array<vec2<f32>, 6> = array<vec2<f32>, 6>( |
| vec2<f32>(0.0, 0.0), |
| vec2<f32>(0.0, 1.0), |
| vec2<f32>(1.0, 0.0), |
| vec2<f32>(0.0, 1.0), |
| vec2<f32>(1.0, 0.0), |
| vec2<f32>(1.0, 1.0)); |
| |
| Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0); |
| o_texCoord = texCoord[VertexIndex]; |
| } |
| )"); |
| wgpu::ShaderModule fs = utils::CreateShaderModule(device, R"( |
| [[group(0), binding(0)]] var sampler0 : sampler; |
| [[group(0), binding(1)]] var texture0 : texture_2d<f32>; |
| |
| [[location(0)]] var<in> texCoord : vec2<f32>; |
| [[location(0)]] var<out> fragColor : vec4<f32>; |
| |
| [[stage(fragment)]] fn main() -> void { |
| fragColor = textureSample(texture0, sampler0, texCoord); |
| } |
| )"); |
| |
| utils::ComboRenderPipelineDescriptor2 descriptor; |
| descriptor.vertex.module = vs; |
| descriptor.cFragment.module = fs; |
| descriptor.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm; |
| |
| pipeline = device.CreateRenderPipeline2(&descriptor); |
| } |
| |
| // The bindgroup containing the texture view for the ioSurface as well as the sampler. |
| wgpu::BindGroup bindGroup; |
| { |
| wgpu::TextureDescriptor textureDescriptor; |
| textureDescriptor.dimension = wgpu::TextureDimension::e2D; |
| textureDescriptor.format = format; |
| textureDescriptor.size = {1, 1, 1}; |
| textureDescriptor.sampleCount = 1; |
| textureDescriptor.mipLevelCount = 1; |
| textureDescriptor.usage = wgpu::TextureUsage::Sampled; |
| wgpu::Texture wrappingTexture = WrapIOSurface(&textureDescriptor, ioSurface, 0); |
| |
| wgpu::TextureView textureView = wrappingTexture.CreateView(); |
| |
| wgpu::Sampler sampler = device.CreateSampler(); |
| |
| bindGroup = utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), |
| {{0, sampler}, {1, textureView}}); |
| } |
| |
| // Submit commands samping from the ioSurface and writing the result to renderPass.color |
| utils::BasicRenderPass renderPass = utils::CreateBasicRenderPass(device, 1, 1); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| { |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup); |
| pass.Draw(6); |
| pass.EndPass(); |
| } |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| EXPECT_PIXEL_RGBA8_EQ(expectedColor, renderPass.color, 0, 0); |
| } |
| |
| // Test that clearing using BeginRenderPass writes correct data in the ioSurface. |
| void DoClearTest(IOSurfaceRef ioSurface, |
| wgpu::TextureFormat format, |
| void* data, |
| size_t dataSize) { |
| // Get a texture view for the ioSurface |
| wgpu::TextureDescriptor textureDescriptor; |
| textureDescriptor.dimension = wgpu::TextureDimension::e2D; |
| textureDescriptor.format = format; |
| textureDescriptor.size = {1, 1, 1}; |
| textureDescriptor.sampleCount = 1; |
| textureDescriptor.mipLevelCount = 1; |
| textureDescriptor.usage = wgpu::TextureUsage::RenderAttachment; |
| wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface, 0); |
| |
| wgpu::TextureView ioSurfaceView = ioSurfaceTexture.CreateView(); |
| |
| utils::ComboRenderPassDescriptor renderPassDescriptor({ioSurfaceView}, {}); |
| renderPassDescriptor.cColorAttachments[0].clearColor = {1 / 255.0f, 2 / 255.0f, 3 / 255.0f, |
| 4 / 255.0f}; |
| |
| // Execute commands to clear the ioSurface |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDescriptor); |
| pass.EndPass(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // Wait for the commands touching the IOSurface to be scheduled |
| dawn_native::metal::WaitForCommandsToBeScheduled(device.Get()); |
| |
| // Check the correct data was written |
| IOSurfaceLock(ioSurface, kIOSurfaceLockReadOnly, nullptr); |
| ASSERT_EQ(0, memcmp(IOSurfaceGetBaseAddress(ioSurface), data, dataSize)); |
| IOSurfaceUnlock(ioSurface, kIOSurfaceLockReadOnly, nullptr); |
| } |
| }; |
| |
| // Test sampling from a R8 IOSurface |
| TEST_P(IOSurfaceUsageTests, SampleFromR8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = |
| CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_OneComponent8, 1); |
| |
| uint8_t data = 0x01; |
| DoSampleTest(ioSurface.get(), wgpu::TextureFormat::R8Unorm, &data, sizeof(data), |
| RGBA8(1, 0, 0, 255)); |
| } |
| |
| // Test clearing a R8 IOSurface |
| TEST_P(IOSurfaceUsageTests, ClearR8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = |
| CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_OneComponent8, 1); |
| |
| uint8_t data = 0x01; |
| DoClearTest(ioSurface.get(), wgpu::TextureFormat::R8Unorm, &data, sizeof(data)); |
| } |
| |
| // Test sampling from a RG8 IOSurface |
| TEST_P(IOSurfaceUsageTests, SampleFromRG8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = |
| CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_TwoComponent8, 2); |
| |
| uint16_t data = 0x0102; // Stored as (G, R) |
| DoSampleTest(ioSurface.get(), wgpu::TextureFormat::RG8Unorm, &data, sizeof(data), |
| RGBA8(2, 1, 0, 255)); |
| } |
| |
| // Test clearing a RG8 IOSurface |
| TEST_P(IOSurfaceUsageTests, ClearRG8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = |
| CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_TwoComponent8, 2); |
| |
| uint16_t data = 0x0201; |
| DoClearTest(ioSurface.get(), wgpu::TextureFormat::RG8Unorm, &data, sizeof(data)); |
| } |
| |
| // Test sampling from a BGRA8 IOSurface |
| TEST_P(IOSurfaceUsageTests, SampleFromBGRA8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32BGRA, 4); |
| |
| uint32_t data = 0x01020304; // Stored as (A, R, G, B) |
| DoSampleTest(ioSurface.get(), wgpu::TextureFormat::BGRA8Unorm, &data, sizeof(data), |
| RGBA8(2, 3, 4, 1)); |
| } |
| |
| // Test clearing a BGRA8 IOSurface |
| TEST_P(IOSurfaceUsageTests, ClearBGRA8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32BGRA, 4); |
| |
| uint32_t data = 0x04010203; |
| DoClearTest(ioSurface.get(), wgpu::TextureFormat::BGRA8Unorm, &data, sizeof(data)); |
| } |
| |
| // Test sampling from an RGBA8 IOSurface |
| TEST_P(IOSurfaceUsageTests, SampleFromRGBA8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32RGBA, 4); |
| |
| uint32_t data = 0x01020304; // Stored as (A, B, G, R) |
| DoSampleTest(ioSurface.get(), wgpu::TextureFormat::RGBA8Unorm, &data, sizeof(data), |
| RGBA8(4, 3, 2, 1)); |
| } |
| |
| // Test clearing an RGBA8 IOSurface |
| TEST_P(IOSurfaceUsageTests, ClearRGBA8IOSurface) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32RGBA, 4); |
| |
| uint32_t data = 0x04030201; |
| DoClearTest(ioSurface.get(), wgpu::TextureFormat::RGBA8Unorm, &data, sizeof(data)); |
| } |
| |
| // Test that texture with color is cleared when isInitialized = false |
| TEST_P(IOSurfaceUsageTests, UninitializedTextureIsCleared) { |
| DAWN_SKIP_TEST_IF(UsesWire()); |
| |
| ScopedIOSurfaceRef ioSurface = CreateSinglePlaneIOSurface(1, 1, kCVPixelFormatType_32RGBA, 4); |
| uint32_t data = 0x04030201; |
| |
| IOSurfaceLock(ioSurface.get(), 0, nullptr); |
| memcpy(IOSurfaceGetBaseAddress(ioSurface.get()), &data, sizeof(data)); |
| IOSurfaceUnlock(ioSurface.get(), 0, nullptr); |
| |
| wgpu::TextureDescriptor textureDescriptor; |
| textureDescriptor.dimension = wgpu::TextureDimension::e2D; |
| textureDescriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| textureDescriptor.size = {1, 1, 1}; |
| textureDescriptor.sampleCount = 1; |
| textureDescriptor.mipLevelCount = 1; |
| textureDescriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; |
| |
| // wrap ioSurface and ensure color is not visible when isInitialized set to false |
| wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface.get(), 0, false); |
| EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 0, 0, 0), ioSurfaceTexture, 0, 0); |
| } |
| |
| DAWN_INSTANTIATE_TEST(IOSurfaceValidationTests, MetalBackend()); |
| DAWN_INSTANTIATE_TEST(IOSurfaceUsageTests, MetalBackend()); |