// ============================================= // Aster: triangle.cpp // Copyright (c) 2020-2024 Anish Bhobe // ============================================= #include "aster/constants.h" #include "aster/context.h" #include "aster/device.h" #include "aster/physical_device.h" #include "aster/window.h" #include "aster/global.h" #include "aster/pipeline.h" #include "aster/swapchain.h" #include #include constexpr u32 MAX_FRAMES_IN_FLIGHT = 3; constexpr auto VERTEX_SHADER_FILE = "shader/triangle.vs.hlsl.spv"; constexpr auto FRAGMENT_SHADER_FILE = "shader/white.frag.glsl.spv"; bool IsSuitableDevice(const PhysicalDevice *physicalDevice); PhysicalDevice FindSuitableDevice(const PhysicalDevices &physicalDevices); QueueAllocation FindAppropriateQueueAllocation(const PhysicalDevice *physicalDevice); eastl::optional> ReadFile(cstr fileName); vk::ShaderModule CreateShader(const Device *device, cstr shaderFile); struct Frame { const Device *m_Device; vk::CommandPool m_Pool; vk::Fence m_FrameAvailableFence; vk::Semaphore m_ImageAcquireSem; vk::Semaphore m_RenderFinishSem; Frame(const Device *device, u32 queueFamilyIndex, u32 frameCount); ~Frame(); [[nodiscard]] vk::CommandBuffer AllocateCommandBuffer() const; }; struct Shader { vk::ShaderModule m_ShaderModule; vk::ShaderStageFlags m_Stage; }; Pipeline CreatePipeline(const Device *device, const Swapchain *swapchain, cstr vertexShaderFile, cstr fragmentShaderFile); int main(int, char **) { MIN_LOG_LEVEL(Logger::LogType::eInfo); Context context = {"Triangle", VERSION}; Window window = {"Triangle (Aster)", &context, {640, 480}}; PhysicalDevices physicalDevices = {&window, &context}; PhysicalDevice deviceToUse = FindSuitableDevice(physicalDevices); INFO("Using {} as the primary device.", deviceToUse.m_DeviceProperties.deviceName.data()); Features enabledDeviceFeatures = {.m_Vulkan13Features = {.dynamicRendering = vk::True}}; QueueAllocation queueAllocation = FindAppropriateQueueAllocation(&deviceToUse); Device device = {&context, &deviceToUse, &enabledDeviceFeatures, {queueAllocation}, "Primary Device"}; vk::Queue commandQueue = device.GetQueue(queueAllocation.m_Family, 1); Swapchain swapchain = {&window, &device, "Primary Chain"}; Pipeline pipeline = CreatePipeline(&device, &swapchain, VERTEX_SHADER_FILE, FRAGMENT_SHADER_FILE); // Persistent variables vk::Viewport viewport = { .x = 0, .y = Cast(swapchain.m_Extent.height), .width = Cast(swapchain.m_Extent.width), .height = -Cast(swapchain.m_Extent.height), .minDepth = 0.0, .maxDepth = 1.0, }; vk::Rect2D scissor = { .offset = {0, 0}, .extent = swapchain.m_Extent, }; vk::ImageMemoryBarrier topOfThePipeBarrier = { .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eColorAttachmentOptimal, .srcQueueFamilyIndex = queueAllocation.m_Family, .dstQueueFamilyIndex = queueAllocation.m_Family, .subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; vk::ImageMemoryBarrier renderToPresentBarrier = { .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, .newLayout = vk::ImageLayout::ePresentSrcKHR, .srcQueueFamilyIndex = queueAllocation.m_Family, .dstQueueFamilyIndex = queueAllocation.m_Family, .subresourceRange = { .aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }, }; // Frames eastl::fixed_vector frames; for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { frames.emplace_back(&device, queueAllocation.m_Family, i); } u32 frameIndex = 0; while (window.Poll()) { Frame *currentFrame = &frames[frameIndex]; auto result = device.m_Device.waitForFences(1, ¤tFrame->m_FrameAvailableFence, vk::True, MaxValue); ERROR_IF(Failed(result), "Waiting for fence {} failed. Cause: {}", frameIndex, result) THEN_ABORT(result); u32 imageIndex; result = device.m_Device.acquireNextImageKHR(swapchain.m_Swapchain, MaxValue, currentFrame->m_ImageAcquireSem, nullptr, &imageIndex); if (Failed(result)) { switch (result) { case vk::Result::eErrorOutOfDateKHR: case vk::Result::eSuboptimalKHR: INFO("Recreating Swapchain. Cause: {}", result); swapchain.Create(&window); viewport.y = Cast(swapchain.m_Extent.height); viewport.width = Cast(swapchain.m_Extent.width); viewport.height = -Cast(swapchain.m_Extent.height); scissor.extent = swapchain.m_Extent; break; default: ERROR("Waiting for swapchain image {} failed. Cause: {}", frameIndex, result) THEN_ABORT(result); } } result = device.m_Device.resetFences(1, ¤tFrame->m_FrameAvailableFence); ERROR_IF(Failed(result), "Fence {} reset failed. Cause: {}", frameIndex, result) THEN_ABORT(result); result = device.m_Device.resetCommandPool(currentFrame->m_Pool, {}); ERROR_IF(Failed(result), "Command pool {} reset failed. Cause: {}", frameIndex, result) THEN_ABORT(result); vk::ImageView currentImageView = swapchain.m_ImageViews[imageIndex]; vk::Image currentImage = swapchain.m_Images[imageIndex]; vk::CommandBuffer cmd = currentFrame->AllocateCommandBuffer(); topOfThePipeBarrier.image = currentImage; renderToPresentBarrier.image = currentImage; vk::CommandBufferBeginInfo beginInfo = {.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}; result = cmd.begin(&beginInfo); ERROR_IF(Failed(result), "Command buffer begin failed. Cause: {}", result) THEN_ABORT(result); cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, 0, nullptr, 0, nullptr, 1, &topOfThePipeBarrier); // Render vk::RenderingAttachmentInfo attachmentInfo = { .imageView = currentImageView, .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, .resolveMode = vk::ResolveModeFlagBits::eNone, .loadOp = vk::AttachmentLoadOp::eClear, .storeOp = vk::AttachmentStoreOp::eStore, .clearValue = vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f}, }; vk::RenderingInfo renderingInfo = { .renderArea = {.extent = swapchain.m_Extent}, .layerCount = 1, .colorAttachmentCount = 1, .pColorAttachments = &attachmentInfo, }; cmd.beginRendering(&renderingInfo); cmd.setViewport(0, 1, &viewport); cmd.setScissor(0, 1, &scissor); cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline.m_Pipeline); cmd.draw(3, 1, 0, 0); cmd.endRendering(); cmd.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe, {}, 0, nullptr, 0, nullptr, 1, &renderToPresentBarrier); result = cmd.end(); ERROR_IF(Failed(result), "Command buffer end failed. Cause: {}", result) THEN_ABORT(result); vk::PipelineStageFlags waitDstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; vk::SubmitInfo submitInfo = { .waitSemaphoreCount = 1, .pWaitSemaphores = ¤tFrame->m_ImageAcquireSem, .pWaitDstStageMask = &waitDstStage, .commandBufferCount = 1, .pCommandBuffers = &cmd, .signalSemaphoreCount = 1, .pSignalSemaphores = ¤tFrame->m_RenderFinishSem, }; result = commandQueue.submit(1, &submitInfo, currentFrame->m_FrameAvailableFence); ERROR_IF(Failed(result), "Command queue submit failed. Cause: {}", result) THEN_ABORT(result); vk::PresentInfoKHR presentInfo = { .waitSemaphoreCount = 1, .pWaitSemaphores = ¤tFrame->m_RenderFinishSem, .swapchainCount = 1, .pSwapchains = &swapchain.m_Swapchain, .pImageIndices = &imageIndex, .pResults = nullptr, }; result = commandQueue.presentKHR(&presentInfo); if (Failed(result)) { switch (result) { case vk::Result::eErrorOutOfDateKHR: case vk::Result::eSuboptimalKHR: INFO("Recreating Swapchain. Cause: {}", result); swapchain.Create(&window); viewport.y = Cast(swapchain.m_Extent.height); viewport.width = Cast(swapchain.m_Extent.width); viewport.height = -Cast(swapchain.m_Extent.height); scissor.extent = swapchain.m_Extent; break; default: ERROR("Command queue present failed. Cause: {}", result) THEN_ABORT(result); } } frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT; } auto result = device.m_Device.waitIdle(); ERROR_IF(Failed(result), "Wait idle failed. Cause: {}", result); return 0; } constexpr QueueSupportFlags REQUIRED_QUEUE_SUPPORT = QueueSupportFlags{} | QueueSupportFlagBits::eGraphics | QueueSupportFlagBits::eCompute | QueueSupportFlagBits::ePresent | QueueSupportFlagBits::eTransfer; PhysicalDevice FindSuitableDevice(const PhysicalDevices &physicalDevices) { for (auto &physicalDevice : physicalDevices) { if (IsSuitableDevice(&physicalDevice)) { return physicalDevice; } } ERROR("No suitable GPU available on the system.") THEN_ABORT(vk::Result::eErrorUnknown); } QueueAllocation FindAppropriateQueueAllocation(const PhysicalDevice *physicalDevice) { for (auto &queueFamilyInfo : physicalDevice->m_QueueFamilies) { if ((queueFamilyInfo.m_Support & REQUIRED_QUEUE_SUPPORT) == REQUIRED_QUEUE_SUPPORT) { return { .m_Family = queueFamilyInfo.m_Index, .m_Count = queueFamilyInfo.m_Count, }; } } ERROR("No suitable queue family on the GPU.") THEN_ABORT(vk::Result::eErrorUnknown); } eastl::optional> ReadFile(cstr fileName) { FILE *filePtr = fopen(fileName, "rb"); if (!filePtr) { ERROR("Invalid read of {}", fileName); return eastl::nullopt; } eastl::vector outputVec; eastl::array buffer{}; usize totalRead = 0; usize readCount; do { readCount = fread(buffer.data(), sizeof(u32), buffer.size(), filePtr); const auto nextSize = totalRead + readCount; outputVec.resize(nextSize); memcpy(outputVec.data() + totalRead, buffer.data(), readCount * sizeof *buffer.data()); totalRead = nextSize; } while (readCount == 1024); return outputVec; } Frame::Frame(const Device *device, const u32 queueFamilyIndex, const u32 frameCount) { m_Device = device; const vk::CommandPoolCreateInfo commandPoolCreateInfo = { .flags = vk::CommandPoolCreateFlagBits::eTransient, .queueFamilyIndex = queueFamilyIndex, }; vk::Result result = device->m_Device.createCommandPool(&commandPoolCreateInfo, nullptr, &m_Pool); ERROR_IF(Failed(result), "Could not command pool for frame {}. Cause: {}", frameCount, result) THEN_ABORT(result); constexpr vk::FenceCreateInfo fenceCreateInfo = {.flags = vk::FenceCreateFlagBits::eSignaled}; result = device->m_Device.createFence(&fenceCreateInfo, nullptr, &m_FrameAvailableFence); ERROR_IF(Failed(result), "Could not create a fence for frame {}. Cause: {}", frameCount, result) THEN_ABORT(result); constexpr vk::SemaphoreCreateInfo semaphoreCreateInfo = {}; result = device->m_Device.createSemaphore(&semaphoreCreateInfo, nullptr, &m_ImageAcquireSem); ERROR_IF(Failed(result), "Could not create IA semaphore for frame {}. Cause: {}", frameCount, result) THEN_ABORT(result); result = device->m_Device.createSemaphore(&semaphoreCreateInfo, nullptr, &m_RenderFinishSem); ERROR_IF(Failed(result), "Could not create RF semaphore for frame {}. Cause: {}", frameCount, result) THEN_ABORT(result); DEBUG("Frame {} created successfully.", frameCount); } vk::CommandBuffer Frame::AllocateCommandBuffer() const { const vk::CommandBufferAllocateInfo allocateInfo = { .commandPool = m_Pool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1}; vk::CommandBuffer commandBuffer; vk::Result result = m_Device->m_Device.allocateCommandBuffers(&allocateInfo, &commandBuffer); ERROR_IF(Failed(result), "Command buffer allocation failed. Cause: {}", result) THEN_ABORT(result); return commandBuffer; } Pipeline CreatePipeline(const Device *device, const Swapchain *swapchain, cstr vertexShaderFile, cstr fragmentShaderFile) { // Pipeline Setup auto vertexShaderModule = CreateShader(device, vertexShaderFile); auto fragmentShaderModule = CreateShader(device, fragmentShaderFile); eastl::array shaderStages = {{ { .stage = vk::ShaderStageFlagBits::eVertex, .module = vertexShaderModule, .pName = "main", }, { .stage = vk::ShaderStageFlagBits::eFragment, .module = fragmentShaderModule, .pName = "main", }, }}; vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo = { .setLayoutCount = 0, .pSetLayouts = nullptr, .pushConstantRangeCount = 0, .pPushConstantRanges = nullptr, }; vk::PipelineLayout pipelineLayout; vk::Result result = device->m_Device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &pipelineLayout); ERROR_IF(Failed(result), "Could not create a pipeline layout. Cause: {}", result) THEN_ABORT(result); device->SetName(pipelineLayout, "Triangle Layout"); vk::PipelineVertexInputStateCreateInfo vertexInputStateCreateInfo = { .vertexBindingDescriptionCount = 0, .pVertexBindingDescriptions = nullptr, .vertexAttributeDescriptionCount = 0, .pVertexAttributeDescriptions = nullptr, }; vk::PipelineInputAssemblyStateCreateInfo inputAssemblyStateCreateInfo = { .topology = vk::PrimitiveTopology::eTriangleList, .primitiveRestartEnable = vk::False, }; vk::PipelineViewportStateCreateInfo viewportStateCreateInfo = { .viewportCount = 1, .scissorCount = 1, }; vk::PipelineRasterizationStateCreateInfo rasterizationStateCreateInfo = { .depthClampEnable = vk::False, .rasterizerDiscardEnable = vk::False, .polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .depthBiasEnable = vk::False, .lineWidth = 1.0, }; vk::PipelineMultisampleStateCreateInfo multisampleStateCreateInfo = { .rasterizationSamples = vk::SampleCountFlagBits::e1, .sampleShadingEnable = vk::False, }; vk::PipelineDepthStencilStateCreateInfo depthStencilStateCreateInfo = { .depthTestEnable = vk::False, .depthWriteEnable = vk::False, }; vk::PipelineColorBlendAttachmentState colorBlendAttachmentState = { .blendEnable = vk::False, .srcColorBlendFactor = vk::BlendFactor::eSrcColor, .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcColor, .colorBlendOp = vk::BlendOp::eAdd, .srcAlphaBlendFactor = vk::BlendFactor::eSrcAlpha, .dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, .alphaBlendOp = vk::BlendOp::eAdd, .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, }; vk::PipelineColorBlendStateCreateInfo colorBlendStateCreateInfo = { .logicOpEnable = vk::False, .attachmentCount = 1, .pAttachments = &colorBlendAttachmentState, }; eastl::array dynamicStates = { vk::DynamicState::eScissor, vk::DynamicState::eViewport, }; vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo = { .dynamicStateCount = Cast(dynamicStates.size()), .pDynamicStates = dynamicStates.data(), }; vk::PipelineRenderingCreateInfo renderingCreateInfo = { .viewMask = 0, .colorAttachmentCount = 1, .pColorAttachmentFormats = &swapchain->m_Format, }; vk::GraphicsPipelineCreateInfo pipelineCreateInfo = { .pNext = &renderingCreateInfo, .stageCount = Cast(shaderStages.size()), .pStages = shaderStages.data(), .pVertexInputState = &vertexInputStateCreateInfo, .pInputAssemblyState = &inputAssemblyStateCreateInfo, .pViewportState = &viewportStateCreateInfo, .pRasterizationState = &rasterizationStateCreateInfo, .pMultisampleState = &multisampleStateCreateInfo, .pDepthStencilState = &depthStencilStateCreateInfo, .pColorBlendState = &colorBlendStateCreateInfo, .pDynamicState = &dynamicStateCreateInfo, .layout = pipelineLayout, }; vk::Pipeline pipeline; result = device->m_Device.createGraphicsPipelines(nullptr, 1, &pipelineCreateInfo, nullptr, &pipeline); ERROR_IF(Failed(result), "Could not create a graphics pipeline. Cause: {}", result) THEN_ABORT(result); device->SetName(pipeline, "Triangle Pipeline"); device->m_Device.destroy(vertexShaderModule, nullptr); device->m_Device.destroy(fragmentShaderModule, nullptr); return {device, pipelineLayout, pipeline}; } vk::ShaderModule CreateShader(const Device *device, cstr shaderFile) { eastl::vector shaderCode; if (auto res = ReadFile(shaderFile)) { shaderCode = res.value(); } else { ERROR("Could not open {}.", shaderFile) THEN_ABORT(-1); } const vk::ShaderModuleCreateInfo shaderModuleCreateInfo = { .codeSize = shaderCode.size() * sizeof(u32), .pCode = shaderCode.data(), }; vk::ShaderModule shaderModule; vk::Result result = device->m_Device.createShaderModule(&shaderModuleCreateInfo, nullptr, &shaderModule); ERROR_IF(Failed(result), "Shader {} could not be created. Cause: {}", shaderFile, result) THEN_ABORT(result); return shaderModule; } Frame::~Frame() { m_Device->m_Device.destroy(m_RenderFinishSem, nullptr); m_Device->m_Device.destroy(m_ImageAcquireSem, nullptr); m_Device->m_Device.destroy(m_FrameAvailableFence, nullptr); m_Device->m_Device.destroy(m_Pool, nullptr); DEBUG("Destoryed Frame"); } bool IsSuitableDevice(const PhysicalDevice *physicalDevice) { const bool hasAllRequiredQueues = std::ranges::any_of(physicalDevice->m_QueueFamilies, [](const auto &queueFamilyProp) { return (queueFamilyProp.m_Support & REQUIRED_QUEUE_SUPPORT) == REQUIRED_QUEUE_SUPPORT; }); const bool isNotCpu = physicalDevice->m_DeviceProperties.deviceType != vk::PhysicalDeviceType::eCpu; const bool hasPresentMode = !physicalDevice->m_PresentModes.empty(); const bool hasSurfaceFormat = !physicalDevice->m_SurfaceFormats.empty(); return hasSurfaceFormat && hasPresentMode && isNotCpu && hasAllRequiredQueues; }