2018-04-17 09:20:18 +02:00
|
|
|
#include "dxvk_hud_renderer.h"
|
2018-01-13 03:53:33 +01:00
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
#include <hud_line_frag.h>
|
|
|
|
#include <hud_line_vert.h>
|
|
|
|
|
|
|
|
#include <hud_text_frag.h>
|
|
|
|
#include <hud_text_vert.h>
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
namespace dxvk::hud {
|
|
|
|
|
2018-06-28 01:00:07 +02:00
|
|
|
HudRenderer::HudRenderer(const Rc<DxvkDevice>& device)
|
2018-04-17 10:01:06 +02:00
|
|
|
: m_mode (Mode::RenderNone),
|
2019-04-14 12:16:21 +02:00
|
|
|
m_surfaceSize { 0, 0 },
|
2019-05-07 22:05:35 +02:00
|
|
|
m_textShaders (createTextShaders(device)),
|
|
|
|
m_lineShaders (createLineShaders(device)),
|
2018-01-13 03:53:33 +01:00
|
|
|
m_fontImage (createFontImage(device)),
|
|
|
|
m_fontView (createFontView(device)),
|
|
|
|
m_fontSampler (createFontSampler(device)),
|
|
|
|
m_vertexBuffer (createVertexBuffer(device)) {
|
2018-06-28 01:00:07 +02:00
|
|
|
this->initFontTexture(device);
|
2018-01-13 03:53:33 +01:00
|
|
|
this->initCharMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
HudRenderer::~HudRenderer() {
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-14 12:16:21 +02:00
|
|
|
void HudRenderer::beginFrame(const Rc<DxvkContext>& context, VkExtent2D surfaceSize) {
|
2019-01-09 17:56:53 +01:00
|
|
|
auto vertexSlice = m_vertexBuffer->allocSlice();
|
2018-04-17 10:01:06 +02:00
|
|
|
context->invalidateBuffer(m_vertexBuffer, vertexSlice);
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
context->bindResourceSampler(1, m_fontSampler);
|
2019-04-03 17:27:53 +02:00
|
|
|
context->bindResourceView (1, m_fontView, nullptr);
|
2018-01-13 03:53:33 +01:00
|
|
|
|
2019-04-14 12:16:21 +02:00
|
|
|
m_mode = Mode::RenderNone;
|
|
|
|
m_surfaceSize = surfaceSize;
|
2018-01-13 03:53:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
void HudRenderer::drawText(
|
2018-01-13 03:53:33 +01:00
|
|
|
const Rc<DxvkContext>& context,
|
|
|
|
float size,
|
|
|
|
HudPos pos,
|
|
|
|
HudColor color,
|
|
|
|
const std::string& text) {
|
2019-05-07 22:05:35 +02:00
|
|
|
beginTextRendering(context);
|
|
|
|
|
|
|
|
uint32_t vertexCount = 6 * text.size();
|
|
|
|
|
|
|
|
auto vertexSlice = allocVertexBuffer(context,
|
|
|
|
vertexCount * sizeof(HudTextVertex));
|
2018-02-08 12:48:54 +01:00
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
context->bindVertexBuffer(0, vertexSlice, sizeof(HudTextVertex));
|
|
|
|
context->pushConstants(0, sizeof(color), &color);
|
|
|
|
context->draw(vertexCount, 1, 0, 0);
|
|
|
|
|
|
|
|
auto vertexData = reinterpret_cast<HudTextVertex*>(
|
|
|
|
vertexSlice.getSliceHandle().mapPtr);
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
const float sizeFactor = size / static_cast<float>(g_hudFont.size);
|
|
|
|
|
|
|
|
for (size_t i = 0; i < text.size(); i++) {
|
|
|
|
const HudGlyph& glyph = g_hudFont.glyphs[
|
|
|
|
m_charMap[static_cast<uint8_t>(text[i])]];
|
|
|
|
|
|
|
|
const HudPos size = {
|
|
|
|
sizeFactor * static_cast<float>(glyph.w),
|
|
|
|
sizeFactor * static_cast<float>(glyph.h) };
|
|
|
|
|
|
|
|
const HudPos origin = {
|
|
|
|
pos.x + sizeFactor * static_cast<float>(glyph.originX),
|
|
|
|
pos.y - sizeFactor * static_cast<float>(glyph.originY) };
|
|
|
|
|
|
|
|
const HudPos posTl = { origin.x, origin.y };
|
|
|
|
const HudPos posBr = { origin.x + size.x, origin.y + size.y };
|
|
|
|
|
|
|
|
const HudTexCoord texTl = {
|
2018-01-22 01:20:07 +01:00
|
|
|
static_cast<uint32_t>(glyph.x),
|
|
|
|
static_cast<uint32_t>(glyph.y), };
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
const HudTexCoord texBr = {
|
2018-01-22 01:20:07 +01:00
|
|
|
static_cast<uint32_t>(glyph.x + glyph.w),
|
|
|
|
static_cast<uint32_t>(glyph.y + glyph.h) };
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
vertexData[6 * i + 0].position = { posTl.x, posTl.y };
|
|
|
|
vertexData[6 * i + 0].texcoord = { texTl.u, texTl.v };
|
|
|
|
|
|
|
|
vertexData[6 * i + 1].position = { posBr.x, posTl.y };
|
|
|
|
vertexData[6 * i + 1].texcoord = { texBr.u, texTl.v };
|
|
|
|
|
|
|
|
vertexData[6 * i + 2].position = { posTl.x, posBr.y };
|
|
|
|
vertexData[6 * i + 2].texcoord = { texTl.u, texBr.v };
|
|
|
|
|
|
|
|
vertexData[6 * i + 3].position = { posBr.x, posBr.y };
|
|
|
|
vertexData[6 * i + 3].texcoord = { texBr.u, texBr.v };
|
|
|
|
|
|
|
|
vertexData[6 * i + 4].position = { posTl.x, posBr.y };
|
|
|
|
vertexData[6 * i + 4].texcoord = { texTl.u, texBr.v };
|
|
|
|
|
|
|
|
vertexData[6 * i + 5].position = { posBr.x, posTl.y };
|
|
|
|
vertexData[6 * i + 5].texcoord = { texBr.u, texTl.v };
|
|
|
|
|
|
|
|
pos.x += sizeFactor * static_cast<float>(g_hudFont.advance);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 10:01:06 +02:00
|
|
|
void HudRenderer::drawLines(
|
|
|
|
const Rc<DxvkContext>& context,
|
|
|
|
size_t vertexCount,
|
2019-05-07 22:05:35 +02:00
|
|
|
const HudLineVertex* vertexData) {
|
|
|
|
beginLineRendering(context);
|
|
|
|
|
|
|
|
auto vertexSlice = allocVertexBuffer(context,
|
|
|
|
vertexCount * sizeof(HudLineVertex));
|
|
|
|
|
|
|
|
context->bindVertexBuffer(0, vertexSlice, sizeof(HudLineVertex));
|
|
|
|
context->draw(vertexCount, 1, 0, 0);
|
2018-04-17 10:01:06 +02:00
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
auto dstVertexData = reinterpret_cast<HudLineVertex*>(
|
|
|
|
vertexSlice.getSliceHandle().mapPtr);
|
2018-04-17 10:01:06 +02:00
|
|
|
|
|
|
|
for (size_t i = 0; i < vertexCount; i++)
|
|
|
|
dstVertexData[i] = vertexData[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
DxvkBufferSlice HudRenderer::allocVertexBuffer(
|
2018-04-17 10:01:06 +02:00
|
|
|
const Rc<DxvkContext>& context,
|
2019-05-07 22:05:35 +02:00
|
|
|
VkDeviceSize dataSize) {
|
|
|
|
dataSize = align(dataSize, 64);
|
|
|
|
|
|
|
|
if (m_vertexOffset + dataSize > m_vertexBuffer->info().size) {
|
|
|
|
context->invalidateBuffer(m_vertexBuffer, m_vertexBuffer->allocSlice());
|
|
|
|
m_vertexOffset = 0;
|
2018-04-17 10:01:06 +02:00
|
|
|
}
|
2019-05-07 22:05:35 +02:00
|
|
|
|
|
|
|
DxvkBufferSlice slice(m_vertexBuffer, m_vertexOffset, dataSize);
|
|
|
|
m_vertexOffset += dataSize;
|
|
|
|
return slice;
|
2018-04-17 10:01:06 +02:00
|
|
|
}
|
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
|
|
|
|
void HudRenderer::beginTextRendering(
|
|
|
|
const Rc<DxvkContext>& context) {
|
|
|
|
if (m_mode != Mode::RenderText) {
|
|
|
|
m_mode = Mode::RenderText;
|
|
|
|
|
|
|
|
context->bindShader(VK_SHADER_STAGE_VERTEX_BIT, m_textShaders.vert);
|
|
|
|
context->bindShader(VK_SHADER_STAGE_FRAGMENT_BIT, m_textShaders.frag);
|
|
|
|
|
|
|
|
static const DxvkInputAssemblyState iaState = {
|
|
|
|
VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
|
|
|
|
VK_FALSE, 0 };
|
|
|
|
|
|
|
|
static const std::array<DxvkVertexAttribute, 2> ilAttributes = {{
|
|
|
|
{ 0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(HudTextVertex, position) },
|
|
|
|
{ 1, 0, VK_FORMAT_R32G32_UINT, offsetof(HudTextVertex, texcoord) },
|
|
|
|
}};
|
|
|
|
|
|
|
|
static const std::array<DxvkVertexBinding, 1> ilBindings = {{
|
|
|
|
{ 0, VK_VERTEX_INPUT_RATE_VERTEX },
|
|
|
|
}};
|
|
|
|
|
|
|
|
context->setInputAssemblyState(iaState);
|
|
|
|
context->setInputLayout(
|
|
|
|
ilAttributes.size(),
|
|
|
|
ilAttributes.data(),
|
|
|
|
ilBindings.size(),
|
|
|
|
ilBindings.data());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void HudRenderer::beginLineRendering(
|
|
|
|
const Rc<DxvkContext>& context) {
|
|
|
|
if (m_mode != Mode::RenderLines) {
|
|
|
|
m_mode = Mode::RenderLines;
|
|
|
|
|
|
|
|
context->bindShader(VK_SHADER_STAGE_VERTEX_BIT, m_lineShaders.vert);
|
|
|
|
context->bindShader(VK_SHADER_STAGE_FRAGMENT_BIT, m_lineShaders.frag);
|
|
|
|
|
|
|
|
static const DxvkInputAssemblyState iaState = {
|
|
|
|
VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
|
|
|
|
VK_FALSE, 0 };
|
|
|
|
|
|
|
|
static const std::array<DxvkVertexAttribute, 2> ilAttributes = {{
|
|
|
|
{ 0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(HudLineVertex, position) },
|
|
|
|
{ 1, 0, VK_FORMAT_R8G8B8A8_UNORM, offsetof(HudLineVertex, color) },
|
|
|
|
}};
|
|
|
|
|
|
|
|
static const std::array<DxvkVertexBinding, 1> ilBindings = {{
|
|
|
|
{ 0, VK_VERTEX_INPUT_RATE_VERTEX },
|
|
|
|
}};
|
|
|
|
|
|
|
|
context->setInputAssemblyState(iaState);
|
|
|
|
context->setInputLayout(
|
|
|
|
ilAttributes.size(),
|
|
|
|
ilAttributes.data(),
|
|
|
|
ilBindings.size(),
|
|
|
|
ilBindings.data());
|
|
|
|
}
|
|
|
|
}
|
2018-04-17 10:01:06 +02:00
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
|
|
|
|
HudRenderer::ShaderPair HudRenderer::createTextShaders(const Rc<DxvkDevice>& device) {
|
|
|
|
ShaderPair result;
|
|
|
|
|
|
|
|
const SpirvCodeBuffer vsCode(hud_text_vert);
|
|
|
|
const SpirvCodeBuffer fsCode(hud_text_frag);
|
2018-01-13 03:53:33 +01:00
|
|
|
|
|
|
|
// One shader resource: Global HUD uniform buffer
|
2019-05-07 22:05:35 +02:00
|
|
|
const std::array<DxvkResourceSlot, 1> vsResources = {{
|
2018-01-13 03:53:33 +01:00
|
|
|
{ 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_IMAGE_VIEW_TYPE_MAX_ENUM },
|
|
|
|
}};
|
2019-05-07 22:05:35 +02:00
|
|
|
|
2018-04-17 10:01:06 +02:00
|
|
|
// Two shader resources: Font texture and sampler
|
2019-05-07 22:05:35 +02:00
|
|
|
const std::array<DxvkResourceSlot, 1> fsResources = {{
|
2019-04-03 17:27:53 +02:00
|
|
|
{ 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_IMAGE_VIEW_TYPE_2D },
|
2018-01-13 03:53:33 +01:00
|
|
|
}};
|
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
result.vert = device->createShader(
|
|
|
|
VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
vsResources.size(),
|
|
|
|
vsResources.data(),
|
2018-04-17 10:01:06 +02:00
|
|
|
{ 0x3, 0x1 },
|
2019-05-07 22:05:35 +02:00
|
|
|
vsCode);
|
|
|
|
|
|
|
|
result.frag = device->createShader(
|
|
|
|
VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
|
|
fsResources.size(),
|
|
|
|
fsResources.data(),
|
|
|
|
{ 0x1, 0x1, 0, sizeof(HudColor) },
|
|
|
|
fsCode);
|
|
|
|
|
|
|
|
return result;
|
2018-04-17 10:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
HudRenderer::ShaderPair HudRenderer::createLineShaders(const Rc<DxvkDevice>& device) {
|
|
|
|
ShaderPair result;
|
|
|
|
|
|
|
|
const SpirvCodeBuffer vsCode(hud_line_vert);
|
|
|
|
const SpirvCodeBuffer fsCode(hud_line_frag);
|
2018-04-17 10:01:06 +02:00
|
|
|
|
2019-05-07 22:05:35 +02:00
|
|
|
// One shader resource: Global HUD uniform buffer
|
|
|
|
const std::array<DxvkResourceSlot, 1> vsResources = {{
|
|
|
|
{ 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_IMAGE_VIEW_TYPE_MAX_ENUM },
|
|
|
|
}};
|
|
|
|
|
|
|
|
result.vert = device->createShader(
|
|
|
|
VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
vsResources.size(),
|
|
|
|
vsResources.data(),
|
|
|
|
{ 0x3, 0x1 },
|
|
|
|
vsCode);
|
|
|
|
|
|
|
|
result.frag = device->createShader(
|
2018-04-17 10:01:06 +02:00
|
|
|
VK_SHADER_STAGE_FRAGMENT_BIT,
|
2019-05-07 22:05:35 +02:00
|
|
|
0, nullptr, { 0x1, 0x1 },
|
|
|
|
fsCode);
|
|
|
|
|
|
|
|
return result;
|
2018-01-13 03:53:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
Rc<DxvkImage> HudRenderer::createFontImage(const Rc<DxvkDevice>& device) {
|
2018-01-13 03:53:33 +01:00
|
|
|
DxvkImageCreateInfo info;
|
|
|
|
info.type = VK_IMAGE_TYPE_2D;
|
|
|
|
info.format = VK_FORMAT_R8_UNORM;
|
|
|
|
info.flags = 0;
|
|
|
|
info.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
info.extent = { g_hudFont.width, g_hudFont.height, 1 };
|
|
|
|
info.numLayers = 1;
|
|
|
|
info.mipLevels = 1;
|
|
|
|
info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
|
|
|
| VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
|
|
info.stages = VK_PIPELINE_STAGE_TRANSFER_BIT
|
|
|
|
| VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
|
|
info.access = VK_ACCESS_TRANSFER_WRITE_BIT
|
|
|
|
| VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
|
|
info.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
|
|
|
return device->createImage(info, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
Rc<DxvkImageView> HudRenderer::createFontView(const Rc<DxvkDevice>& device) {
|
2018-01-13 03:53:33 +01:00
|
|
|
DxvkImageViewCreateInfo info;
|
|
|
|
info.type = VK_IMAGE_VIEW_TYPE_2D;
|
|
|
|
info.format = m_fontImage->info().format;
|
2018-07-21 09:40:07 +02:00
|
|
|
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
|
2018-01-13 03:53:33 +01:00
|
|
|
info.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
|
|
info.minLevel = 0;
|
|
|
|
info.numLevels = 1;
|
|
|
|
info.minLayer = 0;
|
|
|
|
info.numLayers = 1;
|
|
|
|
|
|
|
|
return device->createImageView(m_fontImage, info);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
Rc<DxvkSampler> HudRenderer::createFontSampler(const Rc<DxvkDevice>& device) {
|
2018-01-13 03:53:33 +01:00
|
|
|
DxvkSamplerCreateInfo info;
|
|
|
|
info.magFilter = VK_FILTER_LINEAR;
|
|
|
|
info.minFilter = VK_FILTER_LINEAR;
|
|
|
|
info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
|
|
|
info.mipmapLodBias = 0.0f;
|
|
|
|
info.mipmapLodMin = 0.0f;
|
|
|
|
info.mipmapLodMax = 0.0f;
|
|
|
|
info.useAnisotropy = VK_FALSE;
|
|
|
|
info.maxAnisotropy = 1.0f;
|
|
|
|
info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
|
|
info.compareToDepth = VK_FALSE;
|
|
|
|
info.compareOp = VK_COMPARE_OP_NEVER;
|
2018-10-31 21:51:23 +01:00
|
|
|
info.borderColor = VkClearColorValue();
|
2018-01-13 03:53:33 +01:00
|
|
|
info.usePixelCoord = VK_TRUE;
|
|
|
|
|
|
|
|
return device->createSampler(info);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
Rc<DxvkBuffer> HudRenderer::createVertexBuffer(const Rc<DxvkDevice>& device) {
|
2018-01-13 03:53:33 +01:00
|
|
|
DxvkBufferCreateInfo info;
|
2019-05-07 22:05:35 +02:00
|
|
|
info.size = 1 << 16;
|
2018-01-13 03:53:33 +01:00
|
|
|
info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
|
|
|
|
info.stages = VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;
|
|
|
|
info.access = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
|
|
|
|
|
|
|
|
return device->createBuffer(info,
|
|
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
|
|
|
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
void HudRenderer::initFontTexture(
|
2018-06-28 01:00:07 +02:00
|
|
|
const Rc<DxvkDevice>& device) {
|
|
|
|
Rc<DxvkContext> context = device->createContext();
|
|
|
|
|
2018-01-13 03:53:33 +01:00
|
|
|
context->beginRecording(
|
|
|
|
device->createCommandList());
|
|
|
|
|
|
|
|
context->initImage(m_fontImage,
|
|
|
|
VkImageSubresourceRange {
|
|
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
|
|
0, 1, 0, 1 });
|
|
|
|
|
|
|
|
context->updateImage(m_fontImage,
|
|
|
|
VkImageSubresourceLayers {
|
|
|
|
VK_IMAGE_ASPECT_COLOR_BIT,
|
|
|
|
0, 0, 1 },
|
|
|
|
VkOffset3D { 0, 0, 0 },
|
|
|
|
VkExtent3D { g_hudFont.width, g_hudFont.height, 1 },
|
|
|
|
g_hudFont.texture,
|
|
|
|
g_hudFont.width,
|
|
|
|
g_hudFont.width * g_hudFont.height);
|
|
|
|
|
|
|
|
device->submitCommandList(
|
|
|
|
context->endRecording(),
|
2018-11-28 11:01:47 +01:00
|
|
|
VK_NULL_HANDLE,
|
|
|
|
VK_NULL_HANDLE);
|
2019-06-21 16:29:04 +02:00
|
|
|
|
|
|
|
context->trimStagingBuffers();
|
2018-01-13 03:53:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-17 09:20:18 +02:00
|
|
|
void HudRenderer::initCharMap() {
|
2018-01-13 03:53:33 +01:00
|
|
|
std::fill(m_charMap.begin(), m_charMap.end(), 0);
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < g_hudFont.charCount; i++)
|
|
|
|
m_charMap.at(g_hudFont.glyphs[i].codePoint) = i;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|