Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
292 lines (252 sloc)
10.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Copyright (c) 2019 Hans-Kristian Arntzen | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining | |
| * a copy of this software and associated documentation files (the | |
| * "Software"), to deal in the Software without restriction, including | |
| * without limitation the rights to use, copy, modify, merge, publish, | |
| * distribute, sublicense, and/or sell copies of the Software, and to | |
| * permit persons to whom the Software is furnished to do so, subject to | |
| * the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be | |
| * included in all copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
| * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
| * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
| * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
| * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| */ | |
| #include "vulkan.hpp" | |
| #include "device.hpp" | |
| #include "wsi.hpp" | |
| #include "util.hpp" | |
| #include <SDL2/SDL.h> | |
| #include <SDL2/SDL_vulkan.h> | |
| // See sample 06 for details. | |
| struct SDL2Platform : Vulkan::WSIPlatform | |
| { | |
| explicit SDL2Platform(SDL_Window *window_) | |
| : window(window_) | |
| { | |
| } | |
| VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice) override | |
| { | |
| VkSurfaceKHR surface; | |
| if (SDL_Vulkan_CreateSurface(window, instance, &surface)) | |
| return surface; | |
| else | |
| return VK_NULL_HANDLE; | |
| } | |
| std::vector<const char *> get_instance_extensions() override | |
| { | |
| unsigned instance_ext_count = 0; | |
| SDL_Vulkan_GetInstanceExtensions(window, &instance_ext_count, nullptr); | |
| std::vector<const char *> instance_names(instance_ext_count); | |
| SDL_Vulkan_GetInstanceExtensions(window, &instance_ext_count, instance_names.data()); | |
| return instance_names; | |
| } | |
| uint32_t get_surface_width() override | |
| { | |
| int w, h; | |
| SDL_Vulkan_GetDrawableSize(window, &w, &h); | |
| return w; | |
| } | |
| uint32_t get_surface_height() override | |
| { | |
| int w, h; | |
| SDL_Vulkan_GetDrawableSize(window, &w, &h); | |
| return h; | |
| } | |
| bool alive(Vulkan::WSI &) override | |
| { | |
| return is_alive; | |
| } | |
| void poll_input() override | |
| { | |
| SDL_Event e; | |
| while (SDL_PollEvent(&e)) | |
| { | |
| switch (e.type) | |
| { | |
| case SDL_QUIT: | |
| is_alive = false; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| SDL_Window *window; | |
| bool is_alive = true; | |
| }; | |
| static const uint32_t gbuffer_vert[] = | |
| #include "shaders/gbuffer.vert.inc" | |
| ; | |
| static const uint32_t gbuffer_frag[] = | |
| #include "shaders/gbuffer.frag.inc" | |
| ; | |
| static const uint32_t lighting_vert[] = | |
| #include "shaders/lighting.vert.inc" | |
| ; | |
| static const uint32_t lighting_frag[] = | |
| #include "shaders/lighting.frag.inc" | |
| ; | |
| static bool run_application(SDL_Window *window) | |
| { | |
| // Copy-pastaed from sample 06. | |
| SDL2Platform platform(window); | |
| Vulkan::WSI wsi; | |
| wsi.set_platform(&platform); | |
| wsi.set_backbuffer_srgb(true); // Always choose SRGB backbuffer formats over UNORM. Can be toggled in run-time. | |
| if (!wsi.init(1 /*num_thread_indices*/)) | |
| return false; | |
| Vulkan::Device &device = wsi.get_device(); | |
| Vulkan::Program *gbuffer_prog = device.request_program( | |
| device.request_shader(gbuffer_vert, sizeof(gbuffer_vert)), | |
| device.request_shader(gbuffer_frag, sizeof(gbuffer_frag))); | |
| Vulkan::Program *lighting_prog = device.request_program( | |
| device.request_shader(lighting_vert, sizeof(lighting_vert)), | |
| device.request_shader(lighting_frag, sizeof(lighting_frag))); | |
| while (platform.is_alive) | |
| { | |
| wsi.begin_frame(); | |
| auto cmd = device.request_command_buffer(); | |
| // Here we're exploring the deep support for render passes in Granite. | |
| // Almost all older engines completely neglect this aspect because render passes did not exist in older APIs. | |
| // Re-architecting an entire API abstraction to support render passes is no small task. | |
| // Here we have a toy deferred renderer expressed with Vulkan multipass. | |
| // This setup is very explicit and it's something we need to take advantage of tile-based renderers. | |
| // The gain from doing it like this is dubious on desktop with current architectures, | |
| // but getting optimal performance on all architectures is a huge win. | |
| // Granite is quite low-level here, but there are some huge convenience points we cannot overlook: | |
| // - VkSubpassDependencies are set up automatically by analyzing the use of resources. | |
| // - Appropriate image layouts are set automatically based on resource use. | |
| // - VkAttachmentReferences are set automatically. | |
| // It is up to user to respect these layouts as initialLayout and finalLayout are deduced from how attachments are used, | |
| // but here we are using just WSI and transient images. These images are synchronized automatically with | |
| // VK_SUBPASS_EXTERNAL because initialLayout is UNDEFINED for all these attachments. | |
| // - VkRenderPass handles are created automatically based on ... you guessed it, hashing. | |
| Vulkan::RenderPassInfo rp; | |
| rp.num_color_attachments = 3; | |
| rp.color_attachments[0] = &device.get_swapchain_view(); | |
| // If we can throw away the resources, we can just request on-demand transient images. | |
| // On tile-based, we don't need to spend physical memory for these attachments. | |
| // Transient attachments are naturally throw-away and reuseable, | |
| // so I felt it made sense to do synchronization automatically for these resources. | |
| // Fortunately, we can trivially do this using EXTERNAL subpass dependencies, so there is | |
| // zero tracking required to implement this. | |
| // This is probably the only case where I find EXTERNAL subpass dependencies useful ... | |
| // Transient images are kept around and are deallocated if not used again in a few frames. | |
| // You can create these images yourself as well using the ImageDomain::Transient mode. | |
| // This automatic synchronization is theoretically inoptimal on the GPU, since we might emit | |
| // redundant barriers. There might be room to enable "explicit sync" for transient attachments as well, | |
| // particularly in the render graph. | |
| rp.color_attachments[1] = &device.get_transient_attachment( | |
| device.get_swapchain_view().get_image().get_width(), | |
| device.get_swapchain_view().get_image().get_height(), | |
| VK_FORMAT_R8G8B8A8_UNORM, | |
| 0); | |
| rp.color_attachments[2] = &device.get_transient_attachment( | |
| device.get_swapchain_view().get_image().get_width(), | |
| device.get_swapchain_view().get_image().get_height(), | |
| VK_FORMAT_R8G8B8A8_UNORM, | |
| 1); | |
| // Depth format support varies across devices, so there's a generic "default depth" or depth-stencil format | |
| // which is either D24 or D32F depending on hardware. | |
| rp.depth_stencil = &device.get_transient_attachment( | |
| device.get_swapchain_view().get_image().get_width(), | |
| device.get_swapchain_view().get_image().get_height(), | |
| device.get_default_depth_format()); | |
| // Explicit store, load and clear, the way it should be. | |
| // This is also very important for tile-based GPUs. | |
| // It can have an effect on desktop as well I've found, particularly depth buffers. | |
| // No flags set for an attachment will map to DONT_CARE. | |
| rp.store_attachments = 1 << 0; | |
| rp.clear_attachments = (1 << 0) | (1 << 1) | (1 << 2); | |
| rp.op_flags = Vulkan::RENDER_PASS_OP_CLEAR_DEPTH_STENCIL_BIT; | |
| // We don't *NEED* to provide this. If we don't we get one subpass which is setup in an obvious way based on | |
| // the RenderPassInfo. | |
| Vulkan::RenderPassInfo::Subpass subpasses[2]; | |
| rp.num_subpasses = 2; | |
| rp.subpasses = subpasses; | |
| // Fiddle with clear depth. | |
| rp.clear_depth_stencil.depth = 1.0f; | |
| // Pretend attachment 1 and 2 represent our "gbuffer". | |
| subpasses[0].num_color_attachments = 2; | |
| subpasses[0].color_attachments[0] = 1; | |
| subpasses[0].color_attachments[1] = 2; | |
| // We can control which layout the depth buffer is in. | |
| subpasses[0].depth_stencil_mode = Vulkan::RenderPassInfo::DepthStencil::ReadWrite; | |
| // For the second subpass, we're going to do a fake "lighting" pass. | |
| // We pull in the two color attachments from last subpass as input attachments. | |
| // We can also pull in the depth buffer as an input attachment. | |
| // It is also possible to have a "feedback" where attachments are used as both input attachments and color/depth. | |
| // This triggers GENERAL image layouts and access must be manually synchronized with CommandBuffer::pixel_barrier(). | |
| subpasses[1].num_color_attachments = 1; | |
| subpasses[1].color_attachments[0] = 0; | |
| subpasses[1].num_input_attachments = 3; | |
| subpasses[1].input_attachments[0] = 1; | |
| subpasses[1].input_attachments[1] = 2; | |
| subpasses[1].input_attachments[2] = 3; // Depth attachment is index rp.num_color_attachments. | |
| // ReadOnly depth, so we can use DEPTH_STENCIL_READ_ONLY_OPTIMAL (depth read-only + input attachment layout). | |
| subpasses[1].depth_stencil_mode = Vulkan::RenderPassInfo::DepthStencil::ReadOnly; | |
| // Here we lazily create a VkRenderPass as well as a "compatible" VkRenderPass (used by VkFramebuffer and VkPipeline). | |
| // Granite supplies the attachments inline as we see here, but in Vulkan we need to create VkFramebuffer objects. | |
| // Ideally, Vulkan would not require this object ... | |
| // These framebuffers are also created on-demand and destroyed if not used in a few frames. | |
| // We use the temporary hashmap data structure here as well, similar to descriptor set management. | |
| cmd->begin_render_pass(rp); | |
| { | |
| cmd->set_opaque_state(); | |
| cmd->set_program(gbuffer_prog); | |
| // Fill the two gbuffers with red and blue color, see shaders/gbuffer.frag. | |
| // The vertex shader generates a quad, so no VBO needed. | |
| cmd->draw(3); | |
| } | |
| cmd->next_subpass(); | |
| { | |
| cmd->set_opaque_state(); | |
| cmd->set_program(lighting_prog); | |
| // Need to turn off depth writes, but keeps test enabled. | |
| // This is what a typical deferred renderer would do. | |
| cmd->set_depth_test(true, false); | |
| // Pulls out the input attachment views from the frame buffer and binds them to (0, 0), (0, 1) and (0, 2). | |
| cmd->set_input_attachments(0, 0); | |
| // This shaders adds the two gbuffer attachments together and multiplies with the depth, | |
| // which gives a dark magenta color. | |
| cmd->draw(3); | |
| } | |
| cmd->end_render_pass(); | |
| device.submit(cmd); | |
| wsi.end_frame(); | |
| } | |
| return true; | |
| } | |
| int main() | |
| { | |
| // Copy-pastaed from sample 06. | |
| SDL_Window *window = SDL_CreateWindow("08-render-passes", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, | |
| 640, 360, | |
| SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); | |
| if (!window) | |
| { | |
| LOGE("Failed to create SDL window!\n"); | |
| return 1; | |
| } | |
| // Init loader with GetProcAddr directly from SDL2 rather than letting Granite load the Vulkan loader. | |
| if (!Vulkan::Context::init_loader((PFN_vkGetInstanceProcAddr) SDL_Vulkan_GetVkGetInstanceProcAddr())) | |
| { | |
| LOGE("Failed to create loader!\n"); | |
| return 1; | |
| } | |
| if (!run_application(window)) | |
| { | |
| LOGE("Failed to run application.\n"); | |
| return 1; | |
| } | |
| SDL_DestroyWindow(window); | |
| SDL_Vulkan_UnloadLibrary(); | |
| } |