diff --git a/src/api/l_graphics.c b/src/api/l_graphics.c index a7e7c58c..80f7b298 100644 --- a/src/api/l_graphics.c +++ b/src/api/l_graphics.c @@ -1080,7 +1080,8 @@ static Texture* luax_opttexture(lua_State* L, int index) { } static int l_lovrGraphicsNewMaterial(lua_State* L) { - MaterialInfo info = { 0 }; + MaterialInfo info; + memset(&info, 0, sizeof(info)); Texture* texture = luax_totype(L, 1, Texture); diff --git a/src/api/l_graphics_pass.c b/src/api/l_graphics_pass.c index 32ea6b53..b68ad7b8 100644 --- a/src/api/l_graphics_pass.c +++ b/src/api/l_graphics_pass.c @@ -513,6 +513,21 @@ static int l_lovrPassCircle(lua_State* L) { return 0; } +static int l_lovrPassText(lua_State* L) { + Pass* pass = luax_checktype(L, 1, Pass); + Font* font = luax_totype(L, 2, Font); + int index = font ? 3 : 2; + size_t length; + const char* text = luaL_checklstring(L, index++, &length); + float transform[16]; + index = luax_readmat4(L, index++, transform, 1); + float wrap = luax_optfloat(L, index++, 0.); + HorizontalAlign halign = luax_checkenum(L, index++, HorizontalAlign, "center"); + VerticalAlign valign = luax_checkenum(L, index++, VerticalAlign, "middle"); + lovrPassText(pass, font, text, length, transform, wrap, halign, valign); + return 0; +} + static int l_lovrPassMesh(lua_State* L) { Pass* pass = luax_checktype(L, 1, Pass); Buffer* vertices = !lua_toboolean(L, 2) ? NULL : luax_totype(L, 2, Buffer); @@ -739,6 +754,7 @@ const luaL_Reg lovrPass[] = { { "cube", l_lovrPassCube }, { "box", l_lovrPassBox }, { "circle", l_lovrPassCircle }, + { "text", l_lovrPassText }, { "mesh", l_lovrPassMesh }, { "multimesh", l_lovrPassMultimesh }, diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 5731f612..11c3c3f1 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -33,8 +33,7 @@ typedef struct { typedef struct { struct { float x, y; } position; - struct { uint8_t r, g, b, a; } color; - struct { uint16_t u, v; } uv; + struct { float u, v; } uv; } GlyphVertex; typedef struct { @@ -125,11 +124,9 @@ struct Material { typedef struct { uint32_t codepoint; uint16_t atlas[4]; - float width; - float height; float advance; - float offsetX; - float offsetY; + float box[4]; + float uv[4]; } Glyph; struct Font { @@ -138,6 +135,7 @@ struct Font { Material* material; arr_t(Glyph) glyphs; map_t glyphLookup; + map_t kerning; float pixelDensity; Texture* atlas; uint32_t atlasWidth; @@ -286,11 +284,13 @@ static struct { gpu_stream* stream; bool hasTextureUpload; bool hasMaterialUpload; + bool hasGlyphUpload; gpu_device_info device; gpu_features features; gpu_limits limits; float background[4]; Texture* window; + Font* defaultFont; Buffer* defaultBuffer; Texture* defaultTexture; Sampler* defaultSamplers[2]; @@ -314,6 +314,7 @@ static struct { static void* tempAlloc(size_t size); static void* tempGrow(void* p, size_t size); static void beginFrame(void); +static void cleanupPasses(void); static uint32_t getLayout(gpu_slot* slots, uint32_t count); static gpu_bundle* getBundle(uint32_t layout); static gpu_texture* getAttachment(uint32_t size[2], uint32_t layers, TextureFormat format, bool srgb, uint32_t samples); @@ -457,7 +458,7 @@ bool lovrGraphicsInit(bool debug, bool vsync) { state.vertexFormats[VERTEX_POINT] = (gpu_vertex_format) { .bufferCount = 2, .attributeCount = 5, - .bufferStrides[2] = 12, + .bufferStrides[0] = 12, .attributes[0] = { 0, 10, 0, GPU_TYPE_F32x3 }, .attributes[1] = { 1, 11, 0, GPU_TYPE_F32x4 }, .attributes[2] = { 1, 12, 0, GPU_TYPE_F32x4 }, @@ -468,11 +469,11 @@ bool lovrGraphicsInit(bool debug, bool vsync) { state.vertexFormats[VERTEX_GLYPH] = (gpu_vertex_format) { .bufferCount = 2, .attributeCount = 5, - .bufferStrides[2] = 16, + .bufferStrides[0] = sizeof(GlyphVertex), .attributes[0] = { 0, 10, offsetof(GlyphVertex, position), GPU_TYPE_F32x2 }, .attributes[1] = { 1, 11, 0, GPU_TYPE_F32x4 }, - .attributes[2] = { 0, 12, offsetof(GlyphVertex, uv), GPU_TYPE_UN16x2 }, - .attributes[3] = { 0, 13, offsetof(GlyphVertex, color), GPU_TYPE_UN8x4 }, + .attributes[2] = { 0, 12, offsetof(GlyphVertex, uv), GPU_TYPE_F32x2 }, + .attributes[3] = { 1, 13, 16, GPU_TYPE_F32x4 }, .attributes[4] = { 1, 14, 0, GPU_TYPE_F32x4 } }; @@ -489,12 +490,14 @@ bool lovrGraphicsInit(bool debug, bool vsync) { void lovrGraphicsDestroy() { if (!state.initialized) return; + cleanupPasses(); lovrRelease(state.window, lovrTextureDestroy); for (uint32_t i = 0; i < state.attachments.length; i++) { gpu_texture_destroy(state.attachments.data[i].texture); free(state.attachments.data[i].texture); } arr_free(&state.attachments); + lovrRelease(state.defaultFont, lovrFontDestroy); lovrRelease(state.defaultBuffer, lovrBufferDestroy); lovrRelease(state.defaultTexture, lovrTextureDestroy); lovrRelease(state.defaultSamplers[0], lovrSamplerDestroy); @@ -653,6 +656,14 @@ void lovrGraphicsSubmit(Pass** passes, uint32_t count) { state.hasMaterialUpload = false; } + if (state.hasGlyphUpload) { + barriers[0].prev |= GPU_PHASE_TRANSFER; + barriers[0].next |= GPU_PHASE_SHADER_FRAGMENT; + barriers[0].flush |= GPU_CACHE_TRANSFER_WRITE; + barriers[0].clear |= GPU_CACHE_TEXTURE; + state.hasGlyphUpload = false; + } + // End passes for (uint32_t i = 0; i < count; i++) { streams[i + 1] = passes[i]->stream; @@ -761,25 +772,7 @@ void lovrGraphicsSubmit(Pass** passes, uint32_t count) { gpu_submit(streams, total); - // Clean up ALL passes created during the frame - for (size_t i = 0; i < state.passes.length; i++) { - Pass* pass = state.passes.data[i]; - - for (size_t j = 0; j < pass->access.length; j++) { - Access* access = &pass->access.data[j]; - lovrRelease(access->buffer, lovrBufferDestroy); - lovrRelease(access->texture, lovrTextureDestroy); - } - - for (size_t j = 0; j <= pass->pipelineIndex; j++) { - lovrRelease(pass->pipelines[j].sampler, lovrSamplerDestroy); - lovrRelease(pass->pipelines[j].shader, lovrShaderDestroy); - lovrRelease(pass->pipelines[j].material, lovrMaterialDestroy); - pass->pipelines[j].sampler = NULL; - pass->pipelines[j].shader = NULL; - pass->pipelines[j].material = NULL; - } - } + cleanupPasses(); if (state.window) { state.window->gpu = NULL; @@ -1293,6 +1286,11 @@ Shader* lovrGraphicsGetDefaultShader(DefaultShader type) { info.stages[1] = lovrBlobCreate((void*) lovr_shader_unlit_frag, sizeof(lovr_shader_unlit_frag), "Unlit Fragment Shader"); info.label = "unlit"; break; + case SHADER_FONT: + info.stages[0] = lovrBlobCreate((void*) lovr_shader_unlit_vert, sizeof(lovr_shader_unlit_vert), "Unlit Vertex Shader"); + info.stages[1] = lovrBlobCreate((void*) lovr_shader_font_frag, sizeof(lovr_shader_font_frag), "Font Fragment Shader"); + info.label = "font"; + break; default: lovrUnreachable(); } @@ -1698,8 +1696,22 @@ Font* lovrFontCreate(FontInfo* info) { font->ref = 1; font->info = *info; lovrRetain(info->rasterizer); - map_init(&font->glyphLookup, 36); arr_init(&font->glyphs, realloc); + map_init(&font->glyphLookup, 36); + map_init(&font->kerning, 36); + + // Initial atlas size must be big enough to hold any of the glyphs + float box[4]; + font->atlasWidth = 1; + font->atlasHeight = 1; + lovrRasterizerGetBoundingBox(info->rasterizer, box); + uint32_t width = (uint32_t) ceilf(box[2] - box[0]); + uint32_t height = (uint32_t) ceilf(box[3] - box[1]); + while (font->atlasWidth < 2 * width || font->atlasHeight < 2 * height) { + font->atlasWidth <<= 1; + font->atlasHeight <<= 1; + } + return font; } @@ -1708,8 +1720,9 @@ void lovrFontDestroy(void* ref) { lovrRelease(font->info.rasterizer, lovrRasterizerDestroy); lovrRelease(font->material, lovrMaterialDestroy); lovrRelease(font->atlas, lovrTextureDestroy); - map_free(&font->glyphLookup); arr_free(&font->glyphs); + map_free(&font->glyphLookup); + map_free(&font->kerning); free(font); } @@ -2803,6 +2816,312 @@ void lovrPassCircle(Pass* pass, float* transform, float angle1, float angle2, ui } } +void lovrPassText(Pass* pass, Font* font, const char* text, uint32_t length, float* transform, float wrap, HorizontalAlign halign, VerticalAlign valign) { + if (!font) { + if (!state.defaultFont) { + state.defaultFont = lovrFontCreate(&(FontInfo) { + .rasterizer = lovrRasterizerCreate(NULL, 48), + .padding = 1, + .spread = 4. + }); + } + + font = state.defaultFont; + } + + uint32_t originalGlyphsLength = font->glyphs.length; + + GlyphVertex* vertices = tempAlloc(length * 4 * sizeof(GlyphVertex)); + uint32_t vertexCount = 0; + uint32_t glyphCount = 0; + uint32_t lineCount = 1; + + // Track vertex indices for align/wrap + uint32_t lineStart = 0; + uint32_t wordStart = 0; + + // Cursor + float x = 0.f; + float y = 0.f; + float leading = lovrRasterizerGetLeading(font->info.rasterizer) * .8f; + float ascent = lovrRasterizerGetAscent(font->info.rasterizer) * .8f; + float scale = 1.f / ascent; + wrap /= scale; + + size_t bytes; + uint32_t codepoint; + uint32_t previous = '\0'; + const char* end = text + length; + while ((bytes = utf8_decode(text, end, &codepoint)) > 0) { + uint64_t hash = hash64(&codepoint, 4); + uint64_t index = map_get(&font->glyphLookup, hash); + Glyph* glyph; + + if (index == MAP_NIL) { // New glyph just dropped + map_set(&font->glyphLookup, hash, font->glyphs.length); + arr_expand(&font->glyphs, 1); + glyph = &font->glyphs.data[font->glyphs.length++]; + + glyph->codepoint = codepoint; + glyph->advance = lovrRasterizerGetGlyphAdvance(font->info.rasterizer, codepoint); + + if (lovrRasterizerIsGlyphEmpty(font->info.rasterizer, codepoint)) { + memset(glyph->atlas, 0, sizeof(glyph->atlas)); + memset(glyph->box, 0, sizeof(glyph->atlas)); + memset(glyph->uv, 0, sizeof(glyph->atlas)); + } else { + lovrRasterizerGetGlyphBoundingBox(font->info.rasterizer, codepoint, glyph->box); + + float width = glyph->box[2] - glyph->box[0]; + float height = glyph->box[3] - glyph->box[1]; + + uint32_t pixelWidth = 2 * font->info.padding + (uint32_t) ceilf(width); + uint32_t pixelHeight = 2 * font->info.padding + (uint32_t) ceilf(height); + + if (font->atlasX + pixelWidth > font->atlasWidth) { + font->atlasX = font->atlasWidth == font->atlasHeight ? 0 : font->atlasWidth >> 1; + font->atlasY += font->rowHeight; + if (font->atlasY + pixelHeight > font->atlasHeight) { + if (font->atlasWidth == font->atlasHeight) { + font->atlasX = font->atlasWidth; + font->atlasY = 0; + font->atlasWidth <<= 1; + font->rowHeight = 0; + } else { + font->atlasX = 0; + font->atlasY = font->atlasHeight; + font->atlasHeight <<= 1; + font->rowHeight = 0; + } + } + } + + glyph->atlas[0] = font->atlasX; + glyph->atlas[1] = font->atlasY; + glyph->atlas[2] = pixelWidth; + glyph->atlas[3] = pixelHeight; + + double unused; + glyph->uv[0] = font->atlasX + font->info.padding + (1.f - modf(width, &unused)) / 2.f; + glyph->uv[1] = font->atlasY + font->info.padding + (1.f - modf(height, &unused)) / 2.f; + glyph->uv[2] = glyph->uv[0] + width; + glyph->uv[3] = glyph->uv[1] + height; + + font->atlasX += pixelWidth; + font->rowHeight = MAX(font->rowHeight, font->atlasY + pixelHeight); + } + } else { + glyph = &font->glyphs.data[index]; + } + + if (codepoint == ' ') { + wordStart = vertexCount; + x += glyph->advance; + previous = '\0'; + text += bytes; + continue; + } else if (codepoint == '\n') { + if (halign != ALIGN_LEFT) { + float lineWidth = vertices[vertexCount - 1].position.x; + float align = (float) halign / 2.f * lineWidth; // Sneakily compute shift ratio from halign + for (uint32_t i = lineStart; i < vertexCount; i++) { + vertices[i].position.x -= align; + } + } + + lineStart = vertexCount; + wordStart = vertexCount; + previous = '\0'; + text += bytes; + y -= leading; + lineCount++; + x = 0.f; + continue; + } + + // Keming + if (previous != '\0') { + uint32_t codepoints[] = { previous, codepoint }; + hash = hash64(codepoints, sizeof(codepoints)); + union { float f32; uint64_t u64; } kerning = { .u64 = map_get(&font->kerning, hash) }; + + if (kerning.u64 == MAP_NIL) { + kerning.f32 = lovrRasterizerGetKerning(font->info.rasterizer, previous, codepoint); + map_set(&font->kerning, hash, kerning.u64); + } + + x += kerning.f32; + } + + previous = codepoint; + + // Wrap + if (wrap > 0.f && x + glyph->box[2] > wrap && wordStart != lineStart) { + if (halign != ALIGN_LEFT) { + float lineWidth = vertices[wordStart - 1].position.x; // Edge of last glyph before this word + float align = (float) halign / 2.f * lineWidth; // Sneakily compute shift ratio from halign + for (uint32_t i = lineStart; i < wordStart; i++) { + vertices[i].position.x -= align; + } + } + + float dx = wordStart == vertexCount ? x : vertices[wordStart].position.x; + float dy = leading; + + // Shift the vertices of the overflowing word down a line and back to the beginning + for (uint32_t i = wordStart; i < vertexCount; i++) { + vertices[i].position.x -= dx; + vertices[i].position.y -= dy; + } + + lineStart = wordStart; + lineCount++; + x -= dx; + y -= dy; + } + + // Vertices + if (glyph->atlas[2] > 0 && glyph->atlas[3] > 0) { + if (vertexCount == lineStart) { + x -= glyph->box[0]; + } + + vertices[vertexCount++] = (GlyphVertex) { + .position = { x + glyph->box[0], y + glyph->box[3] }, + .uv = { glyph->uv[0], glyph->uv[1] } + }; + + vertices[vertexCount++] = (GlyphVertex) { + .position = { x + glyph->box[2], y + glyph->box[3] }, + .uv = { glyph->uv[2], glyph->uv[1] } + }; + + vertices[vertexCount++] = (GlyphVertex) { + .position = { x + glyph->box[0], y + glyph->box[1] }, + .uv = { glyph->uv[0], glyph->uv[3] } + }; + + vertices[vertexCount++] = (GlyphVertex) { + .position = { x + glyph->box[2], y + glyph->box[1] }, + .uv = { glyph->uv[2], glyph->uv[3] } + }; + + glyphCount++; + } + + // Advance + x += glyph->advance; + text += bytes; + } + + if (halign != ALIGN_LEFT) { // Align last line + float lineWidth = vertices[vertexCount - 1].position.x; + float align = (float) halign / 2.f * lineWidth; + for (uint32_t i = lineStart; i < vertexCount; i++) { + vertices[i].position.x -= align; + } + } + + for (uint32_t i = 0; i < vertexCount; i++) { // Normalize UVs now that final atlas size is known + vertices[i].uv.u /= font->atlasWidth; + vertices[i].uv.v /= font->atlasHeight; + } + + // If any glyphs were added, resize texture if needed and paste new glyphs (the slow part) + if (font->glyphs.length > originalGlyphsLength) { + if (!font->atlas || font->atlasWidth > font->atlas->info.width || font->atlasHeight > font->atlas->info.height) { + lovrCheck(font->atlasWidth <= 65536, "Font atlas is too big!"); + + Texture* atlas = lovrTextureCreate(&(TextureInfo) { + .type = TEXTURE_2D, + .format = FORMAT_RGBA32F, + .width = font->atlasWidth, + .height = font->atlasHeight, + .depth = 1, + .mipmaps = 1, + .samples = 1, + .usage = TEXTURE_SAMPLE | TEXTURE_TRANSFER, + .label = "Font Atlas" + }); + + float clear[4] = { 0.f, 0.f, 0.f, 0.f }; + gpu_clear_texture(state.stream, atlas->gpu, clear, 0, ~0u, 0, ~0u); + + // This barrier serves 2 purposes: + // - Ensure new atlas clear is finished/flushed before copying to it + // - Ensure any unsynchronized pending uploads to old atlas finish before copying to new atlas + gpu_barrier barrier; + barrier.prev = GPU_PHASE_TRANSFER; + barrier.next = GPU_PHASE_TRANSFER; + barrier.flush = GPU_CACHE_TRANSFER_WRITE; + barrier.clear = GPU_CACHE_TRANSFER_READ; + gpu_sync(state.stream, &barrier, 1); + + if (font->atlas) { + uint32_t srcOffset[4] = { 0, 0, 0, 0 }; + uint32_t dstOffset[4] = { 0, 0, 0, 0 }; + uint32_t extent[3] = { font->atlas->info.width, font->atlas->info.height, 1 }; + gpu_copy_textures(state.stream, font->atlas->gpu, atlas->gpu, srcOffset, dstOffset, extent); + lovrRelease(font->atlas, lovrTextureDestroy); + } + + font->atlas = atlas; + + lovrRelease(font->material, lovrMaterialDestroy); + font->material = lovrMaterialCreate(&(MaterialInfo) { + .data.color = { 1.f, 1.f, 1.f, 1.f }, + .data.uvScale = { 1.f, 1.f }, + .data.sdfRange = { font->info.spread / font->atlasWidth, font->info.spread / font->atlasHeight }, + .texture = font->atlas + }); + } + + gpu_buffer* scratchpad = tempAlloc(gpu_sizeof_buffer()); + + for (uint32_t i = originalGlyphsLength; i < font->glyphs.length; i++) { + Glyph* glyph = &font->glyphs.data[i]; + uint32_t width = glyph->atlas[2]; + uint32_t height = glyph->atlas[3]; + + if (width == 0 || height == 0) { + continue; + } + + void* pixels = gpu_map(scratchpad, width * height * 16, 16, GPU_MAP_WRITE); + lovrRasterizerGetGlyphPixels(font->info.rasterizer, glyph->codepoint, pixels, width, height, font->info.spread); + uint32_t dstOffset[4] = { glyph->atlas[0], glyph->atlas[1], 0, 0 }; + uint32_t extent[3] = { width, height, 1 }; + gpu_copy_buffer_texture(state.stream, scratchpad, font->atlas->gpu, 0, dstOffset, extent); + } + + state.hasGlyphUpload = true; + } + + mat4_scale(transform, scale, scale, scale); + + float totalHeight = ascent + leading * (lineCount - 1); + mat4_translate(transform, 0.f, -ascent + valign / 2.f * totalHeight, 0.f); + + uint16_t* indices; + lovrPassDraw(pass, &(Draw) { + .mode = VERTEX_TRIANGLES, + .shader = SHADER_FONT, + .material = font->material, + .transform = transform, + .vertex.format = VERTEX_GLYPH, + .vertex.count = vertexCount, + .vertex.data = vertices, + .index.count = glyphCount * 6, + .index.pointer = (void**) &indices + }); + + for (uint32_t i = 0; i < vertexCount; i += 4) { + uint16_t quad[] = { i + 0, i + 2, i + 1, i + 1, i + 2, i + 3 }; + memcpy(indices, quad, sizeof(quad)); + indices += COUNTOF(quad); + } +} + void lovrPassMesh(Pass* pass, Buffer* vertices, Buffer* indices, float* transform, uint32_t start, uint32_t count, uint32_t instances) { if (count == ~0u) { count = (indices ? indices : vertices)->info.length - start; @@ -3070,6 +3389,28 @@ static void beginFrame(void) { arr_clear(&state.passes); } +// Clean up ALL passes created during the frame, even unsubmitted ones +static void cleanupPasses(void) { + for (size_t i = 0; i < state.passes.length; i++) { + Pass* pass = state.passes.data[i]; + + for (size_t j = 0; j < pass->access.length; j++) { + Access* access = &pass->access.data[j]; + lovrRelease(access->buffer, lovrBufferDestroy); + lovrRelease(access->texture, lovrTextureDestroy); + } + + for (size_t j = 0; j <= pass->pipelineIndex; j++) { + lovrRelease(pass->pipelines[j].sampler, lovrSamplerDestroy); + lovrRelease(pass->pipelines[j].shader, lovrShaderDestroy); + lovrRelease(pass->pipelines[j].material, lovrMaterialDestroy); + pass->pipelines[j].sampler = NULL; + pass->pipelines[j].shader = NULL; + pass->pipelines[j].material = NULL; + } + } +} + static uint32_t getLayout(gpu_slot* slots, uint32_t count) { uint64_t hash = hash64(slots, count * sizeof(gpu_slot)); diff --git a/src/modules/graphics/graphics.h b/src/modules/graphics/graphics.h index b3b00d53..5d79b8c9 100644 --- a/src/modules/graphics/graphics.h +++ b/src/modules/graphics/graphics.h @@ -249,6 +249,7 @@ const SamplerInfo* lovrSamplerGetInfo(Sampler* sampler); typedef enum { SHADER_UNLIT, + SHADER_FONT, DEFAULT_SHADER_COUNT } DefaultShader; @@ -470,6 +471,7 @@ void lovrPassLine(Pass* pass, uint32_t count, float** vertices); void lovrPassPlane(Pass* pass, float* transform, uint32_t cols, uint32_t rows); void lovrPassBox(Pass* pass, float* transform); void lovrPassCircle(Pass* pass, float* transform, float angle1, float angle2, uint32_t segments); +void lovrPassText(Pass* pass, Font* font, const char* text, uint32_t length, float* transform, float wrap, HorizontalAlign halign, VerticalAlign valign); void lovrPassMesh(Pass* pass, Buffer* vertices, Buffer* indices, float* transform, uint32_t start, uint32_t count, uint32_t instances); void lovrPassMultimesh(Pass* pass, Buffer* vertices, Buffer* indices, Buffer* indirect, uint32_t count, uint32_t offset, uint32_t stride); void lovrPassCompute(Pass* pass, uint32_t x, uint32_t y, uint32_t z, Buffer* indirect, uint32_t offset);