blob: c9ef652edf83d150441cbf1dcb5cf296e1319a0a [file] [log] [blame] [edit]
// 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);
}
}
}