diff --git a/demos/13-skybox/CMakeLists.txt b/demos/13-skybox/CMakeLists.txt index 6a44aa1..fb440e4 100644 --- a/demos/13-skybox/CMakeLists.txt +++ b/demos/13-skybox/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.27) +cmake_minimum_required(VERSION 4.0) project(skybox CXX) build_application( @@ -26,5 +26,6 @@ target_sources(${PROJECT_NAME} PUBLIC FILE_SET CXX_MODULES TYPE CXX_MODULES FILES - environment_map.cppm + hdri_environment.cppm + skybox.cppm ) \ No newline at end of file diff --git a/demos/13-skybox/README.md b/demos/13-skybox/README.md new file mode 100644 index 0000000..64edf81 --- /dev/null +++ b/demos/13-skybox/README.md @@ -0,0 +1,194 @@ +## Demo 13 Skybox + +In demo 13, shows how to configure with the vulkan-cpp to get a skybox to work with the most minimal requirements. + +I am going to be using stbi_image library to help with loading the three faces. + +Note: There are two files in demo 13, one called `skybox.cppm` and another called `hdri_environment.cppm`. The `skybox.cppm` file is what this README will walk you through. + +If you are interested in learning how to load an HDRI environment specifically, checkout `hdri_environment.cppm` as the setup is quite similar. Only difference is you would be loading a single image file and changing from `stbi_load` to `stbi_loadf` instead. + +## Loading 6 faces for cubemap + +To load the skybox, I have provided a skybox directory inside of the assets directory. + +In `asset_samples/skybox`, you should see 6 `.jpg` images. These are going to be the images we use. + +Before configuring any vulkan handles, make sure to store the path correctly to those images. As shown below: + +```C++ +std::array faces = { + "asset_samples/skybox/right.jpg", "asset_samples/skybox/left.jpg", + "asset_samples/skybox/top.jpg", "asset_samples/skybox/bottom.jpg", + "asset_samples/skybox/front.jpg", "asset_samples/skybox/back.jpg" +}; +``` + +To load all 6 of those faces, use stbi_image to load those images. The way, how I have it implemented is by doing the following: + +- First we validate that the images specified are 6 images only. +- We load in the first image to specify the parameters for the dimensions, and the total size in bytes of that image (as they should all be the equivalent). +- Starting at i=1, we load the other 5 images into an array, being `std::array, 6>` + +```C++ +if (faces.size() != 6) { + std::println("Cubemap requires 6 faces, received {} count of faces", + faces.size()); + return; +} + +int w = 0; +int h = 0; +int channels = 0; +std::array, 6> faces{}; + +auto* face0 = + stbi_load(faces[0].c_str(), &w, &h, &channels, STBI_rgb_alpha); +int face_width = w; +int face_height = h; + +VkFormat image_format = VK_FORMAT_R8G8B8A8_SRGB; +const uint32_t bytes_per_pixel = + static_cast(vk::bytes_per_texture_format(image_format)); +auto size_bytes = face_width * face_height * bytes_per_pixel; + +faces[0] = to_bytes(face0, size_bytes); + +for (size_t i = 1; i < faces.size(); i++) { + auto* face_pixels = + stbi_load(faces[i].c_str(), &w, &h, &channels, STBI_rgb_alpha); + faces[i] = to_bytes(face_pixels, size_bytes); + + if (faces[i].empty()) { + std::println("Could not load face: {}", faces[i]); + return; + } + + if (w != face_width || h != face_height) { + std::println("Cubemap faces must match dimensions. Face 0 is " + "{}x{}, face {} is {}x{} ({})", + face_width, + face_height, + i, + w, + h, + p_faces[i]); + return; + } +} +``` + +## Configure `vk::sample_image` + +By now you should be aware that `vk::sample_image`, is essentially a wrapper around the creation for the `VkImage`, `VkImageView`, and `VkSampler`. + +For this skybox demo, these are the parameters to configure those handles with: + +```C++ +vk::image_params skybox_params = { + .extent = { .width = width, .height = height, .depth = 1 }, + .format = image_format, + .memory_mask = p_physical.memory_properties( + vk::memory_property::device_local_bit), + .usage = + vk::image_usage::transfer_dst_bit | vk::image_usage::sampled_bit, + .image_flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, + .view_type = VK_IMAGE_VIEW_TYPE_CUBE, + .layer_count = 6, + .array_layers = 6, +}; +m_skybox_image = vk::sample_image(m_device, skybox_params); +``` + +- `extent` is to specify the width and height for the given image. Since this is a skybox, the width and height should relatively be the same. +- `format` is set to `VK_FORMAT_R8G8B8A8_SRGB` +- `p_physical.memory_properties` is specified to be `device_local_bit` for allocating this image in high-speed, VRAM-only memory local to the GPU. +- `usage` specifies with `image_usage::transfer_dst_bit` because the demo copies raw pixels from staging into this image, we also bitwise OR `image_usage::sampled_bit` so the fragment shader can read it using `sampler2D`. +- `image_flags` is configured with `VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT` because we are creating this image to store 6 regions for a skybox and this tells the image for what kind of data to expect. +- `view_type` configures `VK_IMAGE_VIEW_TYPE_CUBE` to tell the image not to treat the data as a flat texture array. +- `layer_count` specifies that the image will have 6 distinct image layers +- `array_layers` specifies how many texture slices are allocated to VRAM. + +## Configuring Staging Buffer + +We need to create a staging buffer to allow for CPU-visible data to be transferred to via `host_visible` without manually cashe flushing (host_coherent). Populating this buffer with the skybox pixels we loaded, earlier. + +Using this buffer as an entry point using the `transfer_src_bit` to do a fast data transfer onto the device-local skybox texture. + +```C++ +vk::buffer_parameters staging_params = { + .memory_mask = p_physical.memory_properties( + vk::memory_property::host_visible_bit | + vk::memory_property::host_coherent_bit), + .usage = vk::buffer_usage::transfer_src_bit, +}; + +vk::buffer staging(m_device, total_size_bytes, staging_params); +``` + +- `host_visible_bit | host_coherent_bit` is to allocate the buffer to system RAM or a special region of VRAM the CPU can directly write and automatically become visible to the GPU. +- `vk::buffer_usage::transfer_src_bit` is to tell the use of this buffer handle is specifically to store data, which allows the driver to optimize underlying memory allocations specifically for outbound direct memory transfers to the GPU memory. + +## Transferring the faces pixels data + +Then to perform the actual transfer operation for the pixels for the 6 faces. You just call the following API: + +```C++ +// faces is defined as std::array, 6> +staging.transfer(faces); +``` + +## Cleanup stbi_image pixels + +After transferring the pixels and already written, you can just simply perform cleanup: + +```C++ +for (size_t i = 0; i < faces.size(); i++) { + stbi_image_free(faces[i].data()); +} +``` + +## Mapping each Skybox Face Regions with Vulkan + +The `std::array` allows for executing batch of offset stride copy operations. For each region (per-face), we tightly pack a single cubemap face to its source memory. + +That source memory to an un-offset subresource footprint that targets different base array layers and other subresource parameters. + +Allowing for those 6-face data streams to be represented as a linear buffer stream that optimally laid out for the device. + + +## Submitting to the Queue + +Performing this queue submission is to conclude by actually having the data to be fully copied over to the GPU. + +```C++ +VkQueue graphics_queue = nullptr; +vkGetDeviceQueue(m_device, 0, 0, &graphics_queue); +const VkCommandBuffer cmd = upload_cmd; +VkSubmitInfo submit = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .pWaitDstStageMask = nullptr, + .commandBufferCount = 1, + .pCommandBuffers = &cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, +}; +vk::vk_check(vkQueueSubmit(graphics_queue, 1, &submit, nullptr), + "vkQueueSubmit(cubemap upload)"); +vk::vk_check(vkQueueWaitIdle(graphics_queue), + "vkQueueWaitIdle(cubemap upload)"); + +upload_cmd.destruct(); +staging.destruct(); +``` + +## Next Steps + +The `skybox.cppm` implementation actually just recreates a separate `vk::pipeline`, `vk::shader_resource`, `vk::descriptor_resource`, and `vk::uniform_buffer` for the skybox shaders. + +If you have been following along in the vulkan-cpp demos where we use `vk::pipeline` (graphics pipeline) API, `skybox.cppm` follows the same way we have always been configuring and initializing those handles. + +This README is meant to give you an idea on the core portions of the code for loading and performing staging correctly with a skybox. Since this demo, has quite a significant amount of code involved. \ No newline at end of file diff --git a/demos/13-skybox/application.cpp b/demos/13-skybox/application.cpp index 1e37459..66e2016 100644 --- a/demos/13-skybox/application.cpp +++ b/demos/13-skybox/application.cpp @@ -30,7 +30,8 @@ #endif import vk; -import environment_map; +// import environment_map; +import skybox; static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( @@ -298,16 +299,32 @@ main() { // gets set with the renderpass std::array color = { 0.f, 0.5f, 0.5f, 1.f }; - environment_map skybox = environment_map( - logical_device, - std::filesystem::path("asset_samples/skybox/monkstown_castle_4k.hdr"), - physical_device, - main_renderpass); + // environment_map skybox = environment_map( + // logical_device, + // std::filesystem::path("asset_samples/skybox/monkstown_castle_4k.hdr"), + // physical_device, + // main_renderpass); + + std::array faces = { + "asset_samples/skybox/right.jpg", "asset_samples/skybox/left.jpg", + "asset_samples/skybox/top.jpg", "asset_samples/skybox/bottom.jpg", + "asset_samples/skybox/front.jpg", "asset_samples/skybox/back.jpg" + }; + skybox_environment skybox = skybox_environment( + logical_device, physical_device, faces, main_renderpass); float field_of_view = 45.f; glm::vec3 position = { 3.5f, 4.90f, 36.40f }; + + glm::vec3 rotation = glm::vec3(0.f); + glm::highp_vec4 quaternion{ 0.f, 0.f, 0.f, 1.f }; + glm::vec3 scale{ 1.f }; glm::vec2 plane = { 0.1f, 5000.f }; + glm::vec2 last_cursor_pos{}; + float yaw = 0.f; + float pitch = 0.f; + bool is_first_frame = true; while (!glfwWindowShouldClose(window)) { glfwPollEvents(); @@ -326,9 +343,6 @@ main() { }; main_renderpass.begin(current, begin_renderpass); - // Binding a graphics pipeline -- before drawing stuff - // Inside of this graphics pipeline bind, is where you want to do the - // drawing stuff to static auto start_time = std::chrono::high_resolution_clock::now(); auto current_time = std::chrono::high_resolution_clock::now(); @@ -336,19 +350,70 @@ main() { current_time - start_time) .count(); + glm::vec3 up = glm::vec3(0.f, 1.f, 0.f); + glm::vec3 down = glm::vec3(0.f, -1.f, 0.f); + glm::vec3 right = glm::vec3(1.f, 0.f, 0.f); + glm::vec3 left = glm::vec3(-1.f, 0.f, 0.f); + glm::vec3 forward = glm::vec3(0.f, 0.f, 1.f); + glm::vec3 backward = glm::vec3(0.f, 0.f, -1.f); + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { - position.z += 1.f; - } - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { - position.x += 1.f; + position += forward; } + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { - position.z -= 1.f; + position += backward; } + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { - position.x -= 1.f; + position -= right; + } + + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { + position -= right; + } + + double x_pos, y_pos; + glfwGetCursorPos(window, &x_pos, &y_pos); + + glm::vec2 current_cursor_pos = { x_pos, y_pos }; + + if (is_first_frame) { + last_cursor_pos = current_cursor_pos; + is_first_frame = false; } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == + GLFW_PRESS) { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + + float mouse_sensitivity = 0.01; + glm::vec2 cursor_dt = current_cursor_pos - last_cursor_pos; + + yaw -= (cursor_dt.x * mouse_sensitivity); + pitch -= (cursor_dt.y * mouse_sensitivity); + + // set rotation + rotation = glm::vec3(yaw, pitch, 0.f); + + auto quat = glm::quat(rotation); + quaternion = glm::vec4({ quat.x, quat.y, quat.z, quat.w }); + } + + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == + GLFW_RELEASE) { + is_first_frame = true; + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + + last_cursor_pos = current_cursor_pos; + + glm::quat quat = glm::quat({ + quaternion.w, + quaternion.x, + quaternion.y, + quaternion.z, + }); global_uniform ubo = {}; ubo.proj = glm::mat4(1.f); ubo.proj = glm::perspective( @@ -359,7 +424,8 @@ main() { ubo.proj[1][1] *= -1; ubo.view = glm::mat4(1.f); - ubo.view = glm::translate(ubo.view, position); + ubo.view = glm::translate(ubo.view, position) * glm::mat4_cast(quat); + ubo.view = glm::inverse(ubo.view); skybox_uniform sky_ubo = { diff --git a/demos/13-skybox/environment_map.cppm b/demos/13-skybox/hdri_environment.cppm similarity index 99% rename from demos/13-skybox/environment_map.cppm rename to demos/13-skybox/hdri_environment.cppm index 4ce050d..cb9b64e 100644 --- a/demos/13-skybox/environment_map.cppm +++ b/demos/13-skybox/hdri_environment.cppm @@ -29,7 +29,7 @@ module; #include #include -export module environment_map; +export module hdri_environment; import vk; export struct skybox_uniform { diff --git a/demos/13-skybox/skybox.cppm b/demos/13-skybox/skybox.cppm new file mode 100644 index 0000000..8338a18 --- /dev/null +++ b/demos/13-skybox/skybox.cppm @@ -0,0 +1,669 @@ +module; + +#include +#include +#include +#include +#include + +#include + +#define GLM_FORCE_RADIANS +#include +#include +#define GLM_ENABLE_EXPERIMENTAL +#include + +export module skybox; + +import vk; + +export struct skybox_uniform { + glm::mat4 proj_view; +}; + +// Converts raw stbi image stbi_uc* raw pixels pointers to a span +std::span +to_bytes(stbi_uc* p_pixels, uint32_t p_image_size) { + return std::span(reinterpret_cast(p_pixels), + p_image_size); +} + +export class skybox_environment { +public: + skybox_environment(const VkDevice& p_device, + const vk::physical_device& p_physical, + std::span p_faces, + const VkRenderPass& p_renderpass) + : m_device(p_device) { + m_physical = p_physical; + m_renderpass = p_renderpass; + + if (p_faces.size() != 6) { + std::println("Cubemap requires 6 faces, received {} count of faces", + p_faces.size()); + return; + } + + // Loading in all 6-faces images for the skybox + int w = 0; + int h = 0; + int channels = 0; + std::array, 6> faces{}; + + auto* face0 = + stbi_load(p_faces[0].c_str(), &w, &h, &channels, STBI_rgb_alpha); + int face_width = w; + int face_height = h; + + VkFormat image_format = VK_FORMAT_R8G8B8A8_SRGB; + const uint32_t bytes_per_pixel = + static_cast(vk::bytes_per_texture_format(image_format)); + auto size_bytes = face_width * face_height * bytes_per_pixel; + + faces[0] = to_bytes(face0, size_bytes); + + for (size_t i = 1; i < faces.size(); i++) { + auto* face_pixels = + stbi_load(p_faces[i].c_str(), &w, &h, &channels, STBI_rgb_alpha); + faces[i] = to_bytes(face_pixels, size_bytes); + + if (faces[i].empty()) { + std::println("Could not load face: {}", p_faces[i]); + return; + } + + if (w != face_width || h != face_height) { + std::println("Cubemap faces must match dimensions. Face 0 is " + "{}x{}, face {} is {}x{} ({})", + face_width, + face_height, + i, + w, + h, + p_faces[i]); + return; + } + } + + const uint32_t width = static_cast(face_width); + const uint32_t height = static_cast(face_height); + const VkDeviceSize face_size_bytes = + static_cast(width) * static_cast(height) * + static_cast(bytes_per_pixel); + const VkDeviceSize total_size_bytes = face_size_bytes * 6; + + vk::image_params skybox_params = { + .extent = { .width = width, .height = height, .depth = 1 }, + .format = image_format, + .memory_mask = p_physical.memory_properties( + vk::memory_property::device_local_bit), + .usage = + vk::image_usage::transfer_dst_bit | vk::image_usage::sampled_bit, + .image_flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, + .view_type = VK_IMAGE_VIEW_TYPE_CUBE, + .layer_count = 6, + .array_layers = 6, + }; + m_skybox_image = vk::sample_image(m_device, skybox_params); + + // perform staging buffer + vk::buffer_parameters staging_params = { + .memory_mask = p_physical.memory_properties( + vk::memory_property::host_visible_bit | + vk::memory_property::host_coherent_bit), + .usage = vk::buffer_usage::transfer_src_bit, + }; + + vk::buffer staging(m_device, total_size_bytes, staging_params); + + staging.transfer(faces); + + for (size_t i = 0; i < faces.size(); i++) { + stbi_image_free(faces[i].data()); + } + + vk::command_params upload_params = { + .levels = vk::command_levels::primary, + .queue_index = 0, // graphics queue family index + .flags = vk::command_pool_flags::reset, + }; + vk::command_buffer upload_cmd(m_device, upload_params); + upload_cmd.begin(vk::command_usage::one_time_submit); + + // NOTE: explicitly set VK_IMAGE_ASPECT_COLOR_BIT to ensure the wrong + // aspect flags are not being set when using + // vk::sample_image::memory_barrier + m_skybox_image.memory_barrier(upload_cmd, + image_format, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT, + 6); + + std::array regions; + + for (uint32_t face = 0; face < regions.size(); face++) { + // Copy the specific face region to the image + regions[face] = { + .offset = static_cast(face_size_bytes * face), + .base_array_layer = face, + .image_offset = { .width = 0, .height = 0, .depth = 0 }, + .image_extent = { .width = width, .height = height }, + }; + } + + staging.copy_to_image(upload_cmd, m_skybox_image, regions); + + m_skybox_image.memory_barrier(upload_cmd, + image_format, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT, + 6); + upload_cmd.end(); + + VkQueue graphics_queue = nullptr; + vkGetDeviceQueue(m_device, 0, 0, &graphics_queue); + const VkCommandBuffer cmd = upload_cmd; + VkSubmitInfo submit = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .pWaitDstStageMask = nullptr, + .commandBufferCount = 1, + .pCommandBuffers = &cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + vk::vk_check(vkQueueSubmit(graphics_queue, 1, &submit, nullptr), + "vkQueueSubmit(cubemap upload)"); + vk::vk_check(vkQueueWaitIdle(graphics_queue), + "vkQueueWaitIdle(cubemap upload)"); + + upload_cmd.destruct(); + staging.destruct(); + + create_skybox_pipeline(); + } + + void create_skybox_pipeline() { + create_buffers(); + + std::array attribute_entries = { + vk::vertex_attribute_entry{ + .location = 0, + .format = vk::format::rgb32_sfloat, + .stride = offsetof(vk::vertex_input, position), + }, + vk::vertex_attribute_entry{ + .location = 1, + .format = vk::format::rgb32_sfloat, + .stride = offsetof(vk::vertex_input, color), + }, + vk::vertex_attribute_entry{ + .location = 2, + .format = vk::format::rgb32_sfloat, + .stride = offsetof(vk::vertex_input, normals), + }, + vk::vertex_attribute_entry{ + .location = 3, + .format = vk::format::rg32_sfloat, + .stride = offsetof(vk::vertex_input, uv), + } + }; + std::array attribute = { + vk::vertex_attribute{ + // layout (set = 0, binding = 0) + .binding = 0, + .entries = attribute_entries, + .stride = sizeof(vk::vertex_input), + .input_rate = vk::input_rate::vertex, + }, + }; + + const std::array sources = { + vk::shader_source{ + .filename = "shader_samples/sample7-skybox/skybox.vert.spv", + .stage = vk::shader_stage::vertex, + }, + vk::shader_source{ + .filename = "shader_samples/sample7-skybox/skybox.frag.spv", + .stage = vk::shader_stage::fragment, + }, + }; + + vk::shader_resource_info shader_info = { + .sources = sources, + }; + m_skybox_shaders = vk::shader_resource(m_device, shader_info); + m_skybox_shaders.vertex_attributes(attribute); + + // set=0 binding=0 UBO: mat4 VP + // vk::uniform_params ubo_params = { + // .phsyical_memory_properties = p_memory_properties, + // .debug_name = "skybox_ubo", + // .vkSetDebugUtilsObjectNameEXT = nullptr, + // }; + vk::buffer_parameters uniform_params = { + .memory_mask = m_physical.value().memory_properties( + vk::memory_property::host_visible_bit | + vk::memory_property::host_cached_bit), + .usage = vk::buffer_usage::uniform_buffer_bit, + }; + m_skybox_ubo = + vk::uniform_buffer(m_device, sizeof(skybox_uniform), uniform_params); + + skybox_uniform identity = { .proj_view = glm::mat4(1.0f) }; + identity.proj_view[1][1] *= -1; + + m_skybox_ubo.transfer( + std::span(&identity, 1)); + + // set=0 bindings: + // - binding 0: UBO (vertex) + // - binding 1: samplerCube (fragment) + std::array entries = { + vk::descriptor_entry{ + .type = vk::descriptor_type::uniform, + .binding_point = + vk::descriptor_binding_point{ + .binding = 0, .stage = vk::shader_stage::vertex }, + .descriptor_count = 1, + }, + vk::descriptor_entry{ + .type = vk::descriptor_type::combined_image_sampler, + .binding_point = + vk::descriptor_binding_point{ + .binding = 1, .stage = vk::shader_stage::fragment }, + .descriptor_count = 1, + }, + }; + + vk::descriptor_layout desc_layout = { + .slot = 0, + .max_sets = 1, + .entries = entries, + }; + m_skybox_descriptors = vk::descriptor_resource(m_device, desc_layout); + + const std::array ubo_writes = { + vk::write_buffer{ .buffer = m_skybox_ubo, + .offset = 0, + .range = + static_cast(sizeof(skybox_uniform)) }, + }; + const vk::write_buffer_descriptor ubo_write_desc = { + .dst_binding = 0, + .uniforms = ubo_writes, + }; + + const std::array image_writes = { + vk::write_image{ + .sampler = m_skybox_image.sampler(), + .view = m_skybox_image.image_view(), + .layout = vk::image_layout::shader_read_only_optimal, + }, + }; + const vk::write_image_descriptor image_write_desc = { + .dst_binding = 1, + .sample_images = image_writes, + }; + + m_skybox_descriptors.update(std::span(&ubo_write_desc, 1), + std::span(&image_write_desc, 1)); + + const std::array layouts = { + m_skybox_descriptors.layout(), + }; + + const std::array + blend_attachments = { + vk::color_blend_attachment_state{ .blend_enabled = false }, + }; + vk::color_blend_state blend_state = { + .logic_op_enable = false, + .logical_op = vk::logical_op::copy, + .attachments = blend_attachments, + .blend_constants = {}, + }; + + std::array dyn = { + vk::dynamic_state::viewport, + vk::dynamic_state::scissor, + }; + + // pipeline expects a non-const span + std::array pipeline_layouts = layouts; + + vk::pipeline_params pipe_info = { + .renderpass = m_renderpass, + .shader_modules = m_skybox_shaders.handles(), + .vertex_attributes = + m_skybox_shaders.vertex_attributes(), // no vertex input + .vertex_bind_attributes = + m_skybox_shaders.vertex_bind_attributes(), // no vertex input + .descriptor_layouts = pipeline_layouts, + .input_assembly = + vk::input_assembly_state{ + .topology = vk::primitive_topology::triangle_list, + .primitive_restart_enable = false, + }, + .viewport = + vk::viewport_state{ .viewport_count = 1, .scissor_count = 1 }, + .rasterization = + vk::rasterization_state{ + .polygon_mode = vk::polygon_mode::fill, + .cull_mode = vk::cull_mode::front_bit, + // .cull_mode = vk::cull_mode::none, + // .front_face = vk::front_face::counter_clockwise, + .front_face = vk::front_face::clockwise, + .line_width = 1.f, + }, + .multisample = vk::multisample_state{}, + .color_blend = blend_state, + .depth_stencil_enabled = true, + .depth_stencil = + vk::depth_stencil_state{ + .depth_test_enable = true, + .depth_write_enable = false, + .depth_compare_op = vk::compare_op::less_or_equal, + .depth_bounds_test_enable = false, + .stencil_test_enable = false, + }, + .dynamic_states = dyn, + }; + + m_skybox_pipeline = vk::pipeline(m_device, pipe_info); + } + + void create_buffers() { + + std::vector vertices = { + // Front Face + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + + // Left Face + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + + // Right Face + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + + // Back Face + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + + // Top Face + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, 1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + + // Bottom Face + vk::vertex_input{ + .position = { -1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, -1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { -1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + vk::vertex_input{ + .position = { 1.0f, -1.0f, 1.0f }, + .color = { 1.0f, 1.0f, 1.0f }, + .normals = { 0.0f, 0.0f, 0.0f }, + .uv = { 0.0f, 0.0f }, + }, + }; + + m_skybox_vbo_size = vertices.size(); + + vk::buffer_parameters vertex_params = { + .memory_mask = m_physical.value().memory_properties( + vk::memory_property::device_local_bit | + vk::memory_property::host_visible_bit), + .usage = vk::buffer_usage::transfer_dst_bit | + vk::buffer_usage::vertex_buffer_bit, + }; + + m_skybox_vbo = vk::vertex_buffer(m_device, vertices, vertex_params); + } + + void update_uniform(const skybox_uniform& p_uniform) { + m_skybox_ubo.transfer( + std::span(&p_uniform, 1)); + } + + void bind(vk::command_buffer p_command) { + m_skybox_pipeline.bind(p_command); + std::array descriptors = { m_skybox_descriptors }; + p_command.bind_descriptors(m_skybox_pipeline.layout(), + VK_PIPELINE_BIND_POINT_GRAPHICS, + descriptors); + + std::array skybox_buffers = { m_skybox_vbo }; + uint64_t offset = 0; + p_command.bind_vertex_buffers(skybox_buffers, + std::span(&offset, 1)); + } + + void draw(const vk::command_buffer& p_command) { + vkCmdDraw(p_command, m_skybox_vbo_size, 1, 0, 0); + } + + void destruct() { + m_skybox_ubo.destruct(); + m_skybox_image.destruct(); + m_skybox_shaders.destruct(); + m_skybox_pipeline.destruct(); + + m_skybox_vbo.destruct(); + m_skybox_descriptors.destruct(); + } + +private: + vk::uniform_buffer m_skybox_ubo; + VkDevice m_device = nullptr; + std::optional m_physical; + vk::sample_image m_skybox_image; + vk::shader_resource m_skybox_shaders; + vk::pipeline m_skybox_pipeline; + vk::vertex_buffer m_skybox_vbo; + vk::descriptor_resource m_skybox_descriptors; + VkRenderPass m_renderpass; + uint32_t m_skybox_vbo_size = 0; +}; \ No newline at end of file diff --git a/shader_samples/sample7-skybox/skybox.frag b/shader_samples/sample7-skybox/skybox.frag index ffcc2b8..b146b1f 100644 --- a/shader_samples/sample7-skybox/skybox.frag +++ b/shader_samples/sample7-skybox/skybox.frag @@ -1,56 +1,56 @@ -// #version 450 +#version 450 -// layout (location=0) in vec3 TexCoords; +layout (location=0) in vec3 TexCoords; -// layout (location=0) out vec4 out_Color; +layout (location=0) out vec4 out_Color; -// layout(set = 0, binding = 1) uniform samplerCube cubeSampler; +layout(set = 0, binding = 1) uniform samplerCube cubeSampler; -// void main() { -// out_Color = texture(cubeSampler, TexCoords); -// } -#version 450 +void main() { + out_Color = texture(cubeSampler, TexCoords); +} +// #version 450 -layout (location=0) in vec3 TexCoords; // Direction vector from vertex shader +// layout (location=0) in vec3 TexCoords; // Direction vector from vertex shader -layout (location=0) out vec4 out_Color; +// layout (location=0) out vec4 out_Color; -layout(set = 0, binding = 1) uniform sampler2D cubeSampler; +// layout(set = 0, binding = 1) uniform samplerCube cubeSampler; -// Constants to convert direction to spherical UVs -const vec2 invAtan = vec2(0.1591, 0.3183); // 1.0 / (2.0 * PI), 1.0 / PI +// // Constants to convert direction to spherical UVs +// const vec2 invAtan = vec2(0.1591, 0.3183); // 1.0 / (2.0 * PI), 1.0 / PI -vec2 SampleEquirectangular(vec3 v) { - // Normalize the input direction - // We only need to grab the point of the vector - vec3 direction = normalize(v); +// vec2 SampleEquirectangular(vec3 v) { +// // Normalize the input direction +// // We only need to grab the point of the vector +// vec3 direction = normalize(v); - // Calculate spherical coordinates - // atan(direction.z, direction.x) = atan(-PI, PI) - // atan(z, x) returns -PI to PI - // asin(y) returns -PI/2 to PI/2 +// // Calculate spherical coordinates +// // atan(direction.z, direction.x) = atan(-PI, PI) +// // atan(z, x) returns -PI to PI +// // asin(y) returns -PI/2 to PI/2 - vec2 uv = vec2(atan(direction.z, direction.x), asin(direction.y)); +// vec2 uv = vec2(atan(direction.z, direction.x), asin(direction.y)); - // Inverses the uv to be between the [0.0, 1.0] range - uv *= invAtan; - uv += 0.5; - return uv; -} +// // Inverses the uv to be between the [0.0, 1.0] range +// uv *= invAtan; +// uv += 0.5; +// return uv; +// } -void main() { - // Get the 2D UV coordinate from the 3D direction - vec2 uv = SampleEquirectangular(TexCoords); +// void main() { +// // Get the 2D UV coordinate from the 3D direction +// vec2 uv = SampleEquirectangular(TexCoords); - // Sample the HDR map - vec3 color = texture(cubeSampler, uv).rgb; +// // Sample the HDR map +// vec3 color = texture(cubeSampler, uv).rgb; - // Simple Reinhard tone mapping (HDR values can be > 1.0, - // so we must compress them to [0, 1] for the screen) - color = color / (color + vec3(1.0)); +// // Simple Reinhard tone mapping (HDR values can be > 1.0, +// // so we must compress them to [0, 1] for the screen) +// color = color / (color + vec3(1.0)); - // Gamma correction - color = pow(color, vec3(1.0/2.2)); +// // Gamma correction +// color = pow(color, vec3(1.0/2.2)); - out_Color = vec4(color, 1.0); -} \ No newline at end of file +// out_Color = vec4(color, 1.0); +// } \ No newline at end of file diff --git a/shader_samples/sample7-skybox/skybox.frag.spv b/shader_samples/sample7-skybox/skybox.frag.spv index 1dd6d54..c9e20ff 100644 Binary files a/shader_samples/sample7-skybox/skybox.frag.spv and b/shader_samples/sample7-skybox/skybox.frag.spv differ diff --git a/vulkan-cpp/buffer.cppm b/vulkan-cpp/buffer.cppm index e20c305..157962d 100644 --- a/vulkan-cpp/buffer.cppm +++ b/vulkan-cpp/buffer.cppm @@ -5,6 +5,7 @@ module; #include #include #include +#include export module vk:buffer; @@ -140,8 +141,8 @@ export namespace vk { /** * @brief writing uniforms that are represented into bytes * @param p_data are the bytes to allow GPU to access - * Example Usage: * + * Example Usage: * ```C++ * buffers staging_buffer(logical_device, ...); * @@ -164,6 +165,75 @@ export namespace vk { vkUnmapMemory(m_device, m_device_memory); } + /** + * @brief Transfer CPU-accessible data from a sub-range of buffers + * into a single underlying staging allocation + * + * Single mapping block to gather multiple sub-range of data from + * the host then transferring them sequentially into device-visible + * memory, calculating the offsets with the bytes of those + * sub-ranges. + * + * @tparam Range must satisfies the forward_range concept + * @param p_range collection of data sources (containers of spans, + * vectors, arrays, etc) to transfer. + * @param p_offset is the base offset into the Vulkan device memory + * allocation where the mapping begins + * + * @brief Requirements to perform this operation + * - p_range: Must be a valid contiguous buffer (std::span) to + * determine their underlying bytes. + * - alignment: sub-ranges are assumed to be tightly packed by + * default. + * + * @brief Layout of data stored in host-visible memory during + * serialization + * + * [ base offset ........................................ end ] + * |--- Data 0 --|-- Data 1 --|-- Data 2 --| ... |-- Data N --| + * \____________/\___________/\___________/ \___________/ + * offset = 0 size[0] size[0]+size[1] + * + * Example Usage: + * + * ```C++ + * // Under single vkMapMemory call writes all 6 contiguous chunks + * of data to one staging buffer std::array, 6> + * skybox_faces = m_skybox.faces(); + * staging_buffer.transfer(skybox_faces); + * ``` + * + */ + template + requires std::ranges::forward_range + void transfer(Range&& p_range, uint64_t p_offset = 0) { + uint64_t total_size = 0; + + for (const auto& data : p_range) { + total_size += std::span(data).size_bytes(); + } + + void* mapped = nullptr; + vk_check(vkMapMemory(m_device, + m_device_memory, + p_offset, + total_size, + 0, + &mapped), + "vkMapMemory"); + + auto* bytes = static_cast(mapped); + uint64_t offset = 0; + + for (const auto& data : p_range) { + auto s = std::span{ data }; + std::memcpy(bytes + offset, s.data(), s.size_bytes()); + offset += s.size_bytes(); + } + + vkUnmapMemory(m_device, m_device_memory); + } + /** * @brief Transfers CPU-accessible data from the staging buffer to * the GPU resource image. diff --git a/vulkan-cpp/sample_image.cppm b/vulkan-cpp/sample_image.cppm index ec5231c..d5a6324 100644 --- a/vulkan-cpp/sample_image.cppm +++ b/vulkan-cpp/sample_image.cppm @@ -269,10 +269,9 @@ export namespace vk { VkFormat p_format, VkImageLayout p_old, VkImageLayout p_new, - uint32_t p_aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT) { + uint32_t p_aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT, + uint32_t p_layer_count = 1) { - // 1. Image Memory Barrier Initialization (using C++ Designated - // Initializers - C++20) VkImageMemoryBarrier image_memory_barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .pNext = nullptr, @@ -283,20 +282,18 @@ export namespace vk { .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = m_image, - .subresourceRange = { .aspectMask = - static_cast( - p_aspect_mask), - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1 } + .subresourceRange = { + .aspectMask = static_cast(p_aspect_mask), + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = p_layer_count, + }, }; VkPipelineStageFlags source_stage = VK_PIPELINE_STAGE_NONE; VkPipelineStageFlags dst_stages = VK_PIPELINE_STAGE_NONE; - // 2. Aspect Mask Logic (Keep as if/else, but use helper - // function) if (p_new == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL || has_stencil_attachment(p_format)) {