Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,80 @@ Vulkan Grass Rendering

**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5**

* (TODO) YOUR NAME HERE
* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab)
Jason Li ([LinkedIn](https://linkedin.com/in/jeylii))
* Tested on: Windows 10, Ryzen 5 3600X @ 3.80GHz 32 GB, NVIDIA RTX 4070

### (TODO: Your README)
# **Summary**
This project is a GPU Grass Simulator implemented using C++ and the Vulkan API. It simulates each grass blade based on a physical model defined in [Responseive Real-Time Grass Rendering for General 3D Scenes](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf). The simulation includes force simulation, grass blade culling, and different shaders including a tesselation shader.

*DO NOT* leave the README to the last minute! It is a crucial part of the
project, and we will not be able to grade you without a good README.
Example Scene |
:-------------------------:|
![Example Scene](img/grass_example.gif) |

## **Tesselation Shader**
Grass blades are modeled as Bezier curves in this simulation, with three control points v<sub>0</sub>, v<sub>1</sub>, and v<sub>2</sub>. In addition, each blade has an "up vector", a width, height, orientation, and stiffness associated with it. In order to efficiently render such a shape, a tessellation shader is used to create the curvature of each grass blade from the three control points. In this simulation, we are using a "triangle-tip" shape, as described in the above reference.

Blade Model |
:-------------------------:|
![Blade Model](img/blade_model.jpg) |

## **Force Simulation**

Three types of forces are simulated on the grass blades:
- Gravity,
- Recovery,
- and Wind.

Each of these physics-based forces is applied to the "tip" of each blade of grass (v<sub>2</sub>), following the methods given in the [above reference](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf). The other control point v<sub>1</sub> is updated based on the new location of v<sub>2</sub>. A time-varying wind function is used to simulate a gradually changing wind direction which is uniformly applied to each blade of grass. In addition to this, validation is required to ensure that each blade of grass remains in a "good" state, i.e. its control points stay above ground, etc. With just the forces on each blade of grass implemented, our grass simulation looks like this:

Physical Force Simulation |
:-------------------------:|
![Physical Force Simulation](img/grass_forces.gif) |

## **Grass Blade Culling**
After the grass is sufficiently simulated, we can increase the performance of our simulation by culling grass blades that are unnecessary or cause aliasing. In this case, three types of culling were applied:
- Orientation culling,
- View-frustum culling,
- and Distance culling.

### Orientation Culling
For orientation culling, we cull the grass blades which appear nearly parallel to the camera's view direction. This is because our grass blades are only 2-dimensional, so any grass blades which are parallel or similar to the view will be rendered smaller than the size of a pixel. This can cause undesirable aliasing artifacts. Below is an example of what this looks like in our simulation.

Orientation Culling |
:-------------------------:|
![Orientation Culling](img/grass_orientation_culling.gif) |

### View-frustum Culling
We also want to cull blades of grass which are outside of our field of view. This is accomplished using view-frustum culling. This method tests if all three control points of a certain blade of grass are located outside the view-frustum. If so, the blade is culled - with some tolerance to allow for grass on the edge of our view to render. An example of what this looks like in our simulation is included below.

View-frustum Culling |
:-------------------------:|
![View-frustum Culling](img/grass_view_culling.gif) |

### Distance Culling
For grass blades that are seen from a large distance, they may be rendered as a pixel or smaller. To prevent alisaing from this effect, we use distance culling - culling an increasing number of blades the further the blades' positions are from the camera.

Distance Culling |
:-------------------------:|
![Distance Culling](img/grass_distance_culling.gif) |

## **Performance Analysis**
Below is a performance analysis of our grass simulation when compared across different runs with some changing variables. The analysis was done by choosing a standard angle to view the scene, then running the simulation with features on or off or varying the number of blades. Average FPS was aggregated over periods of 10 seconds. This standard scene is shown below.

Performance Analysis Angle |
:-------------------------:|
![Performance Analysis Angle](img/performance_analysis_scene.png) |

### **Performance vs. Number of Blades**
FPS vs. Number of Blades |
:-------------------------:|
![FPS vs. Number of Blades](img/fps_vs_num_blades.png) |

The graph above compares the frames per second on average for our perforamnce analysis scene when using a different number of grass blades in the scene. As expected, the FPS drops when using a larger number of grass blades. As the number of blades is on a logarithmic scale, we can see that the performance drop is quite significant at larger numbers. This is expected, as more work has to be done per blade. However, note that this simulation does not include any interactions between blades of grass, so the performance drop with such interactions included may be even more significant.

### Performance vs. Usage of Culling
FPS vs. Culling Method (2<sup>18</sup> blades of grass) |
:-------------------------:|
![FPS vs. Culling Method](img/fps_vs_blade_culling_method.png) |

The chart above compares the frames per second on average for our performance analysis scene with 2<sup>18</sup> blades of grass when using different methods of grass blade culling. In our specific chosen scene, distance culling seems to be the most significant, as our camera view is quite far away from one edge of the platform. Orientation culling also seems to be effective, as the probability of a random blade of grass being nearly parallel with the camera's view (here we used a coefficient of 0.75) is quite large. View-frustum culling was least effective here, though this is likely due to the limited amount of grass culled in this particular view of the grass scene. The combination of all three culling methods significantly outperforms any individual method, as expected.
Binary file added img/fps_vs_blade_culling_method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/fps_vs_num_blades.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grass_distance_culling.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grass_example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grass_forces.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grass_orientation_culling.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grass_view_culling.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/performance_analysis_scene.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/Blades.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode
indirectDraw.firstVertex = 0;
indirectDraw.firstInstance = 0;

BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, bladesBuffer, bladesBufferMemory);
BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, bladesBuffer, bladesBufferMemory);
BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
BufferUtils::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Blades.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <array>
#include "Model.h"

constexpr static unsigned int NUM_BLADES = 1 << 13;
constexpr static unsigned int NUM_BLADES = 1 << 18;
constexpr static float MIN_HEIGHT = 1.3f;
constexpr static float MAX_HEIGHT = 2.5f;
constexpr static float MIN_WIDTH = 0.1f;
Expand Down
3 changes: 2 additions & 1 deletion src/Camera.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ Camera::Camera(Device* device, float aspectRatio) : device(device) {
cameraBufferObject.viewMatrix = glm::lookAt(glm::vec3(0.0f, 1.0f, 10.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
cameraBufferObject.projectionMatrix = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 100.0f);
cameraBufferObject.projectionMatrix[1][1] *= -1; // y-coordinate is flipped

BufferUtils::CreateBuffer(device, sizeof(CameraBufferObject), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buffer, bufferMemory);
vkMapMemory(device->GetVkDevice(), bufferMemory, 0, sizeof(CameraBufferObject), 0, &mappedData);
memcpy(mappedData, &cameraBufferObject, sizeof(CameraBufferObject));
UpdateOrbit(10, -20, -2.2);

}

VkBuffer Camera::GetBuffer() const {
Expand Down
156 changes: 148 additions & 8 deletions src/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,37 @@ void Renderer::CreateTimeDescriptorSetLayout() {
}

void Renderer::CreateComputeDescriptorSetLayout() {
// TODO: Create the descriptor set layout for the compute pipeline
// Remember this is like a class definition stating why types of information
// will be stored at each binding
VkDescriptorSetLayoutBinding bladesLayoutBinding = {};
bladesLayoutBinding.binding = 0;
bladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
bladesLayoutBinding.descriptorCount = 1;
bladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
bladesLayoutBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding culledBladesLayoutBinding = {};
culledBladesLayoutBinding.binding = 1;
culledBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
culledBladesLayoutBinding.descriptorCount = 1;
culledBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
culledBladesLayoutBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding numBladesLayoutBinding = {};
numBladesLayoutBinding.binding = 2;
numBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
numBladesLayoutBinding.descriptorCount = 1;
numBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
numBladesLayoutBinding.pImmutableSamplers = nullptr;

std::vector<VkDescriptorSetLayoutBinding> bindings = { bladesLayoutBinding, culledBladesLayoutBinding, numBladesLayoutBinding };

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();

if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("Failed to create descriptor set layout");
}
}

void Renderer::CreateDescriptorPool() {
Expand All @@ -216,6 +244,7 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },

// TODO: Add any additional types and counts of descriptors you will need to allocate
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast<uint32_t>(scene->GetBlades().size() * 3)},
};

VkDescriptorPoolCreateInfo poolInfo = {};
Expand Down Expand Up @@ -320,6 +349,44 @@ void Renderer::CreateModelDescriptorSets() {
void Renderer::CreateGrassDescriptorSets() {
// TODO: Create Descriptor sets for the grass.
// This should involve creating descriptor sets which point to the model matrix of each group of grass blades
// Describe the desciptor set
grassDescriptorSets.resize(scene->GetBlades().size());

VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(grassDescriptorSets.size());
allocInfo.pSetLayouts = layouts;

// Allocate descriptor sets
if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate grass descriptor set");
}

std::vector<VkWriteDescriptorSet> descriptorWrites(grassDescriptorSets.size());

for (uint32_t i = 0; i < scene->GetBlades().size(); i++)
{
VkDescriptorBufferInfo grassBufferInfo = {};
grassBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer();
grassBufferInfo.offset = 0;
grassBufferInfo.range = sizeof(ModelBufferObject);

descriptorWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[i].dstSet = grassDescriptorSets[i];
descriptorWrites[i].dstBinding = 0;
descriptorWrites[i].dstArrayElement = 0;
descriptorWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[i].descriptorCount = 1;
descriptorWrites[i].pBufferInfo = &grassBufferInfo;
descriptorWrites[i].pImageInfo = nullptr;
descriptorWrites[i].pTexelBufferView = nullptr;
}


// Update descriptor sets
vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}

void Renderer::CreateTimeDescriptorSet() {
Expand Down Expand Up @@ -360,6 +427,72 @@ void Renderer::CreateTimeDescriptorSet() {
void Renderer::CreateComputeDescriptorSets() {
// TODO: Create Descriptor sets for the compute pipeline
// The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades
computeDescriptorSets.resize(scene->GetBlades().size());

VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(computeDescriptorSets.size());
allocInfo.pSetLayouts = layouts;

// Allocate descriptor sets
if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate compute descriptor set");
}

std::vector<VkWriteDescriptorSet> descriptorWrites(computeDescriptorSets.size() * 3);
for (uint32_t i = 0; i < scene->GetBlades().size(); i++)
{
VkDescriptorBufferInfo bladeBufferInfo = {};
bladeBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
bladeBufferInfo.offset = 0;
bladeBufferInfo.range = NUM_BLADES * sizeof(Blade);

descriptorWrites[3 * i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i].dstBinding = 0;
descriptorWrites[3 * i].dstArrayElement = 0;
descriptorWrites[3 * i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i].descriptorCount = 1;
descriptorWrites[3 * i].pBufferInfo = &bladeBufferInfo;
descriptorWrites[3 * i].pImageInfo = nullptr;
descriptorWrites[3 * i].pTexelBufferView = nullptr;

VkDescriptorBufferInfo culledBladesBufferInfo = {};
culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer();
culledBladesBufferInfo.offset = 0;
culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

descriptorWrites[3 * i + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 1].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 1].dstBinding = 1;
descriptorWrites[3 * i + 1].dstArrayElement = 0;
descriptorWrites[3 * i + 1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 1].descriptorCount = 1;
descriptorWrites[3 * i + 1].pBufferInfo = &culledBladesBufferInfo;
descriptorWrites[3 * i + 1].pImageInfo = nullptr;
descriptorWrites[3 * i + 1].pTexelBufferView = nullptr;

VkDescriptorBufferInfo numBladesBufferInfo = {};
numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer();
numBladesBufferInfo.offset = 0;
numBladesBufferInfo.range = sizeof(BladeDrawIndirect);

descriptorWrites[3 * i + 2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 2].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 2].dstBinding = 2;
descriptorWrites[3 * i + 2].dstArrayElement = 0;
descriptorWrites[3 * i + 2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 2].descriptorCount = 1;
descriptorWrites[3 * i + 2].pBufferInfo = &numBladesBufferInfo;
descriptorWrites[3 * i + 2].pImageInfo = nullptr;
descriptorWrites[3 * i + 2].pTexelBufferView = nullptr;

}
// Update descriptor sets
vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

}

void Renderer::CreateGraphicsPipeline() {
Expand Down Expand Up @@ -717,7 +850,7 @@ void Renderer::CreateComputePipeline() {
computeShaderStageInfo.pName = "main";

// TODO: Add the compute dsecriptor set layout you create to this list
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout };
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout };

// Create pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
Expand Down Expand Up @@ -884,6 +1017,11 @@ void Renderer::RecordComputeCommandBuffer() {
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 1, 1, &timeDescriptorSet, 0, nullptr);

// TODO: For each group of blades bind its descriptor set and dispatch
for (uint32_t i = 0; i < scene->GetBlades().size(); i++) {
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[i], 0, nullptr);
vkCmdDispatch(computeCommandBuffer, (NUM_BLADES / WORKGROUP_SIZE), 1, 1);
}


// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
Expand Down Expand Up @@ -973,16 +1111,17 @@ void Renderer::RecordCommandBuffers() {
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipeline);

for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) {
VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() };
VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; // from GetCulledBladesBuffer
VkDeviceSize offsets[] = { 0 };
// TODO: Uncomment this when the buffers are populated
// vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

// TODO: Bind the descriptor set for each grass blades model

vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS,
grassPipelineLayout, 1, 1, &grassDescriptorSets[j], 0, nullptr);
// Draw
// TODO: Uncomment this when the buffers are populated
// vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
}

// End render pass
Expand Down Expand Up @@ -1057,6 +1196,7 @@ Renderer::~Renderer() {
vkDestroyDescriptorSetLayout(logicalDevice, cameraDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, modelDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, timeDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, computeDescriptorSetLayout, nullptr);

vkDestroyDescriptorPool(logicalDevice, descriptorPool, nullptr);

Expand Down
Loading