#include "renderer/sprite_render_system.hpp" #include #include #include #include "shaders/sprite.frag.spv.h" #include "shaders/sprite.vert.spv.h" #include "vulkan/descriptors/descriptor_writer.hpp" namespace sophia { namespace { constexpr u32 MAX_TEXTURES = 1024; constexpr u32 VERTICES_PER_SPRITE = 6; } // namespace SpriteRenderSystem::SpriteRenderSystem(Device& device, vk::Format colorFormat, ResourceRegistry& resources) : device{device}, resources{resources} { createDescriptors(); createPipelineLayout(); createPipeline(colorFormat); createInstanceBuffers(); } SpriteRenderSystem::~SpriteRenderSystem() { log::trace("destroyed sophia::SpriteRenderSystem"); } void SpriteRenderSystem::beginFrame(u32 frameIndex) { this->frameIndex = frameIndex; this->instanceCursor = 0; } void SpriteRenderSystem::render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection, std::span instances, std::span batches) { ZoneScoped; if (instances.empty()) { return; } u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor; if (instances.size() > available) { log::warning("SpriteRenderSystem: frame instance buffer full, dropping", instances.size() - available, "sprites"); } Buffer& buffer = *this->instanceBuffers[this->frameIndex]; u32 base = this->instanceCursor; u32 count = std::min(static_cast(instances.size()), available); buffer.writeToBuffer(const_cast(instances.data()), count * sizeof(Instance), base * sizeof(Instance)); this->instanceCursor += count; this->graphicsPipeline->bind(commandBuffer); PushConstantData push{viewProjection}; commandBuffer.pushConstants(this->pipelineLayout.get(), vk::ShaderStageFlagBits::eVertex, 0, sizeof(PushConstantData), &push); vk::Buffer vertexBuffer = buffer.getBuffer(); vk::DeviceSize offset = 0; commandBuffer.bindVertexBuffers(0, 1, &vertexBuffer, &offset); for (const Batch& batch : batches) { if (batch.firstInstance >= count) { break; } u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance); vk::DescriptorSet set = getDescriptorSet(batch.texture); commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, this->pipelineLayout.get(), 0, 1, &set, 0, nullptr); commandBuffer.draw(VERTICES_PER_SPRITE, batchCount, 0, base + batch.firstInstance); } } vk::DescriptorSet SpriteRenderSystem::getDescriptorSet(TextureHandle handle) { auto it = this->descriptorSets.find(handle.index); if (it != this->descriptorSets.end() && it->second.generation == handle.generation) { return it->second.set; } const Texture& texture = this->resources.texturePool.get(handle); vk::DescriptorImageInfo imageInfo = texture.descriptorInfo(); DescriptorWriter writer{*this->setLayout, *this->descriptorPool}; writer.writeImage(0, &imageInfo); if (it == this->descriptorSets.end()) { vk::DescriptorSet set; bool allocated = writer.build(set); SOPHIA_ASSERT(allocated, "SpriteRenderSystem: out of texture descriptors"); this->descriptorSets.emplace(static_cast(handle.index), CachedDescriptorSet{handle.generation, set}); return set; } // The slot was reused by a new texture. Releasing a texture waits for the // device to go idle, so no in-flight frame can still be using this set. writer.overwrite(it->second.set); it->second.generation = handle.generation; return it->second.set; } void SpriteRenderSystem::createDescriptors() { this->setLayout = DescriptorSetLayout::Builder(this->device) .addBinding(0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment) .build(); this->descriptorPool = DescriptorPool::Builder(this->device) .setMaxSets(MAX_TEXTURES) .addPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_TEXTURES) .build(); } void SpriteRenderSystem::createPipelineLayout() { vk::PushConstantRange pushConstantRange{}; pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; pushConstantRange.offset = 0; pushConstantRange.size = sizeof(PushConstantData); vk::DescriptorSetLayout setLayout = this->setLayout->getDescriptorSetLayout(); vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {}; pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &setLayout; pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; try { this->pipelineLayout = this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo); log::trace("created vk::PipelineLayout"); } catch (const vk::SystemError& err) { log::fatal("failed to create vk::PipelineLayout"); throw std::runtime_error("failed to create vk::PipelineLayout"); } } void SpriteRenderSystem::createPipeline(vk::Format colorFormat) { auto vertexShader = Shader::Builder() .fromSPIRV(shaders::sprite_vert_spv) .setStage(ShaderStage::VERTEX) .build(); auto fragmentShader = Shader::Builder() .fromSPIRV(shaders::sprite_frag_spv) .setStage(ShaderStage::FRAGMENT) .build(); std::vector bindings{ {0, sizeof(Instance), vk::VertexInputRate::eInstance}}; std::vector attributes; // A mat4 attribute takes four consecutive locations, one per column. for (u32 column = 0; column < 4; column++) { attributes.push_back({column, 0, vk::Format::eR32G32B32A32Sfloat, static_cast(offsetof(Instance, model) + column * sizeof(glm::vec4))}); } attributes.push_back({4, 0, vk::Format::eR32G32B32A32Sfloat, static_cast(offsetof(Instance, color))}); attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat, static_cast(offsetof(Instance, uvRect))}); this->graphicsPipeline = GraphicsPipeline::Builder(this->device) .setVertexShader(*vertexShader) .setFragmentShader(*fragmentShader) .setPipelineLayout(this->pipelineLayout.get()) .setColorAttachmentFormats({colorFormat}) .setVertexInput(bindings, attributes) .setAlphaBlending() .build(); } void SpriteRenderSystem::createInstanceBuffers() { for (std::unique_ptr& buffer : this->instanceBuffers) { buffer = std::make_unique( this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME, vk::BufferUsageFlagBits::eVertexBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); buffer->map(); } } } // namespace sophia