procedural-3d-engine/examples/dynamicrenderingmultisampling/dynamicrenderingmultisampling.cpp
Wenqing Li 214a45099d
Update the old layout of swapchain images
The swapchain image is already used as the resolve target with the general image layout, after that we should specify the old layout as VK_IMAGE_LAYOUT_GENERAL instead of VK_IMAGE_LAYOUT_UNDEFINED.
2025-07-08 12:00:57 +08:00

401 lines
19 KiB
C++

/*
* Vulkan Example - Using Multi sampling with VK_KHR_dynamic_rendering
*
* Copyright (C) 2025 by Sascha Willems - www.saschawillems.de
*
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
*/
#include "vulkanexamplebase.h"
#include "VulkanglTFModel.h"
class VulkanExample : public VulkanExampleBase
{
public:
PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR{ VK_NULL_HANDLE };
PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR{ VK_NULL_HANDLE };
VkPhysicalDeviceDynamicRenderingFeaturesKHR enabledDynamicRenderingFeaturesKHR{};
vkglTF::Model model;
const VkSampleCountFlagBits multiSampleCount = VK_SAMPLE_COUNT_4_BIT;
struct UniformData {
glm::mat4 projection;
glm::mat4 modelView;
glm::vec4 viewPos;
} uniformData;
vks::Buffer uniformBuffer;
VkPipeline pipeline{ VK_NULL_HANDLE };
VkPipelineLayout pipelineLayout{ VK_NULL_HANDLE };
VkDescriptorSet descriptorSet{ VK_NULL_HANDLE };
VkDescriptorSetLayout descriptorSetLayout{ VK_NULL_HANDLE };
// Intermediate images used for multi sampling
struct Image {
VkImage image{ VK_NULL_HANDLE };
VkImageView view{ VK_NULL_HANDLE };
VkDeviceMemory memory{ VK_NULL_HANDLE };
};
Image renderImage;
VulkanExample() : VulkanExampleBase()
{
title = "Multi sampling with dynamic rendering";
camera.type = Camera::CameraType::lookat;
camera.setPosition(glm::vec3(0.0f, 0.0f, -10.0f));
camera.setRotation(glm::vec3(-7.5f, 72.0f, 0.0f));
camera.setPerspective(60.0f, (float)width / (float)height, 0.1f, 256.0f);
settings.overlay = false;
enabledInstanceExtensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
// The sample uses the extension (instead of Vulkan 1.2, where dynamic rendering is core)
enabledDeviceExtensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME);
enabledDeviceExtensions.push_back(VK_KHR_MAINTENANCE2_EXTENSION_NAME);
enabledDeviceExtensions.push_back(VK_KHR_MULTIVIEW_EXTENSION_NAME);
enabledDeviceExtensions.push_back(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME);
enabledDeviceExtensions.push_back(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME);
// in addition to the extension, the feature needs to be explicitly enabled too by chaining the extension structure into device creation
enabledDynamicRenderingFeaturesKHR.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR;
enabledDynamicRenderingFeaturesKHR.dynamicRendering = VK_TRUE;
deviceCreatepNextChain = &enabledDynamicRenderingFeaturesKHR;
}
~VulkanExample() override
{
if (device) {
vkDestroyPipeline(device, pipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
uniformBuffer.destroy();
vkDestroyImage(device, renderImage.image, nullptr);
vkDestroyImageView(device, renderImage.view, nullptr);
vkFreeMemory(device, renderImage.memory, nullptr);
}
}
void setupRenderPass() override
{
// With VK_KHR_dynamic_rendering we no longer need a render pass, so we can skip the sample base render pass setup
renderPass = VK_NULL_HANDLE;
}
void setupFrameBuffer() override
{
// With VK_KHR_dynamic_rendering we no longer need a frame buffer, so we can so skip the sample base framebuffer setup
// For multi sampling we need intermediate images that are then resolved to the final presentation image
vkDestroyImage(device, renderImage.image, nullptr);
vkDestroyImageView(device, renderImage.view, nullptr);
vkFreeMemory(device, renderImage.memory, nullptr);
VkImageCreateInfo renderImageCI = vks::initializers::imageCreateInfo();
renderImageCI.imageType = VK_IMAGE_TYPE_2D;
renderImageCI.format = swapChain.colorFormat;
renderImageCI.extent = { width, height, 1 };
renderImageCI.mipLevels = 1;
renderImageCI.arrayLayers = 1;
renderImageCI.samples = multiSampleCount;
renderImageCI.tiling = VK_IMAGE_TILING_OPTIMAL;
renderImageCI.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
renderImageCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VK_CHECK_RESULT(vkCreateImage(device, &renderImageCI, nullptr, &renderImage.image));
VkMemoryRequirements memReqs{};
vkGetImageMemoryRequirements(device, renderImage.image, &memReqs);
VkMemoryAllocateInfo memAllloc{};
memAllloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memAllloc.allocationSize = memReqs.size;
memAllloc.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAllloc, nullptr, &renderImage.memory));
VK_CHECK_RESULT(vkBindImageMemory(device, renderImage.image, renderImage.memory, 0));
VkImageViewCreateInfo imageViewCI = vks::initializers::imageViewCreateInfo();
imageViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
imageViewCI.image = renderImage.image;
imageViewCI.format = swapChain.colorFormat;
imageViewCI.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
VK_CHECK_RESULT(vkCreateImageView(device, &imageViewCI, nullptr, &renderImage.view));
}
// We need to override the default depth/stencil setup to create a depth image that supports multi sampling
void setupDepthStencil() override
{
VkImageCreateInfo imageCI{};
imageCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCI.imageType = VK_IMAGE_TYPE_2D;
imageCI.format = depthFormat;
imageCI.extent = { width, height, 1 };
imageCI.mipLevels = 1;
imageCI.arrayLayers = 1;
imageCI.samples = multiSampleCount;
imageCI.tiling = VK_IMAGE_TILING_OPTIMAL;
imageCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
VK_CHECK_RESULT(vkCreateImage(device, &imageCI, nullptr, &depthStencil.image));
VkMemoryRequirements memReqs{};
vkGetImageMemoryRequirements(device, depthStencil.image, &memReqs);
VkMemoryAllocateInfo memAllloc{};
memAllloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memAllloc.allocationSize = memReqs.size;
memAllloc.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAllloc, nullptr, &depthStencil.memory));
VK_CHECK_RESULT(vkBindImageMemory(device, depthStencil.image, depthStencil.memory, 0));
VkImageViewCreateInfo depthImageViewCI{};
depthImageViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
depthImageViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
depthImageViewCI.image = depthStencil.image;
depthImageViewCI.format = depthFormat;
depthImageViewCI.subresourceRange.baseMipLevel = 0;
depthImageViewCI.subresourceRange.levelCount = 1;
depthImageViewCI.subresourceRange.baseArrayLayer = 0;
depthImageViewCI.subresourceRange.layerCount = 1;
depthImageViewCI.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
// Stencil aspect should only be set on depth + stencil formats (VK_FORMAT_D16_UNORM_S8_UINT..VK_FORMAT_D32_SFLOAT_S8_UINT
if (depthFormat >= VK_FORMAT_D16_UNORM_S8_UINT) {
depthImageViewCI.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
}
VK_CHECK_RESULT(vkCreateImageView(device, &depthImageViewCI, nullptr, &depthStencil.view));
}
// Enable physical device features required for this example
void getEnabledFeatures() override
{
// Enable anisotropic filtering if supported
if (deviceFeatures.samplerAnisotropy) {
enabledFeatures.samplerAnisotropy = VK_TRUE;
};
}
void loadAssets()
{
const uint32_t glTFLoadingFlags = vkglTF::FileLoadingFlags::PreTransformVertices | vkglTF::FileLoadingFlags::PreMultiplyVertexColors | vkglTF::FileLoadingFlags::FlipY;
model.loadFromFile(getAssetPath() + "models/voyager.gltf", vulkanDevice, queue, glTFLoadingFlags);
}
void buildCommandBuffers() override
{
VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
for (int32_t i = 0; i < drawCmdBuffers.size(); ++i)
{
VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo));
// With dynamic rendering there are no subpass dependencies, so we need to take care of proper layout transitions by using barriers
// This set of barriers prepares the color and depth images for output
vks::tools::insertImageMemoryBarrier(
drawCmdBuffers[i],
renderImage.image,
0,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
vks::tools::insertImageMemoryBarrier(
drawCmdBuffers[i],
depthStencil.image,
0,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT, 0, 1, 0, 1 });
// New structures are used to define the attachments used in dynamic rendering
VkRenderingAttachmentInfoKHR colorAttachment{};
colorAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
colorAttachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.clearValue.color = { 0.0f,0.0f,0.0f,0.0f };
// When multi sampling is used, we use intermediate images to render and resolve to the swap chain images
colorAttachment.imageView = renderImage.view;
colorAttachment.resolveMode = VK_RESOLVE_MODE_AVERAGE_BIT;
colorAttachment.resolveImageView = swapChain.imageViews[i];
colorAttachment.resolveImageLayout = VK_IMAGE_LAYOUT_GENERAL;
// A single depth stencil attachment info can be used, but they can also be specified separately.
// When both are specified separately, the only requirement is that the image view is identical.
VkRenderingAttachmentInfoKHR depthStencilAttachment{};
depthStencilAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
depthStencilAttachment.imageView = depthStencil.view;
depthStencilAttachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depthStencilAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthStencilAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depthStencilAttachment.clearValue.depthStencil = { 1.0f, 0 };
VkRenderingInfoKHR renderingInfo{};
renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR;
renderingInfo.renderArea = { 0, 0, width, height };
renderingInfo.layerCount = 1;
renderingInfo.colorAttachmentCount = 1;
renderingInfo.pColorAttachments = &colorAttachment;
renderingInfo.pDepthAttachment = &depthStencilAttachment;
renderingInfo.pStencilAttachment = &depthStencilAttachment;
// Begin dynamic rendering
vkCmdBeginRenderingKHR(drawCmdBuffers[i], &renderingInfo);
VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f);
vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport);
VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0);
vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor);
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
model.draw(drawCmdBuffers[i], vkglTF::RenderFlags::BindImages, pipelineLayout);
drawUI(drawCmdBuffers[i]);
// End dynamic rendering
vkCmdEndRenderingKHR(drawCmdBuffers[i]);
// This set of barriers prepares the color image for presentation, we don't need to care for the depth image
vks::tools::insertImageMemoryBarrier(
drawCmdBuffers[i],
swapChain.images[i],
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
0,
VK_IMAGE_LAYOUT_GENERAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i]));
}
}
void setupDescriptors()
{
// Pool
std::vector<VkDescriptorPoolSize> poolSizes = {
vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1),
};
VkDescriptorPoolCreateInfo descriptorPoolInfo = vks::initializers::descriptorPoolCreateInfo(poolSizes, 1);
VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool));
// Layout
const std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
// Binding 0 : Vertex shader uniform buffer
vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0),
};
VkDescriptorSetLayoutCreateInfo descriptorLayout = vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings);
VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout));
// Set
VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayout, 1);
VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet));
std::vector<VkWriteDescriptorSet> writeDescriptorSets = {
// Binding 0 : Vertex shader uniform buffer
vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBuffer.descriptor),
};
vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr);
}
void preparePipelines()
{
// Layout
// Uses set 0 for passing vertex shader ubo and set 1 for fragment shader images (taken from glTF model)
const std::vector<VkDescriptorSetLayout> setLayouts = {
descriptorSetLayout,
vkglTF::descriptorSetLayoutImage,
};
VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(setLayouts.data(), 2);
VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout));
// Pipeline
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE);
VkPipelineRasterizationStateCreateInfo rasterizationState = vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, 0);
VkPipelineColorBlendAttachmentState blendAttachmentState = vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE);
VkPipelineColorBlendStateCreateInfo colorBlendState = vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentState);
VkPipelineDepthStencilStateCreateInfo depthStencilState = vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL);
VkPipelineViewportStateCreateInfo viewportState = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0);
VkPipelineMultisampleStateCreateInfo multisampleState = vks::initializers::pipelineMultisampleStateCreateInfo(multiSampleCount, 0);
std::vector<VkDynamicState> dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
VkPipelineDynamicStateCreateInfo dynamicState = vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables);
std::array<VkPipelineShaderStageCreateInfo, 2> shaderStages{};
// We no longer need to set a renderpass for the pipeline create info
VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo();
pipelineCI.layout = pipelineLayout;
pipelineCI.pInputAssemblyState = &inputAssemblyState;
pipelineCI.pRasterizationState = &rasterizationState;
pipelineCI.pColorBlendState = &colorBlendState;
pipelineCI.pMultisampleState = &multisampleState;
pipelineCI.pViewportState = &viewportState;
pipelineCI.pDepthStencilState = &depthStencilState;
pipelineCI.pDynamicState = &dynamicState;
pipelineCI.stageCount = static_cast<uint32_t>(shaderStages.size());
pipelineCI.pStages = shaderStages.data();
pipelineCI.pVertexInputState = vkglTF::Vertex::getPipelineVertexInputState({ vkglTF::VertexComponent::Position, vkglTF::VertexComponent::Normal, vkglTF::VertexComponent::UV });
// New create info to define color, depth and stencil attachments at pipeline create time
VkPipelineRenderingCreateInfoKHR pipelineRenderingCreateInfo{};
pipelineRenderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
pipelineRenderingCreateInfo.colorAttachmentCount = 1;
pipelineRenderingCreateInfo.pColorAttachmentFormats = &swapChain.colorFormat;
pipelineRenderingCreateInfo.depthAttachmentFormat = depthFormat;
pipelineRenderingCreateInfo.stencilAttachmentFormat = depthFormat;
// Chain into the pipeline creat einfo
pipelineCI.pNext = &pipelineRenderingCreateInfo;
shaderStages[0] = loadShader(getShadersPath() + "dynamicrendering/texture.vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
shaderStages[1] = loadShader(getShadersPath() + "dynamicrendering/texture.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);
VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipeline));
}
// Prepare and initialize uniform buffer containing shader uniforms
void prepareUniformBuffers()
{
VK_CHECK_RESULT(vulkanDevice->createBuffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &uniformBuffer, sizeof(uniformData), &uniformData));
VK_CHECK_RESULT(uniformBuffer.map());
updateUniformBuffers();
}
void updateUniformBuffers()
{
uniformData.projection = camera.matrices.perspective;
uniformData.modelView = camera.matrices.view;
uniformData.viewPos = camera.viewPos;
memcpy(uniformBuffer.mapped, &uniformData, sizeof(uniformData));
}
void prepare() override
{
VulkanExampleBase::prepare();
// Since we use an extension, we need to expliclity load the function pointers for extension related Vulkan commands
vkCmdBeginRenderingKHR = reinterpret_cast<PFN_vkCmdBeginRenderingKHR>(vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR"));
vkCmdEndRenderingKHR = reinterpret_cast<PFN_vkCmdEndRenderingKHR>(vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR"));
loadAssets();
prepareUniformBuffers();
setupDescriptors();
preparePipelines();
buildCommandBuffers();
prepared = true;
}
void draw()
{
VulkanExampleBase::prepareFrame();
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
VulkanExampleBase::submitFrame();
}
void render() override
{
if (!prepared)
return;
updateUniformBuffers();
draw();
}
};
VULKAN_EXAMPLE_MAIN()