1 // Copyright 2020 The Dawn Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 // This is an example to manually test swapchain code. Controls are the following, scoped to the
16 // currently focused window:
17 //  - W: creates a new window.
18 //  - L: Latches the current swapchain, to check what happens when the window changes but not the
19 //    swapchain.
20 //  - R: switches the rendering mode, between "The Red Triangle" and color-cycling clears that's
21 //    (WARNING) likely seizure inducing.
22 //  - D: cycles the divisor for the swapchain size.
23 //  - P: switches present modes.
24 //
25 // Closing all the windows exits the example. ^C also works.
26 //
27 // Things to test manually:
28 //
29 //  - Basic tests (with the triangle render mode):
30 //    - Check the triangle is red on a black background and with the pointy side up.
31 //    - Cycle render modes a bunch and check that the triangle background is always solid black.
32 //    - Check that rendering triangles to multiple windows works.
33 //
34 //  - Present mode single-window tests (with cycling color render mode):
35 //    - Check that Fifo cycles at about 1 cycle per second and has no tearing.
36 //    - Check that Mailbox cycles faster than Fifo and has no tearing.
37 //    - Check that Immediate cycles faster than Fifo, it is allowed to have tearing. (dragging
38 //      between two monitors can help see tearing)
39 //
40 //  - Present mode multi-window tests, it should have the same results as single-window tests when
41 //    all windows are in the same present mode. In mixed present modes only Immediate windows are
42 //    allowed to tear.
43 //
44 //  - Resizing tests (with the triangle render mode):
45 //    - Check that cycling divisors on the triangle produces lower and lower resolution triangles.
46 //    - Check latching the swapchain config and resizing the window a bunch (smaller, bigger, and
47 //      diagonal aspect ratio).
48 //
49 //  - Config change tests:
50 //    - Check that cycling between present modes works.
51 //    - TODO can't be tested yet: check cycling the same window over multiple devices.
52 //    - TODO can't be tested yet: check cycling the same window over multiple formats.
53 
54 #include "common/Assert.h"
55 #include "common/Log.h"
56 #include "utils/ComboRenderPipelineDescriptor.h"
57 #include "utils/GLFWUtils.h"
58 #include "utils/WGPUHelpers.h"
59 
60 #include <dawn/dawn_proc.h>
61 #include <dawn/webgpu_cpp.h>
62 #include <dawn_native/DawnNative.h>
63 #include "GLFW/glfw3.h"
64 
65 #include <memory>
66 #include <unordered_map>
67 
68 struct WindowData {
69     GLFWwindow* window = nullptr;
70     uint64_t serial = 0;
71 
72     float clearCycle = 1.0f;
73     bool latched = false;
74     bool renderTriangle = true;
75     uint32_t divisor = 1;
76 
77     wgpu::Surface surface = nullptr;
78     wgpu::SwapChain swapchain = nullptr;
79 
80     wgpu::SwapChainDescriptor currentDesc;
81     wgpu::SwapChainDescriptor targetDesc;
82 };
83 
84 static std::unordered_map<GLFWwindow*, std::unique_ptr<WindowData>> windows;
85 static uint64_t windowSerial = 0;
86 
87 static std::unique_ptr<dawn_native::Instance> instance;
88 static wgpu::Device device;
89 static wgpu::Queue queue;
90 static wgpu::RenderPipeline trianglePipeline;
91 
IsSameDescriptor(const wgpu::SwapChainDescriptor & a,const wgpu::SwapChainDescriptor & b)92 bool IsSameDescriptor(const wgpu::SwapChainDescriptor& a, const wgpu::SwapChainDescriptor& b) {
93     return a.usage == b.usage && a.format == b.format && a.width == b.width &&
94            a.height == b.height && a.presentMode == b.presentMode;
95 }
96 
97 void OnKeyPress(GLFWwindow* window, int key, int, int action, int);
98 
SyncFromWindow(WindowData * data)99 void SyncFromWindow(WindowData* data) {
100     int width;
101     int height;
102     glfwGetFramebufferSize(data->window, &width, &height);
103 
104     data->targetDesc.width = std::max(1u, width / data->divisor);
105     data->targetDesc.height = std::max(1u, height / data->divisor);
106 }
107 
AddWindow()108 void AddWindow() {
109     glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
110     GLFWwindow* window = glfwCreateWindow(400, 400, "", nullptr, nullptr);
111     glfwSetKeyCallback(window, OnKeyPress);
112 
113     wgpu::SwapChainDescriptor descriptor;
114     descriptor.usage = wgpu::TextureUsage::RenderAttachment;
115     descriptor.format = wgpu::TextureFormat::BGRA8Unorm;
116     descriptor.width = 0;
117     descriptor.height = 0;
118     descriptor.presentMode = wgpu::PresentMode::Fifo;
119 
120     std::unique_ptr<WindowData> data = std::make_unique<WindowData>();
121     data->window = window;
122     data->serial = windowSerial++;
123     data->surface = utils::CreateSurfaceForWindow(instance->Get(), window);
124     data->currentDesc = descriptor;
125     data->targetDesc = descriptor;
126     SyncFromWindow(data.get());
127 
128     windows[window] = std::move(data);
129 }
130 
DoRender(WindowData * data)131 void DoRender(WindowData* data) {
132     wgpu::TextureView view = data->swapchain.GetCurrentTextureView();
133     wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
134 
135     if (data->renderTriangle) {
136         utils::ComboRenderPassDescriptor desc({view});
137         // Use Load to check the swapchain is lazy cleared (we shouldn't see garbage from previous
138         // frames).
139         desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
140 
141         wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
142         pass.SetPipeline(trianglePipeline);
143         pass.Draw(3);
144         pass.EndPass();
145     } else {
146         data->clearCycle -= 1.0 / 60.f;
147         if (data->clearCycle < 0.0) {
148             data->clearCycle = 1.0f;
149         }
150 
151         utils::ComboRenderPassDescriptor desc({view});
152         desc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
153         desc.cColorAttachments[0].clearColor = {data->clearCycle, 1.0f - data->clearCycle, 0.0f,
154                                                 1.0f};
155 
156         wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc);
157         pass.EndPass();
158     }
159 
160     wgpu::CommandBuffer commands = encoder.Finish();
161     queue.Submit(1, &commands);
162 
163     data->swapchain.Present();
164 }
165 
operator <<(std::ostream & o,const wgpu::SwapChainDescriptor & desc)166 std::ostream& operator<<(std::ostream& o, const wgpu::SwapChainDescriptor& desc) {
167     // For now only output attachment is possible.
168     ASSERT(desc.usage == wgpu::TextureUsage::RenderAttachment);
169     o << "RenderAttachment ";
170     o << desc.width << "x" << desc.height << " ";
171 
172     // For now only BGRA is allowed
173     ASSERT(desc.format == wgpu::TextureFormat::BGRA8Unorm);
174     o << "BGRA8Unorm ";
175 
176     switch (desc.presentMode) {
177         case wgpu::PresentMode::Immediate:
178             o << "Immediate";
179             break;
180         case wgpu::PresentMode::Fifo:
181             o << "Fifo";
182             break;
183         case wgpu::PresentMode::Mailbox:
184             o << "Mailbox";
185             break;
186     }
187     return o;
188 }
189 
UpdateTitle(WindowData * data)190 void UpdateTitle(WindowData* data) {
191     std::ostringstream o;
192 
193     o << data->serial << " ";
194     if (data->divisor != 1) {
195         o << "Divisor:" << data->divisor << " ";
196     }
197 
198     if (data->latched) {
199         o << "Latched: (" << data->currentDesc << ") ";
200         o << "Target: (" << data->targetDesc << ")";
201     } else {
202         o << "(" << data->currentDesc << ")";
203     }
204 
205     glfwSetWindowTitle(data->window, o.str().c_str());
206 }
207 
OnKeyPress(GLFWwindow * window,int key,int,int action,int)208 void OnKeyPress(GLFWwindow* window, int key, int, int action, int) {
209     if (action != GLFW_PRESS) {
210         return;
211     }
212 
213     ASSERT(windows.count(window) == 1);
214 
215     WindowData* data = windows[window].get();
216     switch (key) {
217         case GLFW_KEY_W:
218             AddWindow();
219             break;
220 
221         case GLFW_KEY_L:
222             data->latched = !data->latched;
223             UpdateTitle(data);
224             break;
225 
226         case GLFW_KEY_R:
227             data->renderTriangle = !data->renderTriangle;
228             UpdateTitle(data);
229             break;
230 
231         case GLFW_KEY_D:
232             data->divisor *= 2;
233             if (data->divisor > 32) {
234                 data->divisor = 1;
235             }
236             break;
237 
238         case GLFW_KEY_P:
239             switch (data->targetDesc.presentMode) {
240                 case wgpu::PresentMode::Immediate:
241                     data->targetDesc.presentMode = wgpu::PresentMode::Fifo;
242                     break;
243                 case wgpu::PresentMode::Fifo:
244                     data->targetDesc.presentMode = wgpu::PresentMode::Mailbox;
245                     break;
246                 case wgpu::PresentMode::Mailbox:
247                     data->targetDesc.presentMode = wgpu::PresentMode::Immediate;
248                     break;
249             }
250             break;
251 
252         default:
253             break;
254     }
255 }
256 
main(int argc,const char * argv[])257 int main(int argc, const char* argv[]) {
258     // Setup GLFW
259     glfwSetErrorCallback([](int code, const char* message) {
260         dawn::ErrorLog() << "GLFW error " << code << " " << message;
261     });
262     if (!glfwInit()) {
263         return 1;
264     }
265 
266     // Choose an adapter we like.
267     // TODO: allow switching the window between devices.
268     DawnProcTable procs = dawn_native::GetProcs();
269     dawnProcSetProcs(&procs);
270 
271     instance = std::make_unique<dawn_native::Instance>();
272     instance->DiscoverDefaultAdapters();
273 
274     std::vector<dawn_native::Adapter> adapters = instance->GetAdapters();
275     dawn_native::Adapter chosenAdapter;
276     for (dawn_native::Adapter& adapter : adapters) {
277         wgpu::AdapterProperties properties;
278         adapter.GetProperties(&properties);
279         if (properties.backendType != wgpu::BackendType::Null) {
280             chosenAdapter = adapter;
281             break;
282         }
283     }
284     ASSERT(chosenAdapter);
285 
286     // Setup the device on that adapter.
287     device = wgpu::Device::Acquire(chosenAdapter.CreateDevice());
288     device.SetUncapturedErrorCallback(
289         [](WGPUErrorType errorType, const char* message, void*) {
290             const char* errorTypeName = "";
291             switch (errorType) {
292                 case WGPUErrorType_Validation:
293                     errorTypeName = "Validation";
294                     break;
295                 case WGPUErrorType_OutOfMemory:
296                     errorTypeName = "Out of memory";
297                     break;
298                 case WGPUErrorType_Unknown:
299                     errorTypeName = "Unknown";
300                     break;
301                 case WGPUErrorType_DeviceLost:
302                     errorTypeName = "Device lost";
303                     break;
304                 default:
305                     UNREACHABLE();
306                     return;
307             }
308             dawn::ErrorLog() << errorTypeName << " error: " << message;
309         },
310         nullptr);
311     queue = device.GetDefaultQueue();
312 
313     // The hacky pipeline to render a triangle.
314     utils::ComboRenderPipelineDescriptor pipelineDesc(device);
315     pipelineDesc.vertexStage.module =
316         utils::CreateShaderModule(device, utils::SingleShaderStage::Vertex, R"(
317         #version 450
318         const vec2 pos[3] = vec2[3](vec2(0.0f, 0.5f), vec2(-0.5f, -0.5f), vec2(0.5f, -0.5f));
319         void main() {
320            gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
321         })");
322     pipelineDesc.cFragmentStage.module =
323         utils::CreateShaderModule(device, utils::SingleShaderStage::Fragment, R"(
324         #version 450
325         layout(location = 0) out vec4 fragColor;
326         void main() {
327            fragColor = vec4(1.0, 0.0, 0.0, 1.0);
328         })");
329     pipelineDesc.colorStateCount = 1;
330     // BGRA shouldn't be hardcoded. Consider having a map[format -> pipeline].
331     pipelineDesc.cColorStates[0].format = wgpu::TextureFormat::BGRA8Unorm;
332     trianglePipeline = device.CreateRenderPipeline(&pipelineDesc);
333 
334     // Craete the first window, since the example exits when there are no windows.
335     AddWindow();
336 
337     while (windows.size() != 0) {
338         glfwPollEvents();
339 
340         for (auto it = windows.begin(); it != windows.end();) {
341             GLFWwindow* window = it->first;
342 
343             if (glfwWindowShouldClose(window)) {
344                 glfwDestroyWindow(window);
345                 it = windows.erase(it);
346             } else {
347                 it++;
348             }
349         }
350 
351         for (auto& it : windows) {
352             WindowData* data = it.second.get();
353 
354             SyncFromWindow(data);
355             if (!IsSameDescriptor(data->currentDesc, data->targetDesc) && !data->latched) {
356                 data->swapchain = device.CreateSwapChain(data->surface, &data->targetDesc);
357                 data->currentDesc = data->targetDesc;
358             }
359             UpdateTitle(data);
360             DoRender(data);
361         }
362     }
363 }
364