| // Copyright 2021 The Emscripten Authors. All rights reserved. |
| // Emscripten is available under two separate licenses, the MIT license and the |
| // University of Illinois/NCSA Open Source License. Both these licenses can be |
| // found in the LICENSE file. |
| |
| // Based on https://github.com/kainino0x/webgpu-cross-platform-demo |
| // (specifically on an old version that didn't use Asyncify; here we've |
| // intentionally kept Asyncify off to have some coverage of that case). |
| |
| #include <webgpu/webgpu_cpp.h> |
| |
| #undef NDEBUG |
| #include <array> |
| #include <cassert> |
| #include <cstdio> |
| #include <cstdlib> |
| #include <memory> |
| |
| #include <emscripten.h> |
| #include <emscripten/html5.h> |
| #include <emscripten/em_js.h> |
| |
| EM_JS_DEPS(deps, "$keepRuntimeAlive"); |
| |
| // Keeps track of whether async tests are still alive to make sure they finish |
| // before exit. This tests that keepalives exist where they should. |
| static int sScopeCount = 0; |
| class ScopedCounter { |
| public: |
| ScopedCounter(const ScopedCounter&&) { Increment(); } |
| ScopedCounter(const ScopedCounter&) { Increment(); } |
| ScopedCounter() { Increment(); } |
| ~ScopedCounter() { Decrement(); } |
| private: |
| void Increment() { sScopeCount++; } |
| void Decrement() { assert(sScopeCount > 0); sScopeCount--; } |
| }; |
| |
| void RegisterCheckScopesAtExit() { |
| atexit([](){ |
| // Check we don't exit before the tests are done. |
| // (Make sure there's a keepalive for everything the test has scopes for.) |
| // Build with -sRUNTIME_DEBUG to trace keepalives. |
| assert(sScopeCount == 0); |
| }); |
| } |
| |
| static const wgpu::Instance instance = wgpuCreateInstance(nullptr); |
| |
| static const char shaderCode[] = R"( |
| @vertex |
| fn main_v(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> { |
| var pos = array<vec2<f32>, 3>( |
| vec2<f32>(0.0, 0.5), vec2<f32>(-0.5, -0.5), vec2<f32>(0.5, -0.5)); |
| return vec4<f32>(pos[idx], 0.0, 1.0); |
| } |
| |
| @fragment |
| fn main_f() -> @location(0) vec4<f32> { |
| return vec4<f32>(0.0, 0.502, 1.0, 1.0); // 0x80/0xff ~= 0.502 |
| } |
| )"; |
| |
| static wgpu::Adapter adapter; |
| static wgpu::Device device; |
| static wgpu::Queue queue; |
| static wgpu::Buffer readbackBuffer; |
| static wgpu::RenderPipeline pipeline; |
| |
| void GetDevice(void (*callback)()) { |
| instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous, [=](wgpu::RequestAdapterStatus status, wgpu::Adapter a, wgpu::StringView message) { |
| if (message.length) { |
| printf("RequestAdapter: %.*s\n", int(message.length), message.data); |
| } |
| assert(status == wgpu::RequestAdapterStatus::Success); |
| |
| wgpu::DeviceDescriptor desc; |
| desc.SetUncapturedErrorCallback( |
| [](const wgpu::Device&, wgpu::ErrorType errorType, wgpu::StringView message) { |
| printf("UncapturedError (type=%d): %.*s\n", errorType, int(message.length), message.data); |
| }); |
| |
| adapter = a; |
| adapter.RequestDevice(&desc, wgpu::CallbackMode::AllowSpontaneous, [=](wgpu::RequestDeviceStatus status, wgpu::Device d, wgpu::StringView message) { |
| if (message.length) { |
| printf("RequestDevice: %.*s\n", int(message.length), message.data); |
| } |
| assert(status == wgpu::RequestDeviceStatus::Success); |
| |
| device = d; |
| callback(); |
| }); |
| }); |
| } |
| |
| void init() { |
| queue = device.GetQueue(); |
| |
| wgpu::ShaderModule shaderModule{}; |
| { |
| wgpu::ShaderSourceWGSL wgslDesc{}; |
| wgslDesc.code = shaderCode; |
| |
| wgpu::ShaderModuleDescriptor descriptor{}; |
| descriptor.nextInChain = &wgslDesc; |
| shaderModule = device.CreateShaderModule(&descriptor); |
| shaderModule.GetCompilationInfo(wgpu::CallbackMode::AllowSpontaneous, |
| [=](wgpu::CompilationInfoRequestStatus status, const wgpu::CompilationInfo* info) { |
| assert(status == wgpu::CompilationInfoRequestStatus::Success); |
| assert(info->messageCount == 0); |
| printf("Shader compile succeeded\n"); |
| }); |
| } |
| |
| { |
| wgpu::BindGroupLayoutDescriptor bglDesc{}; |
| auto bgl = device.CreateBindGroupLayout(&bglDesc); |
| wgpu::BindGroupDescriptor desc{}; |
| desc.layout = bgl; |
| desc.entryCount = 0; |
| desc.entries = nullptr; |
| device.CreateBindGroup(&desc); |
| } |
| |
| { |
| wgpu::PipelineLayoutDescriptor pl{}; |
| pl.bindGroupLayoutCount = 0; |
| pl.bindGroupLayouts = nullptr; |
| |
| wgpu::ColorTargetState colorTargetState{}; |
| colorTargetState.format = wgpu::TextureFormat::BGRA8Unorm; |
| |
| wgpu::FragmentState fragmentState{}; |
| fragmentState.module = shaderModule; |
| fragmentState.targetCount = 1; |
| fragmentState.targets = &colorTargetState; |
| |
| wgpu::DepthStencilState depthStencilState{}; |
| depthStencilState.format = wgpu::TextureFormat::Depth32Float; |
| depthStencilState.depthCompare = wgpu::CompareFunction::Always; |
| |
| wgpu::RenderPipelineDescriptor descriptor{}; |
| descriptor.layout = device.CreatePipelineLayout(&pl); |
| descriptor.vertex.module = shaderModule; |
| descriptor.fragment = &fragmentState; |
| descriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList; |
| descriptor.depthStencil = &depthStencilState; |
| pipeline = device.CreateRenderPipeline(&descriptor); |
| } |
| } |
| |
| // The depth stencil attachment isn't really needed to draw the triangle |
| // and doesn't really affect the render result. |
| // But having one should give us a slightly better test coverage for the compile of the depth stencil descriptor. |
| void render(wgpu::TextureView view, wgpu::TextureView depthStencilView) { |
| wgpu::RenderPassColorAttachment attachment{}; |
| attachment.view = view; |
| attachment.loadOp = wgpu::LoadOp::Clear; |
| attachment.storeOp = wgpu::StoreOp::Store; |
| attachment.clearValue = {0, 0, 0, 1}; |
| |
| wgpu::RenderPassDescriptor renderpass{}; |
| renderpass.colorAttachmentCount = 1; |
| renderpass.colorAttachments = &attachment; |
| |
| wgpu::RenderPassDepthStencilAttachment depthStencilAttachment = {}; |
| depthStencilAttachment.view = depthStencilView; |
| depthStencilAttachment.depthClearValue = 0; |
| depthStencilAttachment.depthLoadOp = wgpu::LoadOp::Clear; |
| depthStencilAttachment.depthStoreOp = wgpu::StoreOp::Store; |
| |
| renderpass.depthStencilAttachment = &depthStencilAttachment; |
| |
| wgpu::CommandBuffer commands; |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| { |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass); |
| pass.SetPipeline(pipeline); |
| pass.Draw(3); |
| pass.End(); |
| } |
| commands = encoder.Finish(); |
| } |
| |
| queue.Submit(1, &commands); |
| } |
| |
| void issueContentsCheck(ScopedCounter scope, std::string functionName, wgpu::Buffer readbackBuffer, uint32_t expectData) { |
| readbackBuffer.MapAsync( |
| wgpu::MapMode::Read, 0, 4, wgpu::CallbackMode::AllowSpontaneous, |
| [=, scope=scope](wgpu::MapAsyncStatus status, wgpu::StringView message) { |
| if (message.length) { |
| printf("readbackBuffer.MapAsync: %.*s\n", int(message.length), message.data); |
| } |
| assert(status == wgpu::MapAsyncStatus::Success); |
| |
| const void* ptr = readbackBuffer.GetConstMappedRange(); |
| |
| printf("%s: readback -> %p%s\n", functionName.c_str(), |
| ptr, ptr ? "" : " <------- FAILED"); |
| assert(ptr != nullptr); |
| uint32_t readback = static_cast<const uint32_t*>(ptr)[0]; |
| readbackBuffer.Unmap(); |
| printf(" got %08x, expected %08x%s\n", |
| readback, expectData, |
| readback == expectData ? "" : " <------- FAILED"); |
| }); |
| } |
| |
| void doCopyTestMappedAtCreation(ScopedCounter scope, bool useRange) { |
| static constexpr uint32_t kValue = 0x05060708; |
| size_t size = useRange ? 12 : 4; |
| wgpu::Buffer src; |
| { |
| wgpu::BufferDescriptor descriptor{}; |
| descriptor.size = size; |
| descriptor.usage = wgpu::BufferUsage::CopySrc; |
| descriptor.mappedAtCreation = true; |
| src = device.CreateBuffer(&descriptor); |
| } |
| size_t offset = useRange ? 8 : 0; |
| uint32_t* ptr = static_cast<uint32_t*>(useRange ? |
| src.GetMappedRange(offset, 4) : |
| src.GetMappedRange()); |
| printf("%s: getMappedRange -> %p%s\n", __FUNCTION__, |
| ptr, ptr ? "" : " <------- FAILED"); |
| assert(ptr != nullptr); |
| *ptr = kValue; |
| src.Unmap(); |
| |
| wgpu::Buffer dst; |
| { |
| wgpu::BufferDescriptor descriptor{}; |
| descriptor.size = 4; |
| descriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; |
| dst = device.CreateBuffer(&descriptor); |
| } |
| |
| // Write some random data to the buffer, just to verify that |
| // wgpuQueueWriteBuffer works. |
| char data[4]; |
| queue.WriteBuffer(dst, 0, data, sizeof(data)); |
| |
| wgpu::CommandBuffer commands; |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyBufferToBuffer(src, offset, dst, 0, 4); |
| commands = encoder.Finish(); |
| } |
| queue.Submit(1, &commands); |
| |
| issueContentsCheck(scope, __FUNCTION__, dst, kValue); |
| } |
| |
| void doCopyTestMapAsync(ScopedCounter scope, bool useRange) { |
| static constexpr uint32_t kValue = 0x01020304; |
| size_t size = useRange ? 12 : 4; |
| wgpu::Buffer src; |
| { |
| wgpu::BufferDescriptor descriptor{}; |
| descriptor.size = size; |
| descriptor.usage = wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc; |
| src = device.CreateBuffer(&descriptor); |
| } |
| size_t offset = useRange ? 8 : 0; |
| |
| std::string functionName = __FUNCTION__; |
| src.MapAsync( |
| wgpu::MapMode::Write, offset, 4, wgpu::CallbackMode::AllowSpontaneous, |
| [=](wgpu::MapAsyncStatus status, wgpu::StringView message) { |
| if (message.length) { |
| printf("src.MapAsync: %.*s\n", int(message.length), message.data); |
| } |
| assert(status == wgpu::MapAsyncStatus::Success); |
| |
| uint32_t* ptr = static_cast<uint32_t*>(useRange ? |
| src.GetMappedRange(offset, 4) : |
| src.GetMappedRange()); |
| printf("%s: getMappedRange -> %p%s\n", functionName.c_str(), |
| ptr, ptr ? "" : " <------- FAILED"); |
| assert(ptr != nullptr); |
| *ptr = kValue; |
| src.Unmap(); |
| |
| wgpu::Buffer dst; |
| { |
| wgpu::BufferDescriptor descriptor{}; |
| descriptor.size = 4; |
| descriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; |
| dst = device.CreateBuffer(&descriptor); |
| } |
| |
| wgpu::CommandBuffer commands; |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyBufferToBuffer(src, offset, dst, 0, 4); |
| commands = encoder.Finish(); |
| } |
| queue.Submit(1, &commands); |
| |
| issueContentsCheck(scope, functionName, dst, kValue); |
| }); |
| } |
| |
| void doRenderTest(ScopedCounter scope) { |
| wgpu::Texture readbackTexture; |
| { |
| wgpu::TextureDescriptor descriptor{}; |
| descriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; |
| descriptor.size = {1, 1, 1}; |
| descriptor.format = wgpu::TextureFormat::BGRA8Unorm; |
| |
| // Test for viewFormats binding |
| std::array<wgpu::TextureFormat, 2> viewFormats = |
| { wgpu::TextureFormat::BGRA8Unorm, wgpu::TextureFormat::BGRA8Unorm }; |
| descriptor.viewFormatCount = viewFormats.size(); |
| descriptor.viewFormats = viewFormats.data(); |
| |
| readbackTexture = device.CreateTexture(&descriptor); |
| } |
| wgpu::Texture depthTexture; |
| { |
| wgpu::TextureDescriptor descriptor{}; |
| descriptor.usage = wgpu::TextureUsage::RenderAttachment; |
| descriptor.size = {1, 1, 1}; |
| descriptor.format = wgpu::TextureFormat::Depth32Float; |
| depthTexture = device.CreateTexture(&descriptor); |
| } |
| render(readbackTexture.CreateView(), depthTexture.CreateView()); |
| |
| { |
| // A little texture.GetFormat test |
| assert(wgpu::TextureFormat::BGRA8Unorm == readbackTexture.GetFormat()); |
| assert(wgpu::TextureFormat::Depth32Float == depthTexture.GetFormat()); |
| } |
| |
| { |
| wgpu::BufferDescriptor descriptor{}; |
| descriptor.size = 4; |
| descriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; |
| |
| readbackBuffer = device.CreateBuffer(&descriptor); |
| } |
| |
| wgpu::CommandBuffer commands; |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::TexelCopyTextureInfo src{}; |
| src.texture = readbackTexture; |
| src.origin = {0, 0, 0}; |
| wgpu::TexelCopyBufferInfo dst{}; |
| dst.buffer = readbackBuffer; |
| dst.layout.bytesPerRow = 256; |
| wgpu::Extent3D extent = {1, 1, 1}; |
| encoder.CopyTextureToBuffer(&src, &dst, &extent); |
| commands = encoder.Finish(); |
| } |
| queue.Submit(1, &commands); |
| |
| // Check the color value encoded in the shader makes it out correctly. |
| static const uint32_t expectData = 0xff0080ff; |
| issueContentsCheck(scope, __FUNCTION__, readbackBuffer, expectData); |
| } |
| |
| wgpu::Surface surface; |
| wgpu::TextureView canvasDepthStencilView; |
| const uint32_t kWidth = 300; |
| const uint32_t kHeight = 150; |
| |
| void frame(void* vp_scope) { |
| auto scope = std::unique_ptr<ScopedCounter>(reinterpret_cast<ScopedCounter*>(vp_scope)); |
| |
| wgpu::SurfaceTexture surfaceTexture; |
| surface.GetCurrentTexture(&surfaceTexture); |
| |
| wgpu::TextureView backbuffer = surfaceTexture.texture.CreateView(); |
| render(backbuffer, canvasDepthStencilView); |
| |
| // Test should complete when runtime exists after all async work is done. |
| emscripten_cancel_main_loop(); |
| } |
| |
| void run() { |
| init(); |
| |
| ScopedCounter scope; |
| doCopyTestMappedAtCreation(scope, false); |
| doCopyTestMappedAtCreation(scope, true); |
| doCopyTestMapAsync(scope, false); |
| doCopyTestMapAsync(scope, true); |
| doRenderTest(scope); |
| |
| { |
| wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc{}; |
| canvasDesc.selector = "#canvas"; |
| |
| wgpu::SurfaceDescriptor surfDesc{}; |
| surfDesc.nextInChain = &canvasDesc; |
| surface = instance.CreateSurface(&surfDesc); |
| |
| wgpu::SurfaceCapabilities capabilities; |
| surface.GetCapabilities(adapter, &capabilities); |
| |
| wgpu::SurfaceConfiguration config{ |
| .device = device, |
| .format = capabilities.formats[0], |
| .usage = wgpu::TextureUsage::RenderAttachment, |
| .width = kWidth, |
| .height = kHeight, |
| .alphaMode = wgpu::CompositeAlphaMode::Auto, |
| .presentMode = wgpu::PresentMode::Fifo}; |
| surface.Configure(&config); |
| |
| { |
| wgpu::TextureDescriptor descriptor{}; |
| descriptor.usage = wgpu::TextureUsage::RenderAttachment; |
| descriptor.size = {kWidth, kHeight, 1}; |
| descriptor.format = wgpu::TextureFormat::Depth32Float; |
| canvasDepthStencilView = device.CreateTexture(&descriptor).CreateView(); |
| } |
| } |
| |
| emscripten_set_main_loop_arg(frame, new ScopedCounter(), 0, false); |
| } |
| |
| int main() { |
| GetDevice(run); |
| |
| RegisterCheckScopesAtExit(); |
| // This is the return code once all runtime-keepalives have completed |
| // (unless something crashes before then). |
| return 0; |
| } |