Example #1
0
void foo() {
{
/* [CompareImageFile] */
CORRADE_COMPARE_WITH("actual.png", "expected.png",
    (DebugTools::CompareImageFile{15.5f, 5.0f}));
/* [CompareImageFile] */
}

{
/* [CompareImageFile-manager] */
PluginManager::Manager<Trade::AbstractImporter> manager;

CORRADE_COMPARE_WITH("actual.png", "expected.png",
    (DebugTools::CompareImageFile{manager, 15.5f, 5.0f}));
/* [CompareImageFile-manager] */
}

{
/* [CompareImageFile-skip] */
PluginManager::Manager<Trade::AbstractImporter> manager;
if(manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound ||
   manager.loadState("PngImporter") == PluginManager::LoadState::NotFound)
    CORRADE_SKIP("AnyImageImporter/PngImporter not found, can't compare.");

CORRADE_COMPARE_WITH("actual.png", "expected.png",
    (DebugTools::CompareImageFile{manager, 15.5f, 5.0f}));
/* [CompareImageFile-skip] */
}

{
/* [CompareImageToFile] */
Image2D actual = doProcessing();
CORRADE_COMPARE_WITH(actual, "expected.png",
    (DebugTools::CompareImageToFile{15.5f, 5.0f}));
/* [CompareImageToFile] */

/* [CompareFileToImage] */
Image2D expected = loadExpectedImage();
CORRADE_COMPARE_WITH("actual.png", expected,
    (DebugTools::CompareFileToImage{15.5f, 5.0f}));
/* [CompareFileToImage] */
}
}
Example #2
0
void AnyImporterTest::ogg() {
    if(_manager.loadState("VorbisAudioImporter") == PluginManager::LoadState::NotFound)
        CORRADE_SKIP("VorbisAudioImporter plugin not found, cannot test");

    AnyImporter importer{_manager};
    CORRADE_VERIFY(importer.openFile(OGG_FILE));

    /* Check only parameters, as it is good enough proof that it is working */
    CORRADE_COMPARE(importer.format(), Buffer::Format::Mono16);
    CORRADE_COMPARE(importer.frequency(), 96000);
}
Example #3
0
void AnyImageConverterTest::png() {
    if(_manager.loadState("PngImageConverter") == PluginManager::LoadState::NotFound)
        CORRADE_SKIP("PngImageConverter plugin not found, cannot test");

    if(Utility::Directory::fileExists(PNG_OUTPUT_FILE))
        CORRADE_VERIFY(Utility::Directory::rm(PNG_OUTPUT_FILE));

    /* Just test that the exported file exists */
    AnyImageConverter converter{_manager};
    CORRADE_VERIFY(converter.exportToFile(Image, PNG_OUTPUT_FILE));
    CORRADE_VERIFY(Utility::Directory::fileExists(PNG_OUTPUT_FILE));
}
Example #4
0
bool screenshot(PluginManager::Manager<Trade::AbstractImageConverter>& manager, GL::AbstractFramebuffer& framebuffer, const PixelFormat format, const std::string& filename) {
    Containers::Pointer<Trade::AbstractImageConverter> converter;
    if(!(converter = manager.loadAndInstantiate("AnyImageConverter")))
        return false;

    Image2D image = framebuffer.read(framebuffer.viewport(), {format});
    if(!converter->exportToFile(image, filename))
        return false;

    Debug{} << "DebugTools::screenshot(): saved a" << format << "image of size" << image.size() << "to" << filename;
    return true;
}
void AnyImageImporterTest::png() {
    if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound)
        CORRADE_SKIP("PngImporter plugin not found, cannot test");

    AnyImageImporter importer{_manager};
    CORRADE_VERIFY(importer.openFile(PNG_FILE));

    /* Check only size, as it is good enough proof that it is working */
    std::optional<ImageData2D> image = importer.image2D(0);
    CORRADE_VERIFY(image);
    CORRADE_COMPARE(image->size(), Vector2i(3, 2));
}
Example #6
0
CubeMapExample::CubeMapExample(const Arguments& arguments): Platform::Application(arguments, Configuration().setTitle("Magnum Cube Map Example")) {
    GL::Renderer::enable(GL::Renderer::Feature::DepthTest);
    GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);

    /* Set up perspective camera */
    (_cameraObject = new Object3D(&_scene))
        ->translate(Vector3::zAxis(3.0f));
    (_camera = new SceneGraph::Camera3D(*_cameraObject))
        ->setAspectRatioPolicy(SceneGraph::AspectRatioPolicy::Extend)
        .setProjectionMatrix(Matrix4::perspectiveProjection(Deg(55.0f), 1.0f, 0.001f, 100.0f))
        .setViewport(GL::defaultFramebuffer.viewport().size());

    /* Load image importer plugin */
    PluginManager::Manager<Trade::AbstractImporter> manager;
    Containers::Pointer<Trade::AbstractImporter> importer = manager.loadAndInstantiate("JpegImporter");
    if(!importer) std::exit(1);

    _resourceManager.set<Trade::AbstractImporter>("jpeg-importer",
        importer.release(), ResourceDataState::Final, ResourcePolicy::Manual);

    /* Add objects to scene */
    (new CubeMap(arguments.argc == 2 ? arguments.argv[1] : "", &_scene, &_drawables))
        ->scale(Vector3(20.0f));

    (new Reflector(&_scene, &_drawables))
        ->scale(Vector3(0.5f))
        .translate(Vector3::xAxis(-0.5f));

    (new Reflector(&_scene, &_drawables))
        ->scale(Vector3(0.3f))
        .rotate(Deg(37.0f), Vector3::xAxis())
        .translate(Vector3::xAxis(0.3f));

    /* We don't need the importer anymore */
    _resourceManager.free<Trade::AbstractImporter>();
}
AreaLightsExample::AreaLightsExample(const Arguments& arguments): Platform::Application{arguments, NoCreate} {
    /* Try to create multisampled context, but be nice and fall back if not
       available. Enable only 2x MSAA if we have enough DPI. */
    {
        const Vector2 dpiScaling = this->dpiScaling({});
        Configuration conf;
        conf.setTitle("Magnum Area Lights Example")
            .setSize(conf.size(), dpiScaling);
        GLConfiguration glConf;
        glConf.setSampleCount(dpiScaling.max() < 2.0f ? 8 : 2);
        if(!tryCreate(conf, glConf))
            create(conf, glConf.setSampleCount(0));
    }

    /* Make it all DARK, eanble face culling so one-sided lights are properly
       visualized */
    GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);
    GL::Renderer::setClearColor(0x000000_rgbf);

    /* Setup the plane mesh, which will be used for both the floor and light
       visualization */
    _vertices = GL::Buffer{};
    _vertices.setData(LightVertices, GL::BufferUsage::StaticDraw);
    _plane = GL::Mesh{};
    _plane.setPrimitive(GL::MeshPrimitive::TriangleFan)
        .addVertexBuffer(_vertices, 0, Shaders::Generic3D::Position{}, Shaders::Generic3D::Normal{})
        .setCount(Containers::arraySize(LightVertices));

    /* Setup project and floor plane tranformation matrix */
    _projection = Matrix4::perspectiveProjection(60.0_degf, 4.0f/3.0f, 0.1f, 50.0f);
    _transformation = Matrix4::rotationX(-90.0_degf)*Matrix4::scaling(Vector3{25.0f});

    /* Load LTC matrix and BRDF textures */
    PluginManager::Manager<Trade::AbstractImporter> manager;
    Containers::Pointer<Trade::AbstractImporter> importer = manager.loadAndInstantiate("DdsImporter");
    if(!importer) std::exit(1);

    const Utility::Resource rs{"arealights-data"};
    if(!importer->openData(rs.getRaw("ltc_amp.dds")))
        std::exit(2);

    /* Set texture data and parameters */
    Containers::Optional<Trade::ImageData2D> image = importer->image2D(0);
    CORRADE_INTERNAL_ASSERT(image);
    _ltcAmp = GL::Texture2D{};
    _ltcAmp.setWrapping(GL::SamplerWrapping::ClampToEdge)
        .setMagnificationFilter(GL::SamplerFilter::Linear)
        .setMinificationFilter(GL::SamplerFilter::Linear)
        .setStorage(1, GL::TextureFormat::RG32F, image->size())
        .setSubImage(0, {}, *image);

    if(!importer->openData(rs.getRaw("ltc_mat.dds")))
        std::exit(2);

    /* Set texture data and parameters */
    image = importer->image2D(0);
    CORRADE_INTERNAL_ASSERT(image);
    _ltcMat = GL::Texture2D{};
    _ltcMat.setWrapping(GL::SamplerWrapping::ClampToEdge)
        .setMagnificationFilter(GL::SamplerFilter::Linear)
        .setMinificationFilter(GL::SamplerFilter::Linear)
        .setStorage(1, GL::TextureFormat::RGBA32F, image->size())
        .setSubImage(0, {}, *image);

    /* Compile shaders */
    _areaLightShader = AreaLightShader{};
    _flatShader = Shaders::Flat3D{};

    /* Create the UI */
    _ui.emplace(Vector2{windowSize()}/dpiScaling(), windowSize(), framebufferSize(), Ui::mcssDarkStyleConfiguration(), "ƒ₀");
    Interconnect::connect(*_ui, &Ui::UserInterface::inputWidgetFocused, *this, &AreaLightsExample::startTextInput);
    Interconnect::connect(*_ui, &Ui::UserInterface::inputWidgetBlurred, *this, &AreaLightsExample::stopTextInput);

    /* Base UI plane */
    _baseUiPlane.emplace(*_ui);
    Interconnect::connect(_baseUiPlane->metalness, &Ui::Input::valueChanged, *this, &AreaLightsExample::enableApplyButton);
    Interconnect::connect(_baseUiPlane->roughness, &Ui::Input::valueChanged, *this, &AreaLightsExample::enableApplyButton);
    Interconnect::connect(_baseUiPlane->f0, &Ui::Input::valueChanged, *this, &AreaLightsExample::enableApplyButton);
    Interconnect::connect(_baseUiPlane->apply, &Ui::Button::tapped, *this, &AreaLightsExample::apply);
    Interconnect::connect(_baseUiPlane->reset, &Ui::Button::tapped, *this, &AreaLightsExample::reset);

    /* Apply the default values */
    apply();
}
Example #8
0
int main() {
    /* Create an instance, load function pointers */
    VkInstance instance;
    {
        VkInstanceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateInstance(&info, nullptr, &instance));
    }
    flextVkInitInstance(instance, &flextVkInstance);

    /* Hardcoded memory type index (yes, I'm cheating) */
    constexpr UnsignedInt MemoryTypeIndex = 0;

    /* Create a device */
    VkPhysicalDevice physicalDevice;
    {
        UnsignedInt count = 1;
        MAGNUM_VK_ASSERT_OUTPUT_INCOMPLETE(vkEnumeratePhysicalDevices(instance, &count, &physicalDevice));
        CORRADE_INTERNAL_ASSERT(count == 1);
    } {
        VkPhysicalDeviceProperties properties;
        vkGetPhysicalDeviceProperties(physicalDevice, &properties);
        Debug{} << "Using" << properties.deviceName;
        CORRADE_INTERNAL_ASSERT(properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ||
                                properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ||
                                properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU);
    } {
        VkQueueFamilyProperties properties;
        UnsignedInt count = 1;
        vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &count, &properties);
        CORRADE_INTERNAL_ASSERT(count == 1);
        CORRADE_INTERNAL_ASSERT(properties.queueFlags & VK_QUEUE_GRAPHICS_BIT);
    } {
        VkPhysicalDeviceMemoryProperties properties{};
        vkGetPhysicalDeviceMemoryProperties(physicalDevice, &properties);
        CORRADE_INTERNAL_ASSERT(properties.memoryTypeCount >= 1);
        CORRADE_INTERNAL_ASSERT(properties.memoryHeapCount >= 1);
        CORRADE_INTERNAL_ASSERT(properties.memoryTypes[MemoryTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
        CORRADE_INTERNAL_ASSERT(properties.memoryTypes[MemoryTypeIndex].heapIndex == 0);
    }
    VkDevice device;
    {
        const Float zero = 0.0f;

        VkDeviceQueueCreateInfo queueInfo{};
        queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
        queueInfo.queueFamilyIndex = 0; /* The first family from above */
        queueInfo.queueCount = 1;
        queueInfo.pQueuePriorities = &zero;

        VkDeviceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
        info.queueCreateInfoCount = 1;
        info.pQueueCreateInfos = &queueInfo;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateDevice(physicalDevice, &info, nullptr, &device));
    }

    flextVkInitDevice(device, &flextVkDevice, vkGetDeviceProcAddr);

    /* Create a queue */
    VkQueue queue;
    vkGetDeviceQueue(device, 0, 0, &queue);

    /* Allocate a command buffer */
    VkCommandPool commandPool;
    {
        VkCommandPoolCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
        info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
        info.queueFamilyIndex = 0;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateCommandPool(device, &info, nullptr, &commandPool));
    }
    VkCommandBuffer commandBuffer;
    {
        VkCommandBufferAllocateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        info.commandPool = commandPool;
        info.commandBufferCount = 1;
        MAGNUM_VK_ASSERT_OUTPUT(vkAllocateCommandBuffers(device, &info, &commandBuffer));
    }

    /* Render pass */
    VkRenderPass renderPass;
    {
        VkAttachmentDescription color{};
        color.format = VK_FORMAT_R8G8B8A8_SRGB;
        color.samples = VK_SAMPLE_COUNT_1_BIT;
        color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
        color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
        color.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
        color.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        VkAttachmentReference colorRef{};
        colorRef.attachment = 0; /* color output from the shader */
        colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

        VkSubpassDescription render{};
        render.colorAttachmentCount = 1;
        render.pColorAttachments = &colorRef;

        VkRenderPassCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
        info.attachmentCount = 1;
        info.pAttachments = &color;
        info.subpassCount = 1;
        info.pSubpasses = &render;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateRenderPass(device, &info, nullptr, &renderPass));
    }

    /* Framebuffer image */
    VkImage image;
    {
        VkImageCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
        info.imageType = VK_IMAGE_TYPE_2D;
        info.format = VK_FORMAT_R8G8B8A8_SRGB;
        info.extent = {800, 600, 1};
        info.mipLevels = 1;
        info.arrayLayers = 1;
        info.samples = VK_SAMPLE_COUNT_1_BIT;
        info.tiling = VK_IMAGE_TILING_LINEAR;
        info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
        info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateImage(device, &info, nullptr, &image));
    }
    VkDeviceMemory imageMemory;
    {
        VkMemoryRequirements requirements;
        vkGetImageMemoryRequirements(device, image, &requirements);
        CORRADE_INTERNAL_ASSERT(requirements.size == 800*600*4);

        VkMemoryAllocateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
        info.allocationSize = requirements.size;
        info.memoryTypeIndex = MemoryTypeIndex; /* Assuming the type from above */
        MAGNUM_VK_ASSERT_OUTPUT(vkAllocateMemory(device, &info, nullptr, &imageMemory));
        MAGNUM_VK_ASSERT_OUTPUT(vkBindImageMemory(device, image, imageMemory, 0));
    }
    VkImageView color;
    {
        VkImageViewCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
        info.image = image;
        info.viewType = VK_IMAGE_VIEW_TYPE_2D;
        info.format = VK_FORMAT_R8G8B8A8_SRGB;
        info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        info.subresourceRange.baseMipLevel = 0;
        info.subresourceRange.levelCount = 1;
        info.subresourceRange.baseArrayLayer = 0;
        info.subresourceRange.layerCount = 1;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateImageView(device, &info, nullptr, &color));
    }

    /* Vertex buffer */
    VkBuffer buffer;
    {
        VkBufferCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        info.size = 3*2*4*4; /* Three vertices, each is four-element pos & color */
        info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateBuffer(device, &info, nullptr, &buffer));
    }
    VkDeviceMemory bufferMemory;
    {
        VkMemoryRequirements requirements;
        vkGetBufferMemoryRequirements(device, buffer, &requirements);
        CORRADE_INTERNAL_ASSERT(requirements.size == 3*2*4*4);

        VkMemoryAllocateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
        info.allocationSize = requirements.size;
        info.memoryTypeIndex = MemoryTypeIndex; /* Assuming the type from above */
        MAGNUM_VK_ASSERT_OUTPUT(vkAllocateMemory(device, &info, nullptr, &bufferMemory));
        MAGNUM_VK_ASSERT_OUTPUT(vkBindBufferMemory(device, buffer, bufferMemory, 0));

        /* Fill the data */
        void* data;
        MAGNUM_VK_ASSERT_OUTPUT(vkMapMemory(device, bufferMemory, 0, VK_WHOLE_SIZE, 0, &data));
        auto view = Containers::arrayCast<Vector4>(Containers::arrayView(static_cast<char*>(data), 3*2*4*4));
        view[0] = {-0.5f, -0.5f, 0.0f, 1.0f}; /* Left vertex, red color */
        view[1] = 0xff0000ff_srgbaf;
        view[2] = { 0.5f, -0.5f, 0.0f, 1.0f}; /* Right vertex, green color */
        view[3] = 0x00ff00ff_srgbaf;
        view[4] = { 0.0f,  0.5f, 0.0f, 1.0f}; /* Top vertex, blue color */
        view[5] = 0x0000ffff_srgbaf;
        vkUnmapMemory(device, bufferMemory);
    }

    /* Framebuffer */
    VkFramebuffer framebuffer;
    {
        VkFramebufferCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
        info.renderPass = renderPass;
        info.attachmentCount = 1;
        info.pAttachments = &color;
        info.width = 800;
        info.height = 600;
        info.layers = 1;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateFramebuffer(device, &info, nullptr, &framebuffer));
    }

    /* Create the shader */
    VkShaderModule shader;
    {
        auto o = [](UnsignedShort length, UnsignedShort opcode) {
            return UnsignedInt(length << 16 | opcode);
        };
        auto s = [](const char(&s)[5]) {
            return UnsignedInt(s[3] << 24 | s[2] << 16 | s[1] << 8 | s[0]);
        };
        const UnsignedInt code[]{
            /* I am the generator, I have 66 IDs max, instruction schema is 0 */
            SpvMagicNumber, SpvVersion, 0, 66, 0,

            o(2, SpvOpCapability), SpvCapabilityShader,
            o(3, SpvOpMemoryModel), SpvAddressingModelLogical, SpvMemoryModelGLSL450,

            /* Function %1 is vertex shader and has %12, %13 as input and %15, %16 as output */
            o(8, SpvOpEntryPoint), SpvExecutionModelVertex, 1, s("ver\0"), 12, 13, 15, 16,

            /* Function %2 is fragment shader and has %5 as input and %6 as output */
            o(6, SpvOpEntryPoint), SpvExecutionModelFragment, 2, s("fra\0"), 5, 6,

            /* Input/output layouts */
            o(4, SpvOpDecorate), 12, SpvDecorationLocation, 0, /* in position = 0 */
            o(4, SpvOpDecorate), 13, SpvDecorationLocation, 1, /* in color = 1 */
            o(4, SpvOpDecorate), 15, SpvDecorationBuiltIn, SpvBuiltInPosition, /* out position = gl_Position */
            o(4, SpvOpDecorate), 16, SpvDecorationLocation, 0, /* out color = 0 */
            o(4, SpvOpDecorate), 5, SpvDecorationLocation, 0, /* in color = 0 */
            o(4, SpvOpDecorate), 6, SpvDecorationLocation, 0, /* out color = 0 */

            /* Types */
            o(2, SpvOpTypeVoid), 7,                 /* %7 = void */
            o(3, SpvOpTypeFunction), 8, 7,          /* %8 = void () */
            o(3, SpvOpTypeFloat), 9, 32,            /* %9 = float */
            o(4, SpvOpTypeVector), 10, 9, 4,        /* %10 = vec4 */

            o(4, SpvOpTypePointer), 11, SpvStorageClassInput, 10, /* %11 = in vec4* */
            o(4, SpvOpVariable), 11, 12, SpvStorageClassInput, /* %12 = in position */
            o(4, SpvOpVariable), 11, 13, SpvStorageClassInput, /* %13 = in color */

            o(4, SpvOpTypePointer), 14, SpvStorageClassOutput, 10, /* %14 = out vec4* */
            o(4, SpvOpVariable), 14, 15, SpvStorageClassOutput, /* %15 = out position */
            o(4, SpvOpVariable), 14, 16, SpvStorageClassOutput, /* %16 = out color */

            o(4, SpvOpVariable), 11, 5, SpvStorageClassInput, /* %5 = frag in color */
            o(4, SpvOpVariable), 14, 6, SpvStorageClassOutput, /* %6 = frag out color */

            /* %1 = void ver() */
            o(5, SpvOpFunction), 7, 1, SpvFunctionControlMaskNone, 8,
            o(2, SpvOpLabel), 33,
            o(4, SpvOpLoad), 10, 30, 12,    /* %30 = load position as vec4 */
            o(4, SpvOpLoad), 10, 31, 13,    /* %31 = load color as vec4 */
            o(3, SpvOpStore), 15, 30,       /* store position from %30 */
            o(3, SpvOpStore), 16, 31,       /* store color from %31 */
            o(1, SpvOpReturn),
            o(1, SpvOpFunctionEnd),

            /* %2 = void fra() */
            o(5, SpvOpFunction), 7, 2, SpvFunctionControlMaskNone, 8,
            o(2, SpvOpLabel), 34,
            o(4, SpvOpLoad), 10, 32, 5,     /* %32 = load color as vec4 */
            o(3, SpvOpStore), 6, 32,        /* store color from %32 */
            o(1, SpvOpReturn),
            o(1, SpvOpFunctionEnd)
        };

        VkShaderModuleCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
        info.codeSize = sizeof(code);
        info.pCode = code;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateShaderModule(device, &info, nullptr, &shader));
    }

    /* Pipeline layout */
    VkPipelineLayout pipelineLayout;
    {
        VkPipelineLayoutCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
        info.setLayoutCount = 0;
        info.pushConstantRangeCount = 0;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreatePipelineLayout(device, &info, nullptr, &pipelineLayout));
    }

    /* Create a graphics pipeline */
    VkPipeline pipeline;
    {
        VkVertexInputBindingDescription binding{};
        binding.binding = 0;
        binding.stride = 2*4*4;

        VkVertexInputAttributeDescription attributes[2]{};
        attributes[0].location = 0; /* position attribute */
        attributes[0].binding = binding.binding;
        attributes[0].format = VK_FORMAT_R32G32B32A32_SFLOAT;
        attributes[0].offset = 0;
        attributes[1].location = 1; /* color attribute */
        attributes[1].binding = binding.binding;
        attributes[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
        attributes[1].offset = 4*4;

        VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
        vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
        vertexInputInfo.vertexBindingDescriptionCount = 1;
        vertexInputInfo.pVertexBindingDescriptions = &binding;
        vertexInputInfo.vertexAttributeDescriptionCount = 2;
        vertexInputInfo.pVertexAttributeDescriptions = attributes;

        VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo{};
        inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
        inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

        VkViewport viewport{};
        viewport.width = 800.0f;
        viewport.height = 600.0f;
        viewport.maxDepth = 1.0f;

        VkRect2D scissor{{}, {800, 600}};

        VkPipelineViewportStateCreateInfo viewportInfo{};
        viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
        viewportInfo.viewportCount = 1;
        viewportInfo.pViewports = &viewport;
        viewportInfo.scissorCount = 1;
        viewportInfo.pScissors = &scissor;

        VkPipelineRasterizationStateCreateInfo rasterizationInfo{};
        rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
        rasterizationInfo.lineWidth = 1.0f;
        /* the zero-filled defaults are good enough apparently */

        VkPipelineMultisampleStateCreateInfo multisampleInfo{};
        multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
        multisampleInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

        VkPipelineColorBlendAttachmentState blend{};
        blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT|
                               VK_COLOR_COMPONENT_G_BIT|
                               VK_COLOR_COMPONENT_B_BIT|
                               VK_COLOR_COMPONENT_A_BIT;
        VkPipelineColorBlendStateCreateInfo colorBlendInfo{};
        colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
        colorBlendInfo.attachmentCount = 1;
        colorBlendInfo.pAttachments = &blend;

        VkPipelineShaderStageCreateInfo stages[2]{};
        stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
        stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
        stages[0].module = shader;
        stages[0].pName = "ver";
        stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
        stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
        stages[1].module = shader;
        stages[1].pName = "fra";

        VkGraphicsPipelineCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
        info.stageCount = 2;
        info.pStages = stages;
        info.pVertexInputState = &vertexInputInfo;
        info.pInputAssemblyState = &inputAssemblyInfo;
        info.pViewportState = &viewportInfo;
        info.pRasterizationState = &rasterizationInfo;
        info.pMultisampleState = &multisampleInfo;
        info.pColorBlendState = &colorBlendInfo;
        info.layout = pipelineLayout;
        info.renderPass = renderPass;
        info.subpass = 0;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateGraphicsPipelines(device, nullptr, 1, &info, nullptr, &pipeline));
    }

    /* Begin recording */
    {
        VkCommandBufferBeginInfo info{};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        MAGNUM_VK_ASSERT_OUTPUT(vkBeginCommandBuffer(commandBuffer, &info));
    }

    /* Convert the image to the proper layout */
    {
        VkImageMemoryBarrier barrier{};
        barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
        barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
        barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
        barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.image = image;
        barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        barrier.subresourceRange.baseMipLevel = 0;
        barrier.subresourceRange.levelCount = 1;
        barrier.subresourceRange.baseArrayLayer = 0;
        barrier.subresourceRange.layerCount = 1;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
    }

    /* Begin a render pass, set up clear color */
    {
        VkRenderPassBeginInfo info{};
        info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        info.renderPass = renderPass;
        info.framebuffer = framebuffer;
        info.renderArea = VkRect2D{{}, {800, 600}};
        info.clearValueCount = 1;
        const Color4 color = 0x1f1f1f_srgbf;
        info.pClearValues = reinterpret_cast<const VkClearValue*>(&color);
        vkCmdBeginRenderPass(commandBuffer, &info, VK_SUBPASS_CONTENTS_INLINE);
    }

    /* Bind the pipeline */
    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);

    /* Bind the vertex buffer */
    {
        const VkDeviceSize offset = 0;
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, &buffer, &offset);
    }

    /* Draw the triangle */
    vkCmdDraw(commandBuffer, 3, 1, 0, 0);

    /* End a render pass */
    vkCmdEndRenderPass(commandBuffer);

    /* End recording */
    MAGNUM_VK_ASSERT_OUTPUT(vkEndCommandBuffer(commandBuffer));

    /* Fence to wait on command buffer completeness */
    VkFence fence;
    {
        VkFenceCreateInfo info{};
        info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        MAGNUM_VK_ASSERT_OUTPUT(vkCreateFence(device, &info, nullptr, &fence));
    }

    /* Submit the command buffer */
    {
        VkSubmitInfo info{};
        info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        info.commandBufferCount = 1;
        info.pCommandBuffers = &commandBuffer;
        MAGNUM_VK_ASSERT_OUTPUT(vkQueueSubmit(queue, 1, &info, fence));
    }

    /* Wait until done, 1 second max */
    MAGNUM_VK_ASSERT_OUTPUT(vkWaitForFences(device, 1, &fence, true, 1000000000ull));

    /* Read the image back */
    {
        void* data;
        MAGNUM_VK_ASSERT_OUTPUT(vkMapMemory(device, imageMemory, 0, VK_WHOLE_SIZE, 0, &data));

        ImageView2D image{PixelFormat::RGBA8Unorm, {800, 600}, Containers::arrayView(static_cast<char*>(data), 800*600*4)};
        PluginManager::Manager<Trade::AbstractImageConverter> manager;
        auto converter = manager.loadAndInstantiate("AnyImageConverter");
        CORRADE_INTERNAL_ASSERT(converter);
        converter->exportToFile(image, "image.png");
        Debug{} << "Saved an image to image.png";

        vkUnmapMemory(device, imageMemory);
    }

    /* Clean up */
    vkDestroyPipeline(device, pipeline, nullptr);
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    vkDestroyShaderModule(device, shader, nullptr);
    vkDestroyFramebuffer(device, framebuffer, nullptr);
    vkDestroyBuffer(device, buffer, nullptr);
    vkFreeMemory(device, bufferMemory, nullptr);
    vkDestroyImageView(device, color, nullptr);
    vkDestroyImage(device, image, nullptr);
    vkFreeMemory(device, imageMemory, nullptr);
    vkDestroyRenderPass(device, renderPass, nullptr);
    vkDestroyFence(device, fence, nullptr);
    vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
    vkDestroyCommandPool(device, commandPool, nullptr);
    vkDestroyDevice(device, nullptr);
    vkDestroyInstance(instance, nullptr);
}