← index
libmx2/libmx/vk_cross_stars_frag/skeleton.cpp
Source: libmx2/libmx/vk_cross_stars_frag/skeleton.cpp
#include <SDL2/SDL.h>
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
#include "argz.hpp"
#endif

#include "mxcross.hpp"
#include "loadpng.hpp"
#include <random>
#include <format>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

struct Star {
    float x, y, z;
    float vx, vy, vz;
    float magnitude;
    float temperature;
    float twinkle;
    float size;
    float sizeFactor;  
    int starType;
    bool isCore;
    bool active;
    int textureIndex;  
    bool isConstellation;
};

struct StarVertex {
    float pos[3];
    float size;
    float color[4];
};

float generateRandomFloat(float min, float max) {
    static std::random_device rd;
    static std::default_random_engine eng(rd());
    std::uniform_real_distribution<float> dist(min, max);
    return dist(eng);
}

glm::vec3 getStarColor(float temperature) {
    float r, g, b;
    
    if (temperature < 3700) {
        r = 1.0f;
        g = temperature / 3700.0f * 0.6f;
        b = 0.0f;
    } else if (temperature < 5200) {
        r = 1.0f;
        g = 0.6f + (temperature - 3700) / 1500.0f * 0.4f;
        b = (temperature - 3700) / 1500.0f * 0.3f;
    } else if (temperature < 6000) {
        r = 1.0f;
        g = 1.0f;
        b = (temperature - 5200) / 800.0f * 0.7f;
    } else if (temperature < 7500) {
        r = 1.0f;
        g = 1.0f;
        b = 0.7f + (temperature - 6000) / 1500.0f * 0.3f;
    } else {
        r = 0.7f - (temperature - 7500) / 10000.0f * 0.4f;
        g = 0.8f + (temperature - 7500) / 10000.0f * 0.2f;
        b = 1.0f;
    }
    
    return glm::vec3(r, g, b);
}

float magnitudeToSize(float magnitude) {
    return glm::clamp(35.0f - magnitude * 3.0f, 2.0f, 35.0f);
}

float magnitudeToAlpha(float magnitude, float lightPollution) {
    float alpha = (6.5f - magnitude) / 6.5f;
    return glm::clamp(alpha - lightPollution, 0.9f, 1.0f);
}

constexpr int NUM_STARS = 20000;
constexpr int NUM_CORE = NUM_STARS / 8; 
#ifdef WITH_GL

#ifndef __EMSCRIPTEN__
const char* glStarVertSource = R"(#version 330 core
layout (location = 0) in vec3 inPosition; 
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;  

out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* glStarFragSource = R"(#version 330 core
in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    vec2 coord = gl_PointCoord;
    vec4 texColor = texture(spriteTexture, coord);

    if(texColor.r < 0.5 && texColor.g < 0.5 && texColor.b < 0.5)
        discard;

    FragColor = texColor * fragColor;
    if (FragColor.a < 0.01)
        discard;
}
)";
#else
const char* glStarVertSource = R"(#version 300 es
precision highp float;
layout (location = 0) in vec3 inPosition;
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;
out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* glStarFragSource = R"(#version 300 es
precision highp float;
in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    vec2 coord = gl_PointCoord;
    vec4 texColor = texture(spriteTexture, coord);
    if(texColor.r < 0.5 && texColor.g < 0.5 && texColor.b < 0.5)
        discard;

    FragColor = texColor * fragColor;
    if (FragColor.a < 0.01)
        discard;
}
)";
#endif

class GL_Starfield {
public:
    gl::ShaderProgram program;
    GLuint VBO[3], VAO;
    GLuint EBO = 0;  
    GLuint starTexture = 0;
    GLuint starTexture2 = 0;
    std::vector<GLuint> tex1Indices;
    std::vector<GLuint> tex2Indices;
    Star stars[NUM_STARS];
    bool initialized = false;
    Uint32 lastUpdateTime = 0;
    
    float cameraX = 0.0f, cameraY = 0.0f, cameraZ = 0.0f;
    float cameraYaw = -90.0f, cameraPitch = 0.0f;
    float cameraSpeed = 50.0f;
    float atmosphericTwinkle = 0.5f;
    float lightPollution = 0.0f;
    float warpSpeed = 175.0f;         
    float starFieldRadius = 500.0f;  
    float starFieldDepth = 1000.0f;  
    const Uint8* keyboardState = nullptr;

    void init(const std::string& dataPath) {
        if (initialized) return;
        
        keyboardState = SDL_GetKeyboardState(nullptr);
        
        if (!program.loadProgramFromText(glStarVertSource, glStarFragSource)) {
            throw mx::Exception("Failed to load GL star shaders");
        }
        
        std::string texPath = dataPath + "/data/star.png";
        starTexture = gl::loadTexture(texPath);
        if (!starTexture) {
            SDL_Log("Warning: Could not load star texture from %s", texPath.c_str());
        }
        
        std::string texPath2 = dataPath + "/data/star.png";
        starTexture2 = gl::loadTexture(texPath2);
        if (!starTexture2) {
            SDL_Log("Warning: Could not load star texture2 from %s", texPath2.c_str());
        }
        
        initStars();
        
        glGenVertexArrays(1, &VAO);
        glGenBuffers(3, VBO);
        
        glBindVertexArray(VAO);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(0);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(1);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(2);
        
        glBindVertexArray(0);
        
        lastUpdateTime = SDL_GetTicks();
        initialized = true;
        SDL_Log("GL Starfield initialized with %d stars", NUM_STARS);
    }

    void initStar(Star& star, bool randomZ = true, bool core = false) {
        float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
        star.z = randomZ ? generateRandomFloat(-300.0f, -10.0f) : -300.0f;
        float r = sqrt(generateRandomFloat(0.0f, 1.0f)) * starFieldRadius * 0.3f;

        star.x = r * cos(theta);
        star.y = r * sin(theta);

        star.isCore = core;
        star.active = true;

        star.vx = generateRandomFloat(-1.0f, 1.0f);
        star.vy = generateRandomFloat(-1.0f, 1.0f);
        star.vz = warpSpeed;
        
        float rand = generateRandomFloat(0.0f, 1.0f);
        if (rand < 0.05f) {
            star.magnitude = generateRandomFloat(-1.5f, 1.5f);
            star.starType = 0;
        } else if (rand < 0.25f) {
            star.magnitude = generateRandomFloat(1.5f, 4.0f);
            star.starType = 1;
        } else {
            star.magnitude = generateRandomFloat(4.0f, 6.5f);
            star.starType = 2;
        }
        
        if (star.starType == 1) {
            star.temperature = generateRandomFloat(3000.0f, 5000.0f);
        } else if (star.starType == 0) {
            star.temperature = generateRandomFloat(4000.0f, 8000.0f);
        } else {
            star.temperature = generateRandomFloat(2500.0f, 4000.0f);
        }
        
        star.twinkle = generateRandomFloat(0.5f, 3.0f);
        star.sizeFactor = generateRandomFloat(0.5f, 2.0f);  
        star.size = magnitudeToSize(star.magnitude) * star.sizeFactor;
        star.textureIndex = (generateRandomFloat(0.0f, 1.0f) < 0.5f) ? 0 : 1;
        star.isConstellation = (star.magnitude < 3.0f && generateRandomFloat(0.0f, 1.0f) < 0.3f);
    }

    void initStars() {
        
        for (int i = 0; i < NUM_CORE; ++i) {
            initStar(stars[i], true, true);
        }
        
        for (int i = NUM_CORE; i < NUM_STARS; ++i) {
            initStar(stars[i], true, false);  
        }
    }

    void update(float deltaTime) {
        if (!initialized) return;
        if (deltaTime > 0.1f) deltaTime = 0.1f;

        std::vector<float> positions;
        std::vector<float> sizes;
        std::vector<float> colors;
        positions.reserve(NUM_STARS * 3);
        sizes.reserve(NUM_STARS);
        colors.reserve(NUM_STARS * 4);
        
        tex1Indices.clear();
        tex2Indices.clear();
        tex1Indices.reserve(NUM_STARS / 2);
        tex2Indices.reserve(NUM_STARS / 2);

        float time = SDL_GetTicks() * 0.001f;

        for (GLuint i = 0; i < NUM_STARS; ++i) {
            auto& star = stars[i];
            star.x += star.vx * deltaTime;
            star.y += star.vy * deltaTime;
            star.z += star.vz * deltaTime;
            
            if (star.z > 10.0f) {
                float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
                star.z = generateRandomFloat(-300.0f, -100.0f);
                float r = sqrt(generateRandomFloat(0.0f, 1.0f)) * starFieldRadius * 0.3f;
                star.x = r * cos(theta);
                star.y = r * sin(theta);
            }

            if (!star.active) continue;

            positions.push_back(star.x);
            positions.push_back(star.y);
            positions.push_back(star.z);

            float twinkleFactor = 1.0f;
            if (atmosphericTwinkle > 0.0f) {
                twinkleFactor = 0.7f + 0.3f * sin(time * star.twinkle) * atmosphericTwinkle;
            }

            float size = star.size * twinkleFactor;
            if (star.isConstellation) {
                size *= 1.2f;
            }
                float dx = star.x - cameraX;
                float dy = star.y - cameraY;
                float dz = star.z - cameraZ;
                float dist = glm::length(glm::vec3(dx, dy, dz));
                float perspectiveScale = glm::clamp(150.0f / glm::max(dist, 1.0f), 0.05f, 20.0f);
                size *= perspectiveScale;
            sizes.push_back(size);

            glm::vec3 starColor = getStarColor(star.temperature);
            float alpha = magnitudeToAlpha(star.magnitude, lightPollution) * twinkleFactor;

            colors.push_back(starColor.r);
            colors.push_back(starColor.g);
            colors.push_back(starColor.b);
            colors.push_back(alpha);
            
            if (star.textureIndex == 0) {
                tex1Indices.push_back(i);
            } else {
                tex2Indices.push_back(i);
            }
        }

        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, positions.size() * sizeof(float), positions.data());

        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizes.size() * sizeof(float), sizes.data());

        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, colors.size() * sizeof(float), colors.data());
    }

    void draw(int screenW, int screenH) {
        if (!initialized) return;

#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f;
        lastUpdateTime = currentTime;

        update(deltaTime);

        program.useProgram();

        glm::mat4 projection = glm::perspective(
            glm::radians(60.0f),
            (float)screenW / (float)screenH,
            0.1f,
            starFieldDepth + 500.0f
        );

        glm::vec3 front;
        front.x = cos(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front.y = sin(glm::radians(cameraPitch));
        front.z = sin(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front = glm::normalize(front);

        glm::vec3 cameraPos(cameraX, cameraY, cameraZ);
        glm::mat4 view = glm::lookAt(cameraPos, cameraPos + front, glm::vec3(0.0f, 1.0f, 0.0f));
        glm::mat4 mvp = projection * view;

        program.setUniform("MVP", mvp);
        program.setUniform("spriteTexture", 0);

        glBindVertexArray(VAO);
        
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, starTexture);
        if (!tex1Indices.empty()) {
            glDrawElements(GL_POINTS, static_cast<GLsizei>(tex1Indices.size()), GL_UNSIGNED_INT, tex1Indices.data());
        }
        
        
        glBindTexture(GL_TEXTURE_2D, starTexture2);
        if (!tex2Indices.empty()) {
            glDrawElements(GL_POINTS, static_cast<GLsizei>(tex2Indices.size()), GL_UNSIGNED_INT, tex2Indices.data());
        }
        
        glBindVertexArray(0);
    }

    void cleanup() {
        if (!initialized) return;
        
        if (VAO) {
            glDeleteVertexArrays(1, &VAO);
            VAO = 0;
        }
        if (VBO[0]) {
            glDeleteBuffers(3, VBO);
            VBO[0] = VBO[1] = VBO[2] = 0;
        }
        if (starTexture) {
            glDeleteTextures(1, &starTexture);
            starTexture = 0;
        }
        if (starTexture2) {
            glDeleteTextures(1, &starTexture2);
            starTexture2 = 0;
        }
        initialized = false;
    }

    void processInput(float deltaTime) {
        if (!keyboardState) return;
        
        glm::vec3 front;
        front.x = cos(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front.y = sin(glm::radians(cameraPitch));
        front.z = sin(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front = glm::normalize(front);
        
        glm::vec3 right = glm::normalize(glm::cross(front, glm::vec3(0.0f, 1.0f, 0.0f)));
        
        float velocity = cameraSpeed * deltaTime;
        
        if (keyboardState[SDL_SCANCODE_W]) {
            cameraX += front.x * velocity;
            cameraY += front.y * velocity;
            cameraZ += front.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_S]) {
            cameraX -= front.x * velocity;
            cameraY -= front.y * velocity;
            cameraZ -= front.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_A]) {
            cameraX -= right.x * velocity;
            cameraZ -= right.z * velocity;
        }
    }
};

#endif 

#ifdef WITH_VK

class VK_Starfield {
public:
    mx::VKWindow* vkWindow = nullptr;
    VkDevice device = VK_NULL_HANDLE;
    VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
    VkCommandPool commandPool = VK_NULL_HANDLE;
    VkQueue graphicsQueue = VK_NULL_HANDLE;
    
    VkImage starTexture = VK_NULL_HANDLE;
    VkDeviceMemory starTextureMemory = VK_NULL_HANDLE;
    VkImageView starTextureView = VK_NULL_HANDLE;
    VkSampler starSampler = VK_NULL_HANDLE;
    
    VkImage starTexture2 = VK_NULL_HANDLE;
    VkDeviceMemory starTextureMemory2 = VK_NULL_HANDLE;
    VkImageView starTextureView2 = VK_NULL_HANDLE;
    
    VkDescriptorSetLayout starDescriptorSetLayout = VK_NULL_HANDLE;
    VkDescriptorPool starDescriptorPool = VK_NULL_HANDLE;
    std::vector<VkDescriptorSet> starDescriptorSets;
    std::vector<VkDescriptorSet> starDescriptorSets2;  
    
    VkPipelineLayout starPipelineLayout = VK_NULL_HANDLE;
    VkPipeline starPipeline = VK_NULL_HANDLE;
    
    VkBuffer starVertexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory starVertexBufferMemory = VK_NULL_HANDLE;
    void* starVertexBufferMapped = nullptr;
    
    VkBuffer starIndexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory starIndexBufferMemory = VK_NULL_HANDLE;
    void* starIndexBufferMapped = nullptr;
    
    Star stars[NUM_STARS];
    StarVertex starVertices[NUM_STARS];
    std::vector<uint32_t> tex1Indices;  
    std::vector<uint32_t> tex2Indices;  
    bool initialized = false;
    Uint32 lastUpdateTime = 0;
    
    float cameraX = 0.0f, cameraY = 0.0f, cameraZ = 0.0f;
    float cameraYaw = -90.0f, cameraPitch = 0.0f;
    float cameraSpeed = 50.0f;
    float atmosphericTwinkle = 0.5f;
    float lightPollution = 0.0f;
    float warpSpeed = 175.0f;
    float starFieldDepth = 15000.0f;
    float starFieldRadius = 500.0f;
    const Uint8* keyboardState = nullptr;

    void cleanup() {
        if (!initialized || device == VK_NULL_HANDLE) return;
        
        SDL_Log("VK_Starfield::cleanup() - start");
        vkDeviceWaitIdle(device);
        
        if (starPipeline != VK_NULL_HANDLE) {
            vkDestroyPipeline(device, starPipeline, nullptr);
            starPipeline = VK_NULL_HANDLE;
        }
        
        if (starPipelineLayout != VK_NULL_HANDLE) {
            vkDestroyPipelineLayout(device, starPipelineLayout, nullptr);
            starPipelineLayout = VK_NULL_HANDLE;
        }
        
        if (starDescriptorPool != VK_NULL_HANDLE) {
            vkDestroyDescriptorPool(device, starDescriptorPool, nullptr);
            starDescriptorPool = VK_NULL_HANDLE;
        }
        starDescriptorSets.clear();
        starDescriptorSets2.clear();
        
        if (starDescriptorSetLayout != VK_NULL_HANDLE) {
            vkDestroyDescriptorSetLayout(device, starDescriptorSetLayout, nullptr);
            starDescriptorSetLayout = VK_NULL_HANDLE;
        }
        
        if (starVertexBuffer != VK_NULL_HANDLE) {
            vkDestroyBuffer(device, starVertexBuffer, nullptr);
            vkFreeMemory(device, starVertexBufferMemory, nullptr);
            starVertexBuffer = VK_NULL_HANDLE;
            starVertexBufferMapped = nullptr;
        }
        
        if (starIndexBuffer != VK_NULL_HANDLE) {
            vkDestroyBuffer(device, starIndexBuffer, nullptr);
            vkFreeMemory(device, starIndexBufferMemory, nullptr);
            starIndexBuffer = VK_NULL_HANDLE;
            starIndexBufferMapped = nullptr;
        }
        
        if (starSampler != VK_NULL_HANDLE) {
            vkDestroySampler(device, starSampler, nullptr);
            starSampler = VK_NULL_HANDLE;
        }
        
        if (starTextureView != VK_NULL_HANDLE) {
            vkDestroyImageView(device, starTextureView, nullptr);
            starTextureView = VK_NULL_HANDLE;
        }
        
        if (starTexture != VK_NULL_HANDLE) {
            vkDestroyImage(device, starTexture, nullptr);
            vkFreeMemory(device, starTextureMemory, nullptr);
            starTexture = VK_NULL_HANDLE;
        }
        
        if (starTextureView2 != VK_NULL_HANDLE) {
            vkDestroyImageView(device, starTextureView2, nullptr);
            starTextureView2 = VK_NULL_HANDLE;
        }
        
        if (starTexture2 != VK_NULL_HANDLE) {
            vkDestroyImage(device, starTexture2, nullptr);
            vkFreeMemory(device, starTextureMemory2, nullptr);
            starTexture2 = VK_NULL_HANDLE;
        }
        
        SDL_Log("VK_Starfield::cleanup() - complete");
        initialized = false;
        device = VK_NULL_HANDLE;
    }

    void init(mx::VKWindow* vkWin, const std::string& dataPath) {
        if (initialized) return;
        
        vkWindow = vkWin;
        device = vkWin->getDevice();
        physicalDevice = vkWin->getPhysicalDevice();
        commandPool = vkWin->getCommandPool();
        graphicsQueue = vkWin->getGraphicsQueue();
        
        keyboardState = SDL_GetKeyboardState(nullptr);
        
        initStars();
        loadStarTexture(dataPath + "/data/star.png", starTexture, starTextureMemory, starTextureView);
        loadStarTexture(dataPath + "/data/star2.png", starTexture2, starTextureMemory2, starTextureView2);
        createStarDescriptorSetLayout();
        createStarPipeline(vkWin, dataPath);
        createStarVertexBuffer();
        createStarDescriptorPool(vkWin);
        createStarDescriptorSets(vkWin);
        
        lastUpdateTime = SDL_GetTicks();
        initialized = true;
        
        SDL_Log("VK Starfield initialized with %d stars", NUM_STARS);
    }

    void initStar(Star& star, bool randomZ = true, bool core = false) {
        float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
        star.z = randomZ ? generateRandomFloat(-300.0f, -10.0f) : -300.0f;
        float r = sqrt(generateRandomFloat(0.0f, 1.0f)) * starFieldRadius * 0.3f;

        star.x = r * cos(theta);
        star.y = r * sin(theta);
        star.isCore = core;
        star.active = true;
        star.vx = generateRandomFloat(-1.0f, 1.0f);
        star.vy = generateRandomFloat(-1.0f, 1.0f);
        star.vz = warpSpeed;
        
        float rand = generateRandomFloat(0.0f, 1.0f);
        if (rand < 0.05f) {
            star.magnitude = generateRandomFloat(-1.5f, 1.5f);
            star.starType = 0;
        } else if (rand < 0.25f) {
            star.magnitude = generateRandomFloat(1.5f, 4.0f);
            star.starType = 1;
        } else {
            star.magnitude = generateRandomFloat(4.0f, 6.5f);
            star.starType = 2;
        }
        
        if (star.starType == 1) {
            star.temperature = generateRandomFloat(3000.0f, 5000.0f);
        } else if (star.starType == 0) {
            star.temperature = generateRandomFloat(4000.0f, 8000.0f);
        } else {
            star.temperature = generateRandomFloat(2500.0f, 4000.0f);
        }
        
        star.twinkle = generateRandomFloat(0.5f, 3.0f);
        star.sizeFactor = generateRandomFloat(0.5f, 2.0f);  
        star.size = magnitudeToSize(star.magnitude) * star.sizeFactor;
        star.textureIndex = (generateRandomFloat(0.0f, 1.0f) < 0.5f) ? 0 : 1;
        star.isConstellation = (star.magnitude < 3.0f && generateRandomFloat(0.0f, 1.0f) < 0.3f);
    }

    void initStars() {
        for (int i = 0; i < NUM_STARS; ++i) {
            initStar(stars[i], true);  
        }
    }

    void loadStarTexture(const std::string& path, VkImage& outTexture, 
                          VkDeviceMemory& outTextureMemory, VkImageView& outTextureView) {
        SDL_Surface* starImg = png::LoadPNG(path.c_str());
        if (!starImg || !starImg->pixels) {
            throw mx::Exception("Failed to load star texture: " + path);
        }
        
        int width = starImg->w;
        int height = starImg->h;
        VkDeviceSize imageSize = width * height * 4;
        
        VkBuffer stagingBuffer;
        VkDeviceMemory stagingBufferMemory;
        
        VkBufferCreateInfo bufferInfo{};
        bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        bufferInfo.size = imageSize;
        bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
        bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
        
        VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &stagingBuffer));
        
        VkMemoryRequirements memRequirements;
        vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);
        
        VkMemoryAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
        allocInfo.allocationSize = memRequirements.size;
        allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
        
        VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &stagingBufferMemory));
        vkBindBufferMemory(device, stagingBuffer, stagingBufferMemory, 0);
        
        void* data;
        vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
        memcpy(data, starImg->pixels, static_cast<size_t>(imageSize));
        vkUnmapMemory(device, stagingBufferMemory);
        
        SDL_FreeSurface(starImg);
        
        VkImageCreateInfo imageInfo{};
        imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
        imageInfo.imageType = VK_IMAGE_TYPE_2D;
        imageInfo.extent.width = static_cast<uint32_t>(width);
        imageInfo.extent.height = static_cast<uint32_t>(height);
        imageInfo.extent.depth = 1;
        imageInfo.mipLevels = 1;
        imageInfo.arrayLayers = 1;
        imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
        imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
        imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
        imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
        imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
        imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
        
        VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &outTexture));
        
        vkGetImageMemoryRequirements(device, outTexture, &memRequirements);
        
        allocInfo.allocationSize = memRequirements.size;
        allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
            VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
        
        VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &outTextureMemory));
        vkBindImageMemory(device, outTexture, outTextureMemory, 0);
        
        transitionImageLayout(outTexture, VK_FORMAT_R8G8B8A8_SRGB,
            VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
        copyBufferToImage(stagingBuffer, outTexture, static_cast<uint32_t>(width), static_cast<uint32_t>(height));
        transitionImageLayout(outTexture, VK_FORMAT_R8G8B8A8_SRGB,
            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
        
        vkDestroyBuffer(device, stagingBuffer, nullptr);
        vkFreeMemory(device, stagingBufferMemory, nullptr);
        
        VkImageViewCreateInfo viewInfo{};
        viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
        viewInfo.image = outTexture;
        viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
        viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
        viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        viewInfo.subresourceRange.baseMipLevel = 0;
        viewInfo.subresourceRange.levelCount = 1;
        viewInfo.subresourceRange.baseArrayLayer = 0;
        viewInfo.subresourceRange.layerCount = 1;
        
        VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &outTextureView));
        
        
        if (starSampler == VK_NULL_HANDLE) {
            VkSamplerCreateInfo samplerInfo{};
            samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
            samplerInfo.magFilter = VK_FILTER_LINEAR;
            samplerInfo.minFilter = VK_FILTER_LINEAR;
            samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
            samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
            samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
            samplerInfo.anisotropyEnable = VK_FALSE;
            samplerInfo.maxAnisotropy = 1.0f;
            samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
            samplerInfo.unnormalizedCoordinates = VK_FALSE;
            samplerInfo.compareEnable = VK_FALSE;
            samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
            samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
            
            VK_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &starSampler));
        }
    }

    void transitionImageLayout(VkImage image, VkFormat format,
        VkImageLayout oldLayout, VkImageLayout newLayout) {
        VkCommandBufferAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        allocInfo.commandPool = commandPool;
        allocInfo.commandBufferCount = 1;
        
        VkCommandBuffer commandBuffer;
        vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
        
        VkCommandBufferBeginInfo beginInfo{};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
        
        vkBeginCommandBuffer(commandBuffer, &beginInfo);
        
        VkImageMemoryBarrier barrier{};
        barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
        barrier.oldLayout = oldLayout;
        barrier.newLayout = newLayout;
        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;
        
        VkPipelineStageFlags sourceStage;
        VkPipelineStageFlags destinationStage;
        
        if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED &&
            newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
            barrier.srcAccessMask = 0;
            barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
            sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
            destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL &&
                   newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
            barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
            barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
            sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
            destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
        } else {
            throw mx::Exception("Unsupported layout transition");
        }
        
        vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage,
            0, 0, nullptr, 0, nullptr, 1, &barrier);
        
        vkEndCommandBuffer(commandBuffer);
        
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &commandBuffer;
        
        vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
        vkQueueWaitIdle(graphicsQueue);
        
        vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
    }

    void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
        VkCommandBufferAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        allocInfo.commandPool = commandPool;
        allocInfo.commandBufferCount = 1;
        
        VkCommandBuffer commandBuffer;
        vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
        
        VkCommandBufferBeginInfo beginInfo{};
        beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
        
        vkBeginCommandBuffer(commandBuffer, &beginInfo);
        
        VkBufferImageCopy region{};
        region.bufferOffset = 0;
        region.bufferRowLength = 0;
        region.bufferImageHeight = 0;
        region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        region.imageSubresource.mipLevel = 0;
        region.imageSubresource.baseArrayLayer = 0;
        region.imageSubresource.layerCount = 1;
        region.imageOffset = {0, 0, 0};
        region.imageExtent = {width, height, 1};
        
        vkCmdCopyBufferToImage(commandBuffer, buffer, image,
            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
        
        vkEndCommandBuffer(commandBuffer);
        
        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &commandBuffer;
        
        vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
        vkQueueWaitIdle(graphicsQueue);
        
        vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
    }

    void createStarDescriptorSetLayout() {
        std::array<VkDescriptorSetLayoutBinding, 2> bindings{};
        
        bindings[0].binding = 0;
        bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        bindings[0].descriptorCount = 1;
        bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
        bindings[0].pImmutableSamplers = nullptr;
        
        bindings[1].binding = 1;
        bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
        bindings[1].descriptorCount = 1;
        bindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
        bindings[1].pImmutableSamplers = nullptr;
        
        VkDescriptorSetLayoutCreateInfo layoutInfo{};
        layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
        layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
        layoutInfo.pBindings = bindings.data();
        
        VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &starDescriptorSetLayout));
    }

    void createStarDescriptorPool(mx::VKWindow* vkWin) {
        uint32_t imageCount = vkWin->getSwapChainImageCount();
        
        
        std::array<VkDescriptorPoolSize, 2> poolSizes{};
        poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
        poolSizes[0].descriptorCount = imageCount * 2;
        poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
        poolSizes[1].descriptorCount = imageCount * 2;
        
        VkDescriptorPoolCreateInfo poolInfo{};
        poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
        poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
        poolInfo.pPoolSizes = poolSizes.data();
        poolInfo.maxSets = imageCount * 2;  
        
        VK_CHECK_RESULT(vkCreateDescriptorPool(device, &poolInfo, nullptr, &starDescriptorPool));
    }

    void createStarDescriptorSets(mx::VKWindow* vkWin) {
        uint32_t imageCount = vkWin->getSwapChainImageCount();
        std::vector<VkDescriptorSetLayout> layouts(imageCount * 2, starDescriptorSetLayout);
        
        VkDescriptorSetAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
        allocInfo.descriptorPool = starDescriptorPool;
        allocInfo.descriptorSetCount = imageCount * 2;
        allocInfo.pSetLayouts = layouts.data();
        
        std::vector<VkDescriptorSet> allSets(imageCount * 2);
        VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, allSets.data()));
        
        starDescriptorSets.resize(imageCount);
        starDescriptorSets2.resize(imageCount);
        for (uint32_t i = 0; i < imageCount; i++) {
            starDescriptorSets[i] = allSets[i];
            starDescriptorSets2[i] = allSets[imageCount + i];
        }
        
        for (size_t i = 0; i < imageCount; i++) {
            VkDescriptorBufferInfo bufferInfo{};
            bufferInfo.buffer = vkWin->getUniformBuffer(i);
            bufferInfo.offset = 0;
            bufferInfo.range = sizeof(mx::UniformBufferObject);
            
            
            VkDescriptorImageInfo imageInfo1{};
            imageInfo1.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
            imageInfo1.imageView = starTextureView;
            imageInfo1.sampler = starSampler;
            
            std::array<VkWriteDescriptorSet, 2> descriptorWrites{};
            
            descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
            descriptorWrites[0].dstSet = starDescriptorSets[i];
            descriptorWrites[0].dstBinding = 0;
            descriptorWrites[0].dstArrayElement = 0;
            descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
            descriptorWrites[0].descriptorCount = 1;
            descriptorWrites[0].pImageInfo = &imageInfo1;
            
            descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
            descriptorWrites[1].dstSet = starDescriptorSets[i];
            descriptorWrites[1].dstBinding = 1;
            descriptorWrites[1].dstArrayElement = 0;
            descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
            descriptorWrites[1].descriptorCount = 1;
            descriptorWrites[1].pBufferInfo = &bufferInfo;
            
            vkUpdateDescriptorSets(device, static_cast<uint32_t>(descriptorWrites.size()),
                                   descriptorWrites.data(), 0, nullptr);
            
            
            VkDescriptorImageInfo imageInfo2{};
            imageInfo2.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
            imageInfo2.imageView = starTextureView2;
            imageInfo2.sampler = starSampler;
            
            descriptorWrites[0].dstSet = starDescriptorSets2[i];
            descriptorWrites[0].pImageInfo = &imageInfo2;
            
            descriptorWrites[1].dstSet = starDescriptorSets2[i];
            
            vkUpdateDescriptorSets(device, static_cast<uint32_t>(descriptorWrites.size()),
                                   descriptorWrites.data(), 0, nullptr);
        }
    }

    void createStarVertexBuffer() {
        VkDeviceSize bufferSize = sizeof(StarVertex) * NUM_STARS;
        
        VkBufferCreateInfo bufferInfo{};
        bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        bufferInfo.size = bufferSize;
        bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
        bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
        
        VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &starVertexBuffer));
        
        VkMemoryRequirements memRequirements;
        vkGetBufferMemoryRequirements(device, starVertexBuffer, &memRequirements);
        
        VkMemoryAllocateInfo allocInfo{};
        allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
        allocInfo.allocationSize = memRequirements.size;
        allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
        
        VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &starVertexBufferMemory));
        vkBindBufferMemory(device, starVertexBuffer, starVertexBufferMemory, 0);
        
        vkMapMemory(device, starVertexBufferMemory, 0, bufferSize, 0, &starVertexBufferMapped);
        
        
        VkDeviceSize indexBufferSize = sizeof(uint32_t) * NUM_STARS;
        
        bufferInfo.size = indexBufferSize;
        bufferInfo.usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
        
        VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &starIndexBuffer));
        
        vkGetBufferMemoryRequirements(device, starIndexBuffer, &memRequirements);
        
        allocInfo.allocationSize = memRequirements.size;
        allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
        
        VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &starIndexBufferMemory));
        vkBindBufferMemory(device, starIndexBuffer, starIndexBufferMemory, 0);
        
        vkMapMemory(device, starIndexBufferMemory, 0, indexBufferSize, 0, &starIndexBufferMapped);
    }

    uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
        VkPhysicalDeviceMemoryProperties memProperties;
        vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
        
        for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
            if ((typeFilter & (1 << i)) &&
                (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
                return i;
            }
        }
        
        throw mx::Exception("Failed to find suitable memory type");
    }

    void createStarPipeline(mx::VKWindow* vkWin, const std::string& dataPath) {
        auto vertShaderCode = mx::readFile(dataPath + "/data/star_vert.spv");
        auto fragShaderCode = mx::readFile(dataPath + "/data/star_frag.spv");
        
        VkShaderModuleCreateInfo createInfo{};
        createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
        createInfo.codeSize = vertShaderCode.size();
        createInfo.pCode = reinterpret_cast<const uint32_t*>(vertShaderCode.data());
        
        VkShaderModule vertShaderModule;
        VK_CHECK_RESULT(vkCreateShaderModule(device, &createInfo, nullptr, &vertShaderModule));
        
        createInfo.codeSize = fragShaderCode.size();
        createInfo.pCode = reinterpret_cast<const uint32_t*>(fragShaderCode.data());
        
        VkShaderModule fragShaderModule;
        VK_CHECK_RESULT(vkCreateShaderModule(device, &createInfo, nullptr, &fragShaderModule));
        
        VkPipelineShaderStageCreateInfo shaderStages[2] = {};
        shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
        shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
        shaderStages[0].module = vertShaderModule;
        shaderStages[0].pName = "main";
        
        shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
        shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
        shaderStages[1].module = fragShaderModule;
        shaderStages[1].pName = "main";
        
        VkVertexInputBindingDescription bindingDescription{};
        bindingDescription.binding = 0;
        bindingDescription.stride = sizeof(StarVertex);
        bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
        
        std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};
        attributeDescriptions[0].binding = 0;
        attributeDescriptions[0].location = 0;
        attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
        attributeDescriptions[0].offset = offsetof(StarVertex, pos);
        
        attributeDescriptions[1].binding = 0;
        attributeDescriptions[1].location = 1;
        attributeDescriptions[1].format = VK_FORMAT_R32_SFLOAT;
        attributeDescriptions[1].offset = offsetof(StarVertex, size);
        
        attributeDescriptions[2].binding = 0;
        attributeDescriptions[2].location = 2;
        attributeDescriptions[2].format = VK_FORMAT_R32G32B32A32_SFLOAT;
        attributeDescriptions[2].offset = offsetof(StarVertex, color);
        
        VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
        vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
        vertexInputInfo.vertexBindingDescriptionCount = 1;
        vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
        vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescriptions.size());
        vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data();
        
        VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
        inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
        inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
        inputAssembly.primitiveRestartEnable = VK_FALSE;
        
        VkViewport viewport{};
        viewport.x = 0.0f;
        viewport.y = 0.0f;
        viewport.width = (float)vkWin->getSwapChainExtent().width;
        viewport.height = (float)vkWin->getSwapChainExtent().height;
        viewport.minDepth = 0.0f;
        viewport.maxDepth = 1.0f;
        
        VkRect2D scissor{};
        scissor.offset = {0, 0};
        scissor.extent = vkWin->getSwapChainExtent();
        
        std::vector<VkDynamicState> dynamicStates = {
            VK_DYNAMIC_STATE_VIEWPORT,
            VK_DYNAMIC_STATE_SCISSOR
        };
        
        VkPipelineDynamicStateCreateInfo dynamicState{};
        dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
        dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
        dynamicState.pDynamicStates = dynamicStates.data();
        
        VkPipelineViewportStateCreateInfo viewportState{};
        viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
        viewportState.viewportCount = 1;
        viewportState.pViewports = &viewport;
        viewportState.scissorCount = 1;
        viewportState.pScissors = &scissor;
        
        VkPipelineRasterizationStateCreateInfo rasterizer{};
        rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
        rasterizer.depthClampEnable = VK_FALSE;
        rasterizer.rasterizerDiscardEnable = VK_FALSE;
        rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
        rasterizer.lineWidth = 1.0f;
        rasterizer.cullMode = VK_CULL_MODE_NONE;
        rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
        rasterizer.depthBiasEnable = VK_FALSE;
        
        VkPipelineMultisampleStateCreateInfo multisampling{};
        multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
        multisampling.sampleShadingEnable = VK_FALSE;
        multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
        
        VkPipelineColorBlendAttachmentState colorBlendAttachment{};
        colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
                                              VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
        colorBlendAttachment.blendEnable = VK_TRUE;
        colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
        colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
        colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
        colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
        colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
        colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
        
        VkPipelineColorBlendStateCreateInfo colorBlending{};
        colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
        colorBlending.logicOpEnable = VK_FALSE;
        colorBlending.attachmentCount = 1;
        colorBlending.pAttachments = &colorBlendAttachment;
        
        VkPipelineDepthStencilStateCreateInfo depthStencil{};
        depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
        depthStencil.depthTestEnable = VK_FALSE;
        depthStencil.depthWriteEnable = VK_FALSE;
        depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
        depthStencil.depthBoundsTestEnable = VK_FALSE;
        depthStencil.stencilTestEnable = VK_FALSE;
        
        VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
        pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
        pipelineLayoutInfo.setLayoutCount = 1;
        pipelineLayoutInfo.pSetLayouts = &starDescriptorSetLayout;
        
        VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &starPipelineLayout));
        
        VkGraphicsPipelineCreateInfo pipelineInfo{};
        pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
        pipelineInfo.stageCount = 2;
        pipelineInfo.pStages = shaderStages;
        pipelineInfo.pVertexInputState = &vertexInputInfo;
        pipelineInfo.pInputAssemblyState = &inputAssembly;
        pipelineInfo.pViewportState = &viewportState;
        pipelineInfo.pRasterizationState = &rasterizer;
        pipelineInfo.pMultisampleState = &multisampling;
        pipelineInfo.pDepthStencilState = &depthStencil;
        pipelineInfo.pColorBlendState = &colorBlending;
        pipelineInfo.pDynamicState = &dynamicState;
        pipelineInfo.layout = starPipelineLayout;
        pipelineInfo.renderPass = vkWin->getRenderPass();
        pipelineInfo.subpass = 0;
        
        VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &starPipeline));
        
        vkDestroyShaderModule(device, fragShaderModule, nullptr);
        vkDestroyShaderModule(device, vertShaderModule, nullptr);
    }

    void update(float deltaTime) {
        if (!initialized) return;
        if (deltaTime > 0.1f) deltaTime = 0.1f;
        
        float time = SDL_GetTicks() * 0.001f;
        
        tex1Indices.clear();
        tex2Indices.clear();
        tex1Indices.reserve(NUM_STARS / 2);
        tex2Indices.reserve(NUM_STARS / 2);
        
        for (int i = 0; i < NUM_STARS; ++i) {
            auto& star = stars[i];
            
            star.x += star.vx * deltaTime;
            star.y += star.vy * deltaTime;
            star.z += star.vz * deltaTime;
            
            
            if (star.z > 10.0f) {
                float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
                star.z = generateRandomFloat(-300.0f, -100.0f);
                float r = sqrt(generateRandomFloat(0.0f, 1.0f)) * starFieldRadius * 0.3f;
                star.x = r * cos(theta);
                star.y = r * sin(theta);
            }

            if (!star.active) continue;

            starVertices[i].pos[0] = star.x;
            starVertices[i].pos[1] = star.y;
            starVertices[i].pos[2] = star.z;
            
            float twinkleFactor = 1.0f;
            if (atmosphericTwinkle > 0.0f) {
                twinkleFactor = 0.7f + 0.3f * sin(time * star.twinkle) * atmosphericTwinkle;
            }
            
            float size = star.size * twinkleFactor;
            if (star.isConstellation) {
                size *= 1.2f;
            }
            float dx = star.x - cameraX;
            float dy = star.y - cameraY;
            float dz = star.z - cameraZ;
            float dist = glm::length(glm::vec3(dx, dy, dz));
            float perspectiveScale = glm::clamp(150.0f / glm::max(dist, 1.0f), 0.05f, 20.0f);
            size *= perspectiveScale;
            starVertices[i].size = size;
            
            glm::vec3 starColor = getStarColor(star.temperature);
            float alpha = magnitudeToAlpha(star.magnitude, lightPollution) * twinkleFactor;
            
            starVertices[i].color[0] = starColor.r;
            starVertices[i].color[1] = starColor.g;
            starVertices[i].color[2] = starColor.b;
            starVertices[i].color[3] = alpha;
            
            
            if (star.textureIndex == 0) {
                tex1Indices.push_back(static_cast<uint32_t>(i));
            } else {
                tex2Indices.push_back(static_cast<uint32_t>(i));
            }
        }
        
        if (starVertexBufferMapped) {
            memcpy(starVertexBufferMapped, starVertices, sizeof(starVertices));
        }
    }

    void draw(VkCommandBuffer commandBuffer, uint32_t imageIndex, int screenW, int screenH) {
        if (!initialized || !vkWindow) return;
        
        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f;
        lastUpdateTime = currentTime;
        
        update(deltaTime);
        
        glm::vec3 front;
        front.x = cos(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front.y = sin(glm::radians(cameraPitch));
        front.z = sin(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front = glm::normalize(front);
        
        glm::vec3 cameraPos(cameraX, cameraY, cameraZ);
        glm::mat4 view = glm::lookAt(cameraPos, cameraPos + front, glm::vec3(0.0f, 1.0f, 0.0f));
        
        glm::mat4 proj = glm::perspective(glm::radians(60.0f),
            (float)screenW / (float)screenH, 0.1f, starFieldDepth + 500.0f);
        proj[1][1] *= -1;
        
        mx::UniformBufferObject ubo{};
        ubo.model = glm::mat4(1.0f);
        ubo.view = view;
        ubo.proj = proj;
        ubo.color = glm::vec4(1.0f);
        ubo.params = glm::vec4(SDL_GetTicks() / 1000.0f, static_cast<float>(screenW), static_cast<float>(screenH), 1.0f);
        vkWindow->updateUniformBuffer(imageIndex, ubo);
        
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, starPipeline);
        
        VkViewport viewport{};
        viewport.x = 0.0f;
        viewport.y = 0.0f;
        viewport.width = static_cast<float>(screenW);
        viewport.height = static_cast<float>(screenH);
        viewport.minDepth = 0.0f;
        viewport.maxDepth = 1.0f;
        vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
        
        VkRect2D scissor{};
        scissor.offset = {0, 0};
        scissor.extent = {static_cast<uint32_t>(screenW), static_cast<uint32_t>(screenH)};
        vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
        
        VkBuffer vertexBuffers[] = {starVertexBuffer};
        VkDeviceSize offsets[] = {0};
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, starIndexBuffer, 0, VK_INDEX_TYPE_UINT32);
        
        
        if (!tex1Indices.empty() && starIndexBufferMapped) {
            memcpy(starIndexBufferMapped, tex1Indices.data(), tex1Indices.size() * sizeof(uint32_t));
            vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
                starPipelineLayout, 0, 1, &starDescriptorSets[imageIndex], 0, nullptr);
            vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(tex1Indices.size()), 1, 0, 0, 0);
        }
        
        
        if (!tex2Indices.empty() && starIndexBufferMapped) {
            memcpy(starIndexBufferMapped, tex2Indices.data(), tex2Indices.size() * sizeof(uint32_t));
            vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
                starPipelineLayout, 0, 1, &starDescriptorSets2[imageIndex], 0, nullptr);
            vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(tex2Indices.size()), 1, 0, 0, 0);
        }
    }

    void processInput(float deltaTime) {
        if (!keyboardState) return;
        
        glm::vec3 front;
        front.x = cos(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front.y = sin(glm::radians(cameraPitch));
        front.z = sin(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front = glm::normalize(front);
        
        glm::vec3 right = glm::normalize(glm::cross(front, glm::vec3(0.0f, 1.0f, 0.0f)));
        
        float velocity = cameraSpeed * deltaTime;
        
        if (keyboardState[SDL_SCANCODE_W]) {
            cameraX += front.x * velocity;
            cameraY += front.y * velocity;
            cameraZ += front.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_S]) {
            cameraX -= front.x * velocity;
            cameraY -= front.y * velocity;
            cameraZ -= front.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_A]) {
            cameraX -= right.x * velocity;
            cameraZ -= right.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_D]) {
            cameraX += right.x * velocity;
            cameraZ += right.z * velocity;
        }
        if (keyboardState[SDL_SCANCODE_Q]) {
            cameraY += velocity;
        }
        if (keyboardState[SDL_SCANCODE_E]) {
            cameraY -= velocity;
        }
    }
};

#endif 

class StarfieldApp : public cross::EventHandler {
public:
#ifdef WITH_GL
    std::unique_ptr<GL_Starfield> glStarfield;
#endif
#ifdef WITH_VK
    std::unique_ptr<VK_Starfield> vkStarfield;
#endif
    
    bool mouseGrabbed = false;
    int lastMouseX = 0, lastMouseY = 0;
    Uint64 lastFrameTime = 0;
    
    void load() override {
        SDL_Log("StarfieldApp::load()");
        lastFrameTime = SDL_GetPerformanceCounter();
        
        std::string path = window->getPath();
        
#ifdef WITH_VK
        auto* vkWin = window->as<cross::VK_Window>();
        if (vkWin) {
            SDL_Log("Using Vulkan renderer");
            vkStarfield = std::make_unique<VK_Starfield>();
            vkStarfield->init(vkWin, path);
            return;
        }
#endif
#ifdef WITH_GL
        auto* glWin = window->as<cross::GL_Window>();
        if (glWin) {
            SDL_Log("Using OpenGL renderer");
            glStarfield = std::make_unique<GL_Starfield>();
            glStarfield->init(path);
            return;
        }
#endif
        SDL_Log("Warning: No renderer initialized!");
    }
    
    void draw() override {
        int w = window->getWidth();
        int h = window->getHeight();
        
#ifdef WITH_VK
        if (vkStarfield) {
            auto* vkWin = window->as<cross::VK_Window>();
            if (vkWin) {
                VkCommandBuffer cmd = vkWin->getCurrentCommandBuffer();
                uint32_t imageIndex = vkWin->getCurrentImageIndex();
                vkStarfield->draw(cmd, imageIndex, w, h);
            }
            return;
        }
#endif
#ifdef WITH_GL
        if (glStarfield) {
            glStarfield->draw(w, h);
            return;
        }
#endif
    }
    
    void event(SDL_Event &e) override {
        if (e.type == SDL_KEYDOWN) {
            switch (e.key.keysym.sym) {
                case SDLK_ESCAPE:
                    window->quit();
                    break;
                case SDLK_g:
                    mouseGrabbed = !mouseGrabbed;
                    SDL_SetRelativeMouseMode(mouseGrabbed ? SDL_TRUE : SDL_FALSE);
                    break;
                case SDLK_UP:
#ifdef WITH_VK
                    if (vkStarfield) vkStarfield->atmosphericTwinkle = std::min(1.0f, vkStarfield->atmosphericTwinkle + 0.1f);
#endif
#ifdef WITH_GL
                    if (glStarfield) glStarfield->atmosphericTwinkle = std::min(1.0f, glStarfield->atmosphericTwinkle + 0.1f);
#endif
                    break;
                case SDLK_DOWN:
#ifdef WITH_VK
                    if (vkStarfield) vkStarfield->atmosphericTwinkle = std::max(0.0f, vkStarfield->atmosphericTwinkle - 0.1f);
#endif
#ifdef WITH_GL
                    if (glStarfield) glStarfield->atmosphericTwinkle = std::max(0.0f, glStarfield->atmosphericTwinkle - 0.1f);
#endif
                    break;
            }
        }
        
        if (e.type == SDL_MOUSEMOTION && mouseGrabbed) {
            float sensitivity = 0.1f;
            float xOffset = e.motion.xrel * sensitivity;
            float yOffset = -e.motion.yrel * sensitivity;
            
#ifdef WITH_VK
            if (vkStarfield) {
                vkStarfield->cameraYaw += xOffset;
                vkStarfield->cameraPitch += yOffset;
                if (vkStarfield->cameraPitch > 89.0f) vkStarfield->cameraPitch = 89.0f;
                if (vkStarfield->cameraPitch < -89.0f) vkStarfield->cameraPitch = -89.0f;
            }
#endif
#ifdef WITH_GL
            if (glStarfield) {
                glStarfield->cameraYaw += xOffset;
                glStarfield->cameraPitch += yOffset;
                if (glStarfield->cameraPitch > 89.0f) glStarfield->cameraPitch = 89.0f;
                if (glStarfield->cameraPitch < -89.0f) glStarfield->cameraPitch = -89.0f;
            }
#endif
        }
    }
    
    void update(float deltaTime) override {
#ifdef WITH_VK
        if (vkStarfield) {
            vkStarfield->processInput(deltaTime);
        }
#endif
#ifdef WITH_GL
        if (glStarfield) {
            glStarfield->processInput(deltaTime);
        }
#endif
    }
    
    void cleanup() override {
        SDL_Log("StarfieldApp::cleanup()");
#ifdef WITH_VK
        if (vkStarfield) {
            vkStarfield->cleanup();
            vkStarfield.reset();
        }
#endif
#ifdef WITH_GL
        if (glStarfield) {
            glStarfield->cleanup();
            glStarfield.reset();
        }
#endif
    }
};




int main(int argc, char **argv) {
    Arguments args = proc_args(argc, argv);
    
    try {
        auto window = cross::createWindow();
        StarfieldApp app;
        
        window->setHandler(&app);
        window->init("Cross-Platform Starfield 3D", args.path, args.width, args.height, args.fullscreen);
        window->loop();
        window->cleanup();
        
    } catch (mx::Exception &e) {
        SDL_Log("mx: Exception: %s", e.text().c_str());
        return EXIT_FAILURE;
    }
    
    return EXIT_SUCCESS;
}