← index
libmx2/libmx/vk_tux/tux.cpp
Source: libmx2/libmx/vk_tux/tux.cpp
#include "vk.hpp"
#include "vk_model.hpp"
#include "loadpng.hpp"
#include "SDL.h"
#include "argz.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <fstream>
#include <sstream>
#include <array>

class TuxWindow : public mx::VKWindow {
public:
    TuxWindow(const std::string& path, int wx, int wy, bool full)
        : mx::VKWindow("-[ Vulkan Tux Model ]-", wx, wy, full) {
        setPath(path);
    }

    void initVulkan() override {
        mx::VKWindow::initVulkan();

        sprite = createSprite(util.path + "/data/ant-bg.png",
                              util.path + "/data/sprite_vert.spv",
                              util.path + "/data/fractal_texture_large.spv");

        model.load(util.path + "/data/tux.obj", 1.0f);
        const auto &verts = model.vertices();
        if (!verts.empty()) {
            float minX = verts[0].pos[0], maxX = minX;
            float minY = verts[0].pos[1], maxY = minY;
            float minZ = verts[0].pos[2], maxZ = minZ;
            for (const auto &v : verts) {
                minX = std::min(minX, v.pos[0]); maxX = std::max(maxX, v.pos[0]);
                minY = std::min(minY, v.pos[1]); maxY = std::max(maxY, v.pos[1]);
                minZ = std::min(minZ, v.pos[2]); maxZ = std::max(maxZ, v.pos[2]);
            }
            modelCenterOffset = glm::vec3(
                -0.5f * (minX + maxX),
                -0.5f * (minY + maxY),
                -0.5f * (minZ + maxZ));
            float maxExtent = std::max(maxX - minX, std::max(maxY - minY, maxZ - minZ));
            if (maxExtent > 1e-6f)
                modelRenderScale = 2.5f / maxExtent;
        }

        model.upload(device, physicalDevice, commandPool, graphicsQueue);
        loadModelTextures();
        createModelDescriptorPool();
        createModelDescriptorSets();
    }

    void proc() override {
        if (sprite) {
            float time_f = SDL_GetTicks() / 1000.0f;
            sprite->setShaderParams(time_f, mouseX, mouseY, mousePressed ? 1.0f : 0.0f);
            sprite->drawSpriteRect(0, 0, getWidth(), getHeight());
        }

        std::string info = "Tux Model - Arrow keys/mouse to rotate, +/- zoom, W wireframe, R auto-rotate";
        printText(info, 10, 10, {255, 255, 255, 255});
    }

    void draw() override {
        uint32_t imageIndex = 0;
        VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX,
                                                 imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
        if (result == VK_ERROR_OUT_OF_DATE_KHR) {
            recreateSwapChain();
            return;
        }
        if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
            throw mx::Exception("Failed to acquire swap chain image!");

        vkResetFences(device, 1, &inFlightFence);
        vkResetCommandBuffer(commandBuffers[imageIndex], 0);

        VkCommandBufferBeginInfo beginInfo{};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        if (vkBeginCommandBuffer(commandBuffers[imageIndex], &beginInfo) != VK_SUCCESS)
            throw mx::Exception("Failed to begin recording command buffer!");

        VkRenderPassBeginInfo rpInfo{};
        rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        rpInfo.renderPass = renderPass;
        rpInfo.framebuffer = swapChainFramebuffers[imageIndex];
        rpInfo.renderArea.offset = {0, 0};
        rpInfo.renderArea.extent = swapChainExtent;
        std::array<VkClearValue, 2> clearValues{};
        clearValues[0].color = {{0.05f, 0.05f, 0.1f, 1.0f}};
        clearValues[1].depthStencil = {1.0f, 0};
        rpInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
        rpInfo.pClearValues = clearValues.data();

        VkCommandBuffer cmd = commandBuffers[imageIndex];
        vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);

        VkViewport viewport{};
        viewport.x = 0.0f;
        viewport.y = 0.0f;
        viewport.width  = static_cast<float>(swapChainExtent.width);
        viewport.height = static_cast<float>(swapChainExtent.height);
        viewport.minDepth = 0.0f;
        viewport.maxDepth = 1.0f;
        VkRect2D scissor{{0, 0}, swapChainExtent};
        vkCmdSetViewport(cmd, 0, 1, &viewport);
        vkCmdSetScissor(cmd, 0, 1, &scissor);

        if (spritePipeline != VK_NULL_HANDLE && !sprites.empty()) {
            vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, spritePipeline);
            for (auto &s : sprites) {
                if (s) s->renderSprites(cmd, spritePipelineLayout,
                                        swapChainExtent.width, swapChainExtent.height);
            }
        }


        VkPipeline activePipeline = (useWireFrame && graphicsPipelineMatrix != VK_NULL_HANDLE)
                                    ? graphicsPipelineMatrix : graphicsPipeline;
        vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, activePipeline);


        mx::UniformBufferObject ubo{};
        float time = SDL_GetTicks() / 1000.0f;
        float dt = time - lastTime;
        lastTime = time;
        if (autoRotate)
            autoAngle += dt * glm::radians(45.0f);

        glm::mat4 modelMat = glm::mat4(1.0f);
        modelMat = glm::scale(modelMat, glm::vec3(modelRenderScale));
        modelMat = glm::rotate(modelMat, glm::radians(rotationX), glm::vec3(1.0f, 0.0f, 0.0f));
        modelMat = glm::rotate(modelMat, glm::radians(rotationY) + autoAngle, glm::vec3(0.0f, 1.0f, 0.0f));
        modelMat = glm::translate(modelMat, modelCenterOffset);

        float aspect = static_cast<float>(swapChainExtent.width) / static_cast<float>(swapChainExtent.height);
        ubo.model = modelMat;
        ubo.view  = glm::lookAt(glm::vec3(0.0f, 0.0f, camDist), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
        ubo.proj  = glm::perspective(glm::radians(45.0f), aspect, 0.01f, 1000.0f);
        ubo.proj[1][1] *= -1;  // Vulkan Y-flip
        ubo.params = glm::vec4(time, (float)swapChainExtent.width, (float)swapChainExtent.height, 0.0f);
        ubo.color  = glm::vec4(1.0f);

        if (imageIndex < uniformBuffersMapped.size() && uniformBuffersMapped[imageIndex])
            memcpy(uniformBuffersMapped[imageIndex], &ubo, sizeof(ubo));

        const size_t texCount = std::max<size_t>(1, modelTextures.size());
        if (model.subMeshCount() > 0) {
            for (size_t i = 0; i < model.subMeshCount(); ++i) {
                size_t texIdx = std::min<size_t>(model.subMesh(i).textureIndex, texCount - 1);
                size_t setIdx = static_cast<size_t>(imageIndex) * texCount + texIdx;
                if (setIdx < modelDescriptorSets.size()) {
                    vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
                                            pipelineLayout, 0, 1, &modelDescriptorSets[setIdx], 0, nullptr);
                }
                model.drawSubMesh(cmd, i);
            }
        } else {
            size_t texIdx = std::min<size_t>(model.textureIndex(), texCount - 1);
            size_t setIdx = static_cast<size_t>(imageIndex) * texCount + texIdx;
            if (setIdx < modelDescriptorSets.size()) {
                vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
                                        pipelineLayout, 0, 1, &modelDescriptorSets[setIdx], 0, nullptr);
            }
            model.draw(cmd);
        }

        if (textRenderer && textPipeline != VK_NULL_HANDLE) {
            vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, textPipeline);
            textRenderer->renderText(cmd, textPipelineLayout,
                                     swapChainExtent.width, swapChainExtent.height);
        }

        vkCmdEndRenderPass(cmd);

        if (vkEndCommandBuffer(cmd) != VK_SUCCESS)
            throw mx::Exception("Failed to record command buffer!");

        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        VkSemaphore waitSems[] = {imageAvailableSemaphore};
        VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = waitSems;
        submitInfo.pWaitDstStageMask = waitStages;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &cmd;
        VkSemaphore signalSems[] = {renderFinishedSemaphore};
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = signalSems;

        if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS)
            throw mx::Exception("Failed to submit draw command buffer!");

        VkPresentInfoKHR presentInfo{};
        presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        presentInfo.waitSemaphoreCount = 1;
        presentInfo.pWaitSemaphores = signalSems;
        presentInfo.swapchainCount = 1;
        presentInfo.pSwapchains = &swapChain;
        presentInfo.pImageIndices = &imageIndex;
        result = vkQueuePresentKHR(presentQueue, &presentInfo);
        if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
            recreateSwapChain();
        } else if (result != VK_SUCCESS) {
            throw mx::Exception("Failed to present swap chain image!");
        }

        vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX);
        clearTextQueue();
        for (auto &s : sprites) {
            if (s) s->clearQueue();
        }
    }

    void event(SDL_Event& e) override {
        if (e.type == SDL_QUIT) { quit(); return; }
        if (e.type == SDL_KEYDOWN) {
            switch (e.key.keysym.sym) {
                case SDLK_ESCAPE: quit(); break;
                case SDLK_w:
                    useWireFrame = !useWireFrame;
                    break;
                case SDLK_r:
                    autoRotate = !autoRotate;
                    break;
                case SDLK_UP:    rotationX -= 5.0f; break;
                case SDLK_DOWN:  rotationX += 5.0f; break;
                case SDLK_LEFT:  rotationY -= 5.0f; break;
                case SDLK_RIGHT: rotationY += 5.0f; break;
                case SDLK_EQUALS: case SDLK_PLUS:
                    camDist = std::max(0.1f, camDist - 0.5f); break;
                case SDLK_MINUS:
                    camDist = std::min(100.0f, camDist + 0.5f); break;
                case SDLK_HOME:
                    rotationX = 0; rotationY = 0; camDist = 5.0f; autoAngle = 0; break;
            }
        }
        if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) {
            mouseDown = true;
            mousePressed = true;
            lastMouseX = e.button.x;
            lastMouseY = e.button.y;
            mouseX = static_cast<float>(e.button.x);
            mouseY = static_cast<float>(e.button.y);
        }
        if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) {
            mouseDown = false;
            mousePressed = false;
        }
        if (e.type == SDL_MOUSEMOTION) {
            mouseX = static_cast<float>(e.motion.x);
            mouseY = static_cast<float>(e.motion.y);
            if (mouseDown) {
                rotationY += (e.motion.x - lastMouseX) * 0.5f;
                rotationX += (e.motion.y - lastMouseY) * 0.5f;
                rotationX = glm::clamp(rotationX, -89.0f, 89.0f);
                lastMouseX = e.motion.x;
                lastMouseY = e.motion.y;
            }
        }
        if (e.type == SDL_MOUSEWHEEL) {
            camDist -= e.wheel.y * 0.5f;
            camDist = glm::clamp(camDist, 0.1f, 100.0f);
        }
    }

    void onResize() override {
        if (modelDescriptorPool != VK_NULL_HANDLE) {
            vkDestroyDescriptorPool(device, modelDescriptorPool, nullptr);
            modelDescriptorPool = VK_NULL_HANDLE;
        }
        modelDescriptorSets.clear();
        createModelDescriptorPool();
        createModelDescriptorSets();
    }

    void cleanup() override {
        vkDeviceWaitIdle(device);
        model.cleanup(device);
        for (auto &t : modelTextures) {
            if (t.view   != VK_NULL_HANDLE) vkDestroyImageView(device, t.view, nullptr);
            if (t.image  != VK_NULL_HANDLE) vkDestroyImage(device, t.image, nullptr);
            if (t.memory != VK_NULL_HANDLE) vkFreeMemory(device, t.memory, nullptr);
        }
        modelTextures.clear();
        if (modelDescriptorPool != VK_NULL_HANDLE)
            vkDestroyDescriptorPool(device, modelDescriptorPool, nullptr);
        mx::VKWindow::cleanup();
    }

private:
    struct TexEntry {
        VkImage        image  = VK_NULL_HANDLE;
        VkDeviceMemory memory = VK_NULL_HANDLE;
        VkImageView    view   = VK_NULL_HANDLE;
        uint32_t       w = 0, h = 0;
    };

    void loadModelTextures() {
        std::string texPath = util.getFilePath("data/tux.tex");
        std::ifstream tf(texPath);
        if (!tf.is_open())
            throw mx::Exception("Failed to open .tex file: " + texPath);

        std::string prefix = util.path + "/data";
        std::vector<std::string> imagePaths;
        std::string line;
        while (std::getline(tf, line)) {
            size_t b = line.find_first_not_of(" \t\r\n");
            if (b == std::string::npos) continue;
            size_t e = line.find_last_not_of(" \t\r\n");
            line = line.substr(b, e - b + 1);
            if (line.empty() || line[0] == '#') continue;
            imagePaths.push_back(prefix + "/" + line);
        }
        if (imagePaths.empty())
            throw mx::Exception("No textures in .tex file");

        VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM;
        for (size_t i = 0; i < imagePaths.size(); ++i) {
            SDL_Surface *img = png::LoadPNG(imagePaths[i].c_str());
            if (!img) {
                img = SDL_CreateRGBSurfaceWithFormat(0, 1, 1, 32, SDL_PIXELFORMAT_RGBA32);
                if (img) { uint32_t *px = (uint32_t*)img->pixels; *px = 0xFFFFFFFF; }
                else throw mx::Exception("Failed to create placeholder surface");
            }

            TexEntry tex;
            tex.w = img->w;
            tex.h = img->h;
            VkDeviceSize imageSize = tex.w * tex.h * 4;

            createImage(tex.w, tex.h, fmt, VK_IMAGE_TILING_OPTIMAL,
                        VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, tex.image, tex.memory);

            transitionImageLayout(tex.image, fmt, VK_IMAGE_LAYOUT_UNDEFINED,
                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

            VkBuffer staging; VkDeviceMemory stagingMem;
            createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                         staging, stagingMem);
            void *data;
            vkMapMemory(device, stagingMem, 0, imageSize, 0, &data);
            memcpy(data, img->pixels, (size_t)imageSize);
            vkUnmapMemory(device, stagingMem);
            copyBufferToImage(staging, tex.image, tex.w, tex.h);

            transitionImageLayout(tex.image, fmt, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                                  VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

            vkDestroyBuffer(device, staging, nullptr);
            vkFreeMemory(device, stagingMem, nullptr);
            SDL_FreeSurface(img);

            tex.view = createImageView(tex.image, fmt, VK_IMAGE_ASPECT_COLOR_BIT);
            modelTextures.push_back(tex);
        }
    }

    void createModelDescriptorPool() {
        const uint32_t texCount = std::max<uint32_t>(1u, static_cast<uint32_t>(modelTextures.size()));
        const uint32_t setCount = static_cast<uint32_t>(swapChainImages.size()) * texCount;

        std::array<VkDescriptorPoolSize, 2> sizes{};
        sizes[0] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, setCount};
        sizes[1] = {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, setCount};

        VkDescriptorPoolCreateInfo ci{};
        ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
        ci.poolSizeCount = static_cast<uint32_t>(sizes.size());
        ci.pPoolSizes = sizes.data();
        ci.maxSets = setCount;
        if (vkCreateDescriptorPool(device, &ci, nullptr, &modelDescriptorPool) != VK_SUCCESS)
            throw mx::Exception("Failed to create model descriptor pool!");
    }

    void createModelDescriptorSets() {
        const size_t texCount = std::max<size_t>(1, modelTextures.size());
        const size_t setCount = swapChainImages.size() * texCount;

        std::vector<VkDescriptorSetLayout> layouts(setCount, descriptorSetLayout);
        VkDescriptorSetAllocateInfo ai{};
        ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
        ai.descriptorPool = modelDescriptorPool;
        ai.descriptorSetCount = static_cast<uint32_t>(setCount);
        ai.pSetLayouts = layouts.data();
        modelDescriptorSets.resize(setCount);
        if (vkAllocateDescriptorSets(device, &ai, modelDescriptorSets.data()) != VK_SUCCESS)
            throw mx::Exception("Failed to allocate model descriptor sets!");

        for (size_t frame = 0; frame < swapChainImages.size(); ++frame) {
            VkDescriptorBufferInfo bufInfo{};
            bufInfo.buffer = uniformBuffers[frame];
            bufInfo.offset = 0;
            bufInfo.range  = sizeof(mx::UniformBufferObject);

            for (size_t tex = 0; tex < texCount; ++tex) {
                size_t setIndex = frame * texCount + tex;

                VkDescriptorImageInfo imgInfo{};
                imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
                imgInfo.imageView   = modelTextures[tex].view;
                imgInfo.sampler     = textureSampler;

                std::array<VkWriteDescriptorSet, 2> writes{};
                writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
                writes[0].dstSet = modelDescriptorSets[setIndex];
                writes[0].dstBinding = 0;
                writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
                writes[0].descriptorCount = 1;
                writes[0].pImageInfo = &imgInfo;

                writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
                writes[1].dstSet = modelDescriptorSets[setIndex];
                writes[1].dstBinding = 1;
                writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
                writes[1].descriptorCount = 1;
                writes[1].pBufferInfo = &bufInfo;

                vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()),
                                       writes.data(), 0, nullptr);
            }
        }
    }

    mx::VKSprite* sprite = nullptr;
    mx::MXModel model;

    std::vector<TexEntry> modelTextures;
    VkDescriptorPool modelDescriptorPool = VK_NULL_HANDLE;
    std::vector<VkDescriptorSet> modelDescriptorSets;

    float rotationX = 0.0f, rotationY = 0.0f;
    float camDist = 5.0f;
    float autoAngle = 0.0f;
    float lastTime = 0.0f;
    float modelRenderScale = 1.0f;
    glm::vec3 modelCenterOffset = glm::vec3(0.0f);
    bool autoRotate = true;
    bool mouseDown = false;
    bool mousePressed = false;
    float mouseX = 0.0f, mouseY = 0.0f;
    int lastMouseX = 0, lastMouseY = 0;

    void textQuads_clear() {
        clearTextQueue();
        for (auto &s : sprites) {
            if (s) s->clearQueue();
        }
    }
};

int main(int argc, char **argv) {
    Arguments args = proc_args(argc, argv);
    try {
        TuxWindow window(args.path, args.width, args.height, args.fullscreen);
        window.initVulkan();
        window.loop();
        window.cleanup();
    } catch (mx::Exception &e) {
        SDL_Log("mx: Exception: %s\n", e.text().c_str());
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}