font: glyph per-instance data, draw multiple individual glyphs

This commit is contained in:
Zack Buhman 2026-05-20 23:19:06 -05:00
parent 4042bbd623
commit bb6f76cf72
4 changed files with 261 additions and 18 deletions

View File

@ -28,9 +28,26 @@ namespace font::outline {
AllocatedImage allocatedImage;
};
struct GlyphInstance {
uint16_t x;
uint16_t y;
uint32_t glyph;
uint32_t color;
};
static_assert((sizeof (GlyphInstance)) == 4 * 3);
struct Glyph {
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
};
struct font {
static constexpr int perVertexSize = (4) * 2;
static constexpr int perInstanceSize = (0) * 2;
static constexpr int perInstanceSize = (sizeof (GlyphInstance));
static constexpr int maximumGlyphCount = 1024;
VkInstance instance;
VkDevice device;
@ -49,6 +66,16 @@ namespace font::outline {
VertexIndex vertexIndex;
LoadedFont loadedFont;
VkDeviceSize instanceBufferOffset[2];
VkBuffer instanceBuffer;
VkDeviceMemory instanceMemory;
VkDeviceSize instanceMemorySize;
GlyphInstance * instanceMappedData;
VkBuffer glyphsBuffer;
VkDeviceMemory glyphsMemory;
VkDeviceSize glyphsBufferSize;
VkDescriptorPool descriptorPool{ VK_NULL_HANDLE };
static constexpr int descriptorSetLayoutCount = 1;
VkDescriptorSetLayout descriptorSetLayouts[descriptorSetLayoutCount]; // unrelated to maxFrames, unrelated to descriptorCount
@ -70,7 +97,9 @@ namespace font::outline {
void load_shader();
void create_descriptor_sets();
void write_descriptor_sets(VkImageView fontImageView);
void create_instance_buffers();
void create_pipeline();
void create_glyphs_buffer(types::font const * const font, types::glyph const * const glyphs);
void draw(VkCommandBuffer commandBuffer,
uint32_t frameIndex);

View File

@ -1,11 +1,22 @@
// set 1: constant
struct GlyphBitmap
{
uint2 Position; // x y, in texels
uint2 Size; // width height
};
// set 0: constant
[[vk::binding(0, 0)]] SamplerState ClosestSampler;
[[vk::binding(1, 0)]] Texture2D FontTexture;
[[vk::binding(2, 0)]] StructuredBuffer<GlyphBitmap> Glyphs;
struct VSInput
{
float2 Position : POSITION0;
float2 Texture : TEXCOORD0;
// per-instance
uint2 InstancePosition : InstancePosition;
uint InstanceGlyph : InstanceGlyph;
float4 InstanceColor : InstanceColor;
};
struct VSOutput
@ -17,9 +28,14 @@ struct VSOutput
[shader("vertex")]
VSOutput VSMain(VSInput input)
{
float2 inverseTexel = float2(1.0 / 256.0, 1.0 / 256.0);
float2 inversePixel = float2(1.0 / 1024.0, 1.0 / 1024.0);
int index = input.InstanceGlyph;
VSOutput output = (VSOutput)0;
output.Position = float4(input.Position, 0, 1);
output.Texture = input.Texture;
float2 position = (input.Texture * Glyphs[index].Size + input.InstancePosition) * inversePixel;
output.Position = float4(position * 2.0 - 1.0, 0, 1);
output.Texture = (input.Texture * Glyphs[index].Size + Glyphs[index].Position) * inverseTexel;
return output;
}

View File

@ -58,7 +58,9 @@ namespace font::outline {
load_shader();
create_descriptor_sets();
loadedFont = load_font(uncial_antiqua[0]);
create_glyphs_buffer(loadedFont.font, loadedFont.glyphs);
write_descriptor_sets(loadedFont.allocatedImage.imageView);
create_instance_buffers();
create_pipeline();
}
@ -201,7 +203,8 @@ namespace font::outline {
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT
};
VkVertexInputBindingDescription vertexBindingDescriptions[2]{
constexpr int vertexBindingDescriptionsCount = 2;
VkVertexInputBindingDescription vertexBindingDescriptions[vertexBindingDescriptionsCount]{
{
.binding = 0,
.stride = perVertexSize,
@ -214,7 +217,8 @@ namespace font::outline {
}
};
VkVertexInputAttributeDescription vertexAttributeDescriptions[2]{
constexpr int vertexAttributeDescriptionsCount = 5;
VkVertexInputAttributeDescription vertexAttributeDescriptions[vertexAttributeDescriptionsCount]{
// per-vertex
{ // position
.location = 0,
@ -228,14 +232,32 @@ namespace font::outline {
.format = VK_FORMAT_R16G16_SFLOAT,
.offset = 4,
},
// per-instance
{
.location = 2,
.binding = 1,
.format = VK_FORMAT_R16G16_UINT,
.offset = 0,
},
{
.location = 3,
.binding = 1,
.format = VK_FORMAT_R32_UINT,
.offset = 4,
},
{
.location = 4,
.binding = 1,
.format = VK_FORMAT_R8G8B8A8_UNORM,
.offset = 8,
},
};
VkPipelineVertexInputStateCreateInfo vertexInputState{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
//.vertexBindingDescriptionCount = 2,
.vertexBindingDescriptionCount = 1,
.vertexBindingDescriptionCount = vertexBindingDescriptionsCount,
.pVertexBindingDescriptions = vertexBindingDescriptions,
.vertexAttributeDescriptionCount = 2,
.vertexAttributeDescriptionCount = vertexAttributeDescriptionsCount,
.pVertexAttributeDescriptions = vertexAttributeDescriptions,
};
@ -358,7 +380,7 @@ namespace font::outline {
//
// pool
//
constexpr int descriptorPoolSizesCount = 2;
constexpr int descriptorPoolSizesCount = 3;
VkDescriptorPoolSize descriptorPoolSizes[descriptorPoolSizesCount]{
{ // linear sampler
.type = VK_DESCRIPTOR_TYPE_SAMPLER,
@ -367,7 +389,11 @@ namespace font::outline {
{
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.descriptorCount = 1,
}
},
{
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
},
};
VkDescriptorPoolCreateInfo descriptorPoolCreateInfo{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
@ -381,7 +407,7 @@ namespace font::outline {
// (set 0, constant)
//
{
constexpr int bindingCount = 2;
constexpr int bindingCount = 3;
VkDescriptorSetLayoutBinding descriptorSetLayoutBindings[bindingCount]{
{
.binding = 0,
@ -394,6 +420,12 @@ namespace font::outline {
.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT
},
{
.binding = 2,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_VERTEX_BIT
}
};
@ -420,7 +452,7 @@ namespace font::outline {
void font::write_descriptor_sets(VkImageView fontImageView)
{
constexpr uint32_t writeCount = 2;
constexpr uint32_t writeCount = 3;
VkWriteDescriptorSet writeDescriptorSets[writeCount];
uint32_t writeIndex = 0;
@ -448,11 +480,129 @@ namespace font::outline {
.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
.pImageInfo = &terrainDescriptorImageInfo
};
VkDescriptorBufferInfo glyphsDescriptorBufferInfo{
.buffer = glyphsBuffer,
.offset = 0,
.range = glyphsBufferSize,
};
writeDescriptorSets[writeIndex++] = {
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.dstSet = descriptorSet0,
.dstBinding = 2,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.pBufferInfo = &glyphsDescriptorBufferInfo
};
assert(writeIndex == writeCount);
vkUpdateDescriptorSets(device, writeIndex, writeDescriptorSets, 0, nullptr);
}
//////////////////////////////////////////////////////////////////////
// create instance buffer
//////////////////////////////////////////////////////////////////////
void font::create_instance_buffers()
{
constexpr VkDeviceSize bufferSize{ maximumGlyphCount * (sizeof (GlyphInstance)) };
instanceMemorySize = bufferSize * 2;
instanceBufferOffset[0] = bufferSize * 0;
instanceBufferOffset[1] = bufferSize * 1;
// create buffer
VkBufferCreateInfo bufferCreateInfo{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = instanceMemorySize,
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE
};
VK_CHECK(vkCreateBuffer(device, &bufferCreateInfo, nullptr, &instanceBuffer));
// allocate memory
VkMemoryRequirements memoryRequirements;
vkGetBufferMemoryRequirements(device, instanceBuffer, &memoryRequirements);
VkMemoryPropertyFlags memoryPropertyFlags{ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT };
VkMemoryAllocateFlags memoryAllocateFlags{};
VkDeviceSize stride;
allocateFromMemoryRequirements(device,
physicalDeviceProperties.limits.nonCoherentAtomSize,
physicalDeviceMemoryProperties,
memoryRequirements,
memoryPropertyFlags,
memoryAllocateFlags,
1,
&instanceMemory,
&stride);
VK_CHECK(vkBindBufferMemory(device, instanceBuffer, instanceMemory, 0));
// map memory
VK_CHECK(vkMapMemory(device, instanceMemory, 0, VK_WHOLE_SIZE, 0, (void **)&instanceMappedData));
}
//////////////////////////////////////////////////////////////////////
// create instance buffer
//////////////////////////////////////////////////////////////////////
void font::create_glyphs_buffer(types::font const * const font, types::glyph const * const glyphs)
{
glyphsBufferSize = (sizeof (Glyph)) * font->glyph_count;
// create buffer
VkBufferCreateInfo bufferCreateInfo{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = glyphsBufferSize,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE
};
VK_CHECK(vkCreateBuffer(device, &bufferCreateInfo, nullptr, &glyphsBuffer));
// allocate memory
VkMemoryRequirements memoryRequirements;
vkGetBufferMemoryRequirements(device, glyphsBuffer, &memoryRequirements);
VkMemoryPropertyFlags memoryPropertyFlags{ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT };
VkMemoryAllocateFlags memoryAllocateFlags{};
VkDeviceSize stride;
allocateFromMemoryRequirements(device,
physicalDeviceProperties.limits.nonCoherentAtomSize,
physicalDeviceMemoryProperties,
memoryRequirements,
memoryPropertyFlags,
memoryAllocateFlags,
1,
&glyphsMemory,
&stride);
VK_CHECK(vkBindBufferMemory(device, glyphsBuffer, glyphsMemory, 0));
// map memory
Glyph * glyphsMappedData;
VK_CHECK(vkMapMemory(device, glyphsMemory, 0, VK_WHOLE_SIZE, 0, (void **)&glyphsMappedData));
for (int i = 0; i < font->glyph_count; i++) {
glyphsMappedData[i].x = glyphs[i].bitmap.x;
glyphsMappedData[i].y = glyphs[i].bitmap.y;
glyphsMappedData[i].width = glyphs[i].bitmap.width;
glyphsMappedData[i].height = glyphs[i].bitmap.height;
}
// flush
constexpr int mappedMemoryRangesCount = 1;
VkMappedMemoryRange mappedMemoryRanges[mappedMemoryRangesCount]{
{
.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
.memory = glyphsMemory,
.offset = 0,
.size = VK_WHOLE_SIZE,
}
};
vkFlushMappedMemoryRanges(device, mappedMemoryRangesCount, mappedMemoryRanges);
vkUnmapMemory(device, glyphsMemory);
}
//////////////////////////////////////////////////////////////////////
// draw
//////////////////////////////////////////////////////////////////////
@ -460,6 +610,52 @@ namespace font::outline {
void font::draw(VkCommandBuffer commandBuffer,
uint32_t frameIndex)
{
// transfer
const char * string = "so when Nico wants to run this game on his\n4K monitor, he gets a dinky little 1280x720\nwindow instead?";
int outputIndex = 0;
int stringIndex = 0;
uint32_t x = 64 << 6;
uint32_t y = 64 << 6;
while (true) {
char c = string[stringIndex++];
if (c == 0)
break;
if (c != ' ') {
instanceMappedData[maximumGlyphCount * frameIndex + outputIndex++] = {
(uint16_t)((x + loadedFont.glyphs[c - 32].metrics.horiBearingX) >> 6),
(uint16_t)((y - loadedFont.glyphs[c - 32].metrics.horiBearingY) >> 6),
(uint32_t)(c - 32),
0xaabbccdd,
};
}
if (c == '\n') {
y += loadedFont.font->face_metrics.height * 1.2;
x = 64 << 6;
} else {
x += loadedFont.glyphs[c - 32].metrics.horiAdvance;
}
};
// flush
constexpr int mappedMemoryRangesCount = 1;
VkMappedMemoryRange mappedMemoryRanges[mappedMemoryRangesCount]{
{
.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
.memory = instanceMemory,
.offset = 0,
.size = (sizeof (GlyphInstance)),
}
};
alignMappedMemoryRanges(physicalDeviceProperties.limits.nonCoherentAtomSize,
instanceMemorySize,
mappedMemoryRangesCount,
mappedMemoryRanges);
vkFlushMappedMemoryRanges(device, mappedMemoryRangesCount, mappedMemoryRanges);
// bind/draw
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
VkDescriptorSet descriptorSets[1] = {
@ -473,9 +669,10 @@ namespace font::outline {
vkCmdBindIndexBuffer(commandBuffer, vertexIndex.buffer, vertexIndex.indexOffset, VK_INDEX_TYPE_UINT16);
VkDeviceSize vertexOffset{ 0 };
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexIndex.buffer, &vertexOffset);
VkDeviceSize vertexOffsets[2]{ 0, instanceBufferOffset[frameIndex] };
VkBuffer vertexBuffers[2]{ vertexIndex.buffer, instanceBuffer };
vkCmdBindVertexBuffers(commandBuffer, 0, 2, vertexBuffers, vertexOffsets);
vkCmdDrawIndexed(commandBuffer, 4, 1, 0, 0, 0);
vkCmdDrawIndexed(commandBuffer, 4, outputIndex, 0, 0, 0);
}
}

View File

@ -1001,7 +1001,8 @@ int main()
.imageLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.clearValue{ .color{ 0.0f, 0.0f, 0.2f, 1.0f } }
//.clearValue{ .color{ 0.0f, 0.0f, 0.2f, 1.0f } }
.clearValue{ .color{ 0.0f, 0.0f, 0.0f, 0.0f } }
};
VkRenderingAttachmentInfo depthRenderingAttachmentInfo{
.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,