| // Copyright 2020 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // This is an example to manually test surface code. Controls are the following, scoped to the |
| // currently focused window: |
| // - W: creates a new window. |
| // - L: Latches the current swapchain, to check what happens when the window changes but not the |
| // swapchain. |
| // - R: switches the rendering mode, between "The Red Triangle" and color-cycling clears that's |
| // (WARNING) likely seizure inducing. |
| // - D: cycles the divisor for the swapchain size. |
| // - P: switches present modes. |
| // - A: switches alpha modes. |
| // - F: switches formats. |
| // |
| // Closing all the windows exits the example. ^C also works. |
| // |
| // Things to test manually: |
| // |
| // - Basic tests (with the triangle render mode): |
| // - Check the triangle is red on a black background and with the pointy side up. |
| // - Cycle render modes a bunch and check that the triangle background is always solid black. |
| // - Check that rendering triangles to multiple windows works. |
| // |
| // - Present mode single-window tests (with cycling color render mode): |
| // - Check that Fifo cycles at about 1 cycle per second and has no tearing. |
| // - Check that Mailbox cycles faster than Fifo and has no tearing. |
| // - Check that Immediate cycles faster than Fifo, it is allowed to have tearing. (dragging |
| // between two monitors can help see tearing) |
| // |
| // - Present mode multi-window tests, it should have the same results as single-window tests when |
| // all windows are in the same present mode. In mixed present modes only Immediate windows are |
| // allowed to tear. |
| // |
| // - Resizing tests (with the triangle render mode): |
| // - Check that cycling divisors on the triangle produces lower and lower resolution triangles. |
| // - Check latching the swapchain config and resizing the window a bunch (smaller, bigger, and |
| // diagonal aspect ratio). |
| // |
| // - Config change tests: |
| // - Check that cycling between present modes. |
| // - Check that cycling between alpha modes (it sometimes produce a meaningful difference). |
| // - Check that cycling between formats works and gives the same color. |
| // |
| // - Frame throttling: |
| // - In all present modes, check that there isn't extra latency when going from a render mode |
| // to another (like from the color cycling one to the triangle). |
| // |
| // - Additional things to test that aren't supported in this file yet. |
| // - Check cycling the same window over multiple devices. |
| // - Check sRGB vs not sRGB gradients. |
| // - Check wide gamut / extended color range. |
| // - Check OpenGL rendering with extra usages / depth buffer / MRT. |
| // - Check with GLFW transparency on / off. |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "GLFW/glfw3.h" |
| #include "dawn/common/Assert.h" |
| #include "dawn/common/Log.h" |
| #include "dawn/dawn_proc.h" |
| #include "dawn/native/DawnNative.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| #include "dawn/webgpu_cpp.h" |
| #include "dawn/webgpu_cpp_print.h" |
| #include "webgpu/webgpu_glfw.h" |
| |
| template <typename T> |
| std::string NoPrefix(T wgpuThing) { |
| std::ostringstream o; |
| o << wgpuThing; |
| std::string withPrefix = o.str(); |
| return withPrefix.substr(withPrefix.rfind(':') + 1); |
| } |
| |
| template <typename T> |
| void CycleIn(T* value, const std::vector<T>& cycle) { |
| auto it = std::find(cycle.begin(), cycle.end(), *value); |
| DAWN_ASSERT(it != cycle.end()); |
| it++; |
| if (it != cycle.end()) { |
| *value = *it; |
| } else { |
| *value = cycle.front(); |
| } |
| } |
| |
| struct WindowData { |
| GLFWwindow* window = nullptr; |
| uint64_t serial = 0; |
| |
| float clearCycle = 1.0f; |
| bool latched = false; |
| bool renderTriangle = true; |
| uint32_t divisor = 1; |
| |
| std::vector<wgpu::PresentMode> presentModes; |
| std::vector<wgpu::CompositeAlphaMode> alphaModes; |
| std::vector<wgpu::TextureFormat> formats; |
| |
| wgpu::Surface surface = nullptr; |
| wgpu::SurfaceConfiguration currentConfig; |
| wgpu::SurfaceConfiguration targetConfig; |
| }; |
| |
| static std::unordered_map<GLFWwindow*, std::unique_ptr<WindowData>> windows; |
| static uint64_t windowSerial = 0; |
| |
| static std::unique_ptr<dawn::native::Instance> instance; |
| static wgpu::Adapter adapter; |
| static wgpu::Device device; |
| static wgpu::Queue queue; |
| |
| static std::unordered_map<wgpu::TextureFormat, wgpu::RenderPipeline> trianglePipelines; |
| wgpu::RenderPipeline GetOrCreateTrianglePipeline(wgpu::TextureFormat format) { |
| if (trianglePipelines.count(format)) { |
| return trianglePipelines[format]; |
| } |
| |
| // The hacky pipeline to render a triangle. |
| wgpu::ShaderModule module = dawn::utils::CreateShaderModule(device, R"( |
| @vertex fn vs(@builtin(vertex_index) VertexIndex : u32) |
| -> @builtin(position) vec4f { |
| var pos = array( |
| vec2f( 0.0, 0.5), |
| vec2f(-0.5, -0.5), |
| vec2f( 0.5, -0.5) |
| ); |
| return vec4f(pos[VertexIndex], 0, 1); |
| } |
| |
| @fragment fn fs() -> @location(0) vec4f { |
| return vec4f(1, 0, 0, 1); |
| } |
| )"); |
| |
| dawn::utils::ComboRenderPipelineDescriptor pipelineDesc; |
| pipelineDesc.vertex.module = module; |
| pipelineDesc.cFragment.module = module; |
| pipelineDesc.cTargets[0].format = format; |
| trianglePipelines[format] = device.CreateRenderPipeline(&pipelineDesc); |
| return trianglePipelines[format]; |
| } |
| |
| bool IsSameConfig(const wgpu::SurfaceConfiguration& a, const wgpu::SurfaceConfiguration& b) { |
| DAWN_ASSERT(a.viewFormatCount == 0); |
| DAWN_ASSERT(b.viewFormatCount == 0); |
| return a.device.Get() == b.device.Get() && // |
| a.format == b.format && // |
| a.usage == b.usage && // |
| a.alphaMode == b.alphaMode && // |
| a.width == b.width && // |
| a.height == b.height && // |
| a.presentMode == b.presentMode; |
| } |
| |
| void OnKeyPress(GLFWwindow* window, int key, int, int action, int); |
| |
| void SyncFromWindow(WindowData* data) { |
| int width; |
| int height; |
| glfwGetFramebufferSize(data->window, &width, &height); |
| |
| data->targetConfig.width = std::max(1u, width / data->divisor); |
| data->targetConfig.height = std::max(1u, height / data->divisor); |
| } |
| |
| void AddWindow() { |
| glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); |
| GLFWwindow* window = glfwCreateWindow(400, 400, "", nullptr, nullptr); |
| glfwSetKeyCallback(window, OnKeyPress); |
| |
| wgpu::Surface surface = wgpu::glfw::CreateSurfaceForWindow(instance->Get(), window); |
| wgpu::SurfaceCapabilities caps; |
| surface.GetCapabilities(adapter, &caps); |
| |
| wgpu::SurfaceConfiguration config; |
| config.device = device; |
| config.usage = wgpu::TextureUsage::RenderAttachment; |
| config.format = caps.formats[0]; |
| config.alphaMode = caps.alphaModes[0]; |
| config.presentMode = caps.presentModes[0]; |
| config.width = 0; |
| config.height = 0; |
| |
| std::unique_ptr<WindowData> data = std::make_unique<WindowData>(); |
| data->window = window; |
| data->serial = windowSerial++; |
| data->surface = surface; |
| data->currentConfig = config; |
| data->targetConfig = config; |
| SyncFromWindow(data.get()); |
| data->presentModes.assign(caps.presentModes, caps.presentModes + caps.presentModeCount); |
| data->alphaModes.assign(caps.alphaModes, caps.alphaModes + caps.alphaModeCount); |
| data->formats.assign(caps.formats, caps.formats + caps.formatCount); |
| |
| windows[window] = std::move(data); |
| } |
| |
| void DoRender(WindowData* data) { |
| wgpu::SurfaceTexture surfaceTexture; |
| data->surface.GetCurrentTexture(&surfaceTexture); |
| wgpu::TextureView view = surfaceTexture.texture.CreateView(); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| if (data->renderTriangle) { |
| dawn::utils::ComboRenderPassDescriptor desc({view}); |
| // Use Load to check the surface is lazy cleared (we shouldn't see garbage from previous |
| // frames). |
| desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load; |
| |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc); |
| pass.SetPipeline(GetOrCreateTrianglePipeline(data->currentConfig.format)); |
| pass.Draw(3); |
| pass.End(); |
| } else { |
| data->clearCycle -= 1.0 / 60.f; |
| if (data->clearCycle < 0.0) { |
| data->clearCycle = 1.0f; |
| } |
| |
| dawn::utils::ComboRenderPassDescriptor desc({view}); |
| desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear; |
| desc.cColorAttachments[0].clearValue = {data->clearCycle, 1.0f - data->clearCycle, 0.0f, |
| 1.0f}; |
| |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc); |
| pass.End(); |
| } |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| data->surface.Present(); |
| } |
| |
| std::ostream& operator<<(std::ostream& o, const wgpu::SurfaceConfiguration& desc) { |
| // For now only render attachment is possible. |
| DAWN_ASSERT(desc.usage == wgpu::TextureUsage::RenderAttachment); |
| o << "RenderAttachment "; |
| |
| o << desc.width << "x" << desc.height << " "; |
| o << NoPrefix(desc.format) << " "; |
| o << NoPrefix(desc.presentMode) << " "; |
| o << NoPrefix(desc.alphaMode) << " "; |
| return o; |
| } |
| |
| void UpdateTitle(WindowData* data) { |
| std::ostringstream o; |
| |
| o << data->serial << " "; |
| if (data->divisor != 1) { |
| o << "Divisor:" << data->divisor << " "; |
| } |
| |
| if (data->latched) { |
| o << "Latched: (" << data->currentConfig << ") "; |
| o << "Target: (" << data->targetConfig << ")"; |
| } else { |
| o << "(" << data->currentConfig << ")"; |
| } |
| |
| glfwSetWindowTitle(data->window, o.str().c_str()); |
| } |
| |
| void OnKeyPress(GLFWwindow* window, int key, int, int action, int) { |
| if (action != GLFW_PRESS) { |
| return; |
| } |
| |
| DAWN_ASSERT(windows.count(window) == 1); |
| |
| WindowData* data = windows[window].get(); |
| switch (key) { |
| case GLFW_KEY_W: |
| AddWindow(); |
| break; |
| |
| case GLFW_KEY_L: |
| data->latched = !data->latched; |
| UpdateTitle(data); |
| break; |
| |
| case GLFW_KEY_R: |
| data->renderTriangle = !data->renderTriangle; |
| UpdateTitle(data); |
| break; |
| |
| case GLFW_KEY_D: |
| data->divisor *= 2; |
| if (data->divisor > 32) { |
| data->divisor = 1; |
| } |
| break; |
| |
| case GLFW_KEY_P: |
| CycleIn(&data->targetConfig.presentMode, data->presentModes); |
| break; |
| |
| case GLFW_KEY_A: |
| CycleIn(&data->targetConfig.alphaMode, data->alphaModes); |
| break; |
| |
| case GLFW_KEY_F: |
| CycleIn(&data->targetConfig.format, data->formats); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| int main(int argc, const char* argv[]) { |
| // Setup GLFW |
| glfwSetErrorCallback([](int code, const char* message) { |
| dawn::ErrorLog() << "GLFW error " << code << " " << message; |
| }); |
| if (!glfwInit()) { |
| return 1; |
| } |
| |
| // Choose an adapter we like. |
| // TODO(dawn:269): allow switching the window between devices. |
| DawnProcTable procs = dawn::native::GetProcs(); |
| dawnProcSetProcs(&procs); |
| |
| instance = std::make_unique<dawn::native::Instance>(); |
| |
| dawn::native::Adapter chosenAdapter = instance->EnumerateAdapters()[0]; |
| DAWN_ASSERT(chosenAdapter); |
| adapter = wgpu::Adapter(chosenAdapter.Get()); |
| |
| // Setup the device on that adapter. |
| device = wgpu::Device::Acquire(chosenAdapter.CreateDevice()); |
| device.SetUncapturedErrorCallback( |
| [](WGPUErrorType errorType, const char* message, void*) { |
| const char* errorTypeName = ""; |
| switch (errorType) { |
| case WGPUErrorType_Validation: |
| errorTypeName = "Validation"; |
| break; |
| case WGPUErrorType_OutOfMemory: |
| errorTypeName = "Out of memory"; |
| break; |
| case WGPUErrorType_Unknown: |
| errorTypeName = "Unknown"; |
| break; |
| case WGPUErrorType_DeviceLost: |
| errorTypeName = "Device lost"; |
| break; |
| default: |
| DAWN_UNREACHABLE(); |
| return; |
| } |
| dawn::ErrorLog() << errorTypeName << " error: " << message; |
| DAWN_ASSERT(false); |
| }, |
| nullptr); |
| queue = device.GetQueue(); |
| |
| // Create the first window, since the example exits when there are no windows. |
| AddWindow(); |
| |
| while (windows.size() != 0) { |
| glfwPollEvents(); |
| wgpuInstanceProcessEvents(instance->Get()); |
| |
| for (auto it = windows.begin(); it != windows.end();) { |
| GLFWwindow* window = it->first; |
| |
| if (glfwWindowShouldClose(window)) { |
| glfwDestroyWindow(window); |
| it = windows.erase(it); |
| } else { |
| it++; |
| } |
| } |
| |
| for (auto& it : windows) { |
| WindowData* data = it.second.get(); |
| |
| SyncFromWindow(data); |
| if (!IsSameConfig(data->currentConfig, data->targetConfig) && !data->latched) { |
| data->surface.Configure(&data->targetConfig); |
| data->currentConfig = data->targetConfig; |
| } |
| UpdateTitle(data); |
| DoRender(data); |
| } |
| } |
| } |