#include "Graphics.h" #include #include FT_FREETYPE_H #include #include #include #include #define STB_IMAGE_IMPLEMENTATION #include namespace { using namespace ADVect::Graphics; bgfx::ShaderHandle loadShader(const std::string& FILENAME) { std::string shaderPath{}; switch (bgfx::getRendererType()) { case bgfx::RendererType::Noop: case bgfx::RendererType::Direct3D9: shaderPath = "shaders/dx9/"; break; case bgfx::RendererType::Direct3D11: case bgfx::RendererType::Direct3D12: shaderPath = "shaders/dx11/"; break; case bgfx::RendererType::Gnm: shaderPath = "shaders/pssl/"; break; case bgfx::RendererType::Metal: shaderPath = "shaders/metal/"; break; case bgfx::RendererType::OpenGL: shaderPath = "shaders/glsl/"; break; case bgfx::RendererType::OpenGLES: shaderPath = "shaders/essl/"; break; case bgfx::RendererType::Vulkan: shaderPath = "shaders/spirv/"; break; } std::string filePath = shaderPath + FILENAME; FILE* file = fopen(filePath.c_str(), "rb"); fseek(file, 0, SEEK_END); long fileSize = ftell(file); fseek(file, 0, SEEK_SET); const bgfx::Memory* mem = bgfx::alloc(fileSize + 1); fread(mem->data, 1, fileSize, file); mem->data[mem->size - 1] = '\0'; fclose(file); return bgfx::createShader(mem); } struct PosUVVertex { f32 x; f32 y; f32 z; f32 u; f32 v; }; static PosUVVertex quad_vert[] = { { 0.f, 0.f, 0.0f, 0.0f, 0.0f }, { 0.f, 1.f, 0.0f, 0.0f, 1.0f }, { 1.f, 0.f, 0.0f, 1.0f, 0.0f }, { 1.f, 1.f, 0.0f, 1.0f, 1.0f } }; static const u16 quad_indices[] = { 0, 2, 1, 1, 2, 3 }; FT_Library library; FT_Error error; struct Font { FT_Face face = nullptr; std::unordered_map, u32, u32, i32, i32, i32, i32>> cache{}; std::tuple, u32, u32, i32, i32, i32, i32>& get_char(NVL::Char c) { if (cache.find(c) == cache.end()) { FT_GlyphSlot slot = face->glyph; error = FT_Load_Char(face, c, FT_LOAD_RENDER); if (error) { std::cerr << "ADV: Failed to load character" << std::endl; } if (slot->bitmap.width != 0) { const bgfx::Memory* buf = bgfx::copy(slot->bitmap.buffer, slot->bitmap.pitch * slot->bitmap.rows); cache.emplace(c, std::make_tuple( bgfx::createTexture2D( slot->bitmap.width, slot->bitmap.rows, false, 1, bgfx::TextureFormat::A8, BGFX_TEXTURE_NONE | BGFX_SAMPLER_UVW_CLAMP, buf ), slot->bitmap.width, slot->bitmap.rows, slot->bitmap_left, slot->bitmap_top, slot->advance.x, slot->advance.y )); } else cache.emplace(c, std::make_tuple( std::nullopt, 0, 0, 0, 0, slot->advance.x, slot->advance.y)); } return cache[c]; } }; std::optional get_font(const char* path) { Font f; error = FT_New_Face(library, path, 0, &f.face); if (error == FT_Err_Unknown_File_Format) { std::cerr << "ADV: FreeType Unknown_File_Format: " << error << std::endl; return std::nullopt; } else if (error) { std::cerr << "ADV: FreeType font loading error: " << error << std::endl; return std::nullopt; } error = FT_Set_Char_Size(f.face, 0, 20 * 64, 72, 72); if (error) { std::cerr << "ADV: FreeType Set_Char_Size error: " << error << std::endl; return std::nullopt; } return f; } void destroy_font(Font& f) { FT_Done_Face(f.face); for (auto it = f.cache.begin(); it != f.cache.end(); it++) { if(auto& tx = std::get<0>(it->second)) bgfx::destroy(*tx); } } inline bgfx::ProgramHandle load_shader_program(const std::string& shader) { return bgfx::createProgram(loadShader(shader + ".vert.bin"), loadShader(shader + ".frag.bin"), false); } std::optional get_image_texture(const std::string& file) { ImageTexture i; i.buffer = stbi_load(file.c_str(), &i.w, &i.h, &i.channels, 0); if (i.buffer == NULL) { std::cerr << "ADV: STB IMAGE loading failed for " << file << std::endl; return std::nullopt; } const bgfx::Memory* buf = bgfx::makeRef(i.buffer, i.w * i.h * i.channels * sizeof(u8)); i.tx = bgfx::createTexture2D( i.w, i.h, false, 1, i.channels == 4 ? bgfx::TextureFormat::RGBA8 : bgfx::TextureFormat::RGB8, BGFX_TEXTURE_NONE | BGFX_SAMPLER_UVW_CLAMP, buf ); return i; } void destroy_image_texture(ImageTexture& i) { stbi_image_free(i.buffer); bgfx::destroy(i.tx); } Font regular, bold; bgfx::ProgramHandle a_program, img_program, imga_program; bgfx::UniformHandle s_texColor; bgfx::UniformHandle imga_opacity; bgfx::IndexBufferHandle ibh; bgfx::VertexBufferHandle vbh; bgfx::VertexLayout pcvDecl; std::unordered_map imgs; inline Font* ResolveStyleFlags(u32 style_flags) { Font* f = ®ular; switch (style_flags) { case TEXT_NONE: break; case TEXT_BOLD: f = &bold; break; case TEXT_ITALIC: break; case TEXT_ITALIC | TEXT_BOLD: break; } return f; } inline void HandleMarkupFlags(const NVL::Environment::MarkupSegment& s, u32& style_flags, const NVL::String*& ruby) { for (const auto& e : s.efs) { if (e.first == u"b") { style_flags |= TEXT_BOLD; } else if (e.first == u"i") { style_flags |= TEXT_ITALIC; } else if (e.first == u"u") { style_flags |= TEXT_UNDERLINE; } else if (e.first == u"s") { style_flags |= TEXT_STRIKETHROUGH; } else if (e.first == u"o") { style_flags |= TEXT_OVERLINE; } else if (e.first == u"rb") { ruby = &e.second[0]; } else { std::cerr << "ADV: Unrecognized text markup: " << NVL::to_std_string(e.first) << std::endl; } } } } namespace ADVect::Graphics { bool Init(u16 width, u16 height) { error = FT_Init_FreeType(&library); if (error) { std::cerr << "ADV: FreeType init error: " << error << std::endl; return false; } if (auto f = get_font("SourceHanSerif-Regular.ttc")) regular = *f; else return false; if (auto f = get_font("SourceHanSerif-Heavy.ttc")) bold = *f; else return false; pcvDecl.begin() .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) .end(); ibh = bgfx::createIndexBuffer(bgfx::makeRef(quad_indices, sizeof(quad_indices))); vbh = bgfx::createVertexBuffer(bgfx::makeRef(quad_vert, sizeof(quad_vert)), pcvDecl); // img_program = load_shader_program("test"); // RGBA imga_program = load_shader_program("ImageAlpha"); // RGBA + Opacity a_program = load_shader_program("AlphaStencil"); // A -> FFFA imga_opacity = bgfx::createUniform("f_opacity", bgfx::UniformType::Vec4); // x -> alpha, yzw are dummies s_texColor = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler); return true; } void Shutdown() { destroy_font(regular); destroy_font(bold); FT_Done_FreeType(library); for (auto it = imgs.begin(); it != imgs.end(); it++) { destroy_image_texture(it->second); } bgfx::destroy(a_program); //bgfx::destroy(img_program); bgfx::destroy(imga_program); bgfx::destroy(ibh); bgfx::destroy(vbh); bgfx::destroy(s_texColor); bgfx::destroy(imga_opacity); } // Draws quad with texture on Z = 0 void DrawTexture(const bgfx::TextureHandle& tex, i32 pos_x, i32 pos_y, u32 w, u32 h, u64 state, const bgfx::ProgramHandle& pg) { f32 mtx1[16], mtx2[16], mtx3[16]; bx::mtxTranslate(mtx1, pos_x, pos_y, 0.0f); bx::mtxScale(mtx2, w, h, 1.0f); bx::mtxMul(mtx3, mtx2, mtx1); bgfx::setTransform(mtx3); bgfx::setVertexBuffer(0, vbh); bgfx::setIndexBuffer(ibh); bgfx::setTexture(0, s_texColor, tex); bgfx::setState(state); bgfx::submit(0, pg); } void DrawTextureImage(const bgfx::TextureHandle& tex, i32 pos_x, i32 pos_y, u32 w, u32 h) { DrawTexture(tex, pos_x, pos_y, w, h, (BGFX_STATE_DEFAULT | BGFX_STATE_BLEND_ALPHA) & ~(BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS), img_program); } void DrawTextureImage(const ImageTexture& img, i32 pos_x, i32 pos_y) { DrawTextureImage(img.tx, pos_x, pos_y, img.w, img.h); } void DrawTextureImageAlpha(const ImageTexture& img, i32 pos_x, i32 pos_y, f32 alpha) { f32 pack[4]{}; pack[0] = alpha; bgfx::setUniform(imga_opacity, pack); DrawTexture(img.tx, pos_x, pos_y, img.w, img.h, (BGFX_STATE_DEFAULT | BGFX_STATE_BLEND_ALPHA) & ~(BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS), imga_program); } ImageTexture* GetImageTextureFromFile(const std::string& file) { if (imgs.find(file) == imgs.end()) { if (auto i = get_image_texture(file)) { imgs[file] = std::move(*i); return &imgs[file]; } else return nullptr; } else return &imgs[file]; } void DrawTextureStencilAlpha(const bgfx::TextureHandle& tex, i32 pos_x, i32 pos_y, u32 w, u32 h) { DrawTexture(tex, pos_x, pos_y, w, h, (BGFX_STATE_DEFAULT | BGFX_STATE_BLEND_ALPHA) & ~(BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS), a_program); } template void RenderGlyph(NVL::Char c, std::conditional_t pos_x, std::conditional_t pos_y, u32 col, u32 style_flags) { Font* f = ResolveStyleFlags(style_flags); auto& [tx, w, h, l, t, x, y] = f->get_char(c); pos_x += l; if (auto& buf = tx; DoRender && tx && w != 0) { DrawTextureStencilAlpha(*buf, pos_x, pos_y - (h - t), w, h); } pos_x += x >> 6; pos_y += y >> 6; } template void RenderString(const NVL::String& s, std::conditional_t pos_x, std::conditional_t pos_y, u32 col, u32 style_flags) { for (const auto& c : s) { RenderGlyph(c, pos_x, pos_y, col, style_flags); } } template void RenderString(NVL::String::const_iterator cbegin, NVL::String::const_iterator cend, std::conditional_t pos_x, std::conditional_t pos_y, u32 col, u32 style_flags) { while (cbegin < cend) { RenderGlyph(*cbegin++, pos_x, pos_y, col, style_flags); } } void RenderStringCentered(const NVL::String& s, i32 pos_x, i32 pos_y, u32 col, u32 style_flags = TEXT_NONE) { i32 copy_x = 0, copy_y = 0; RenderString(s, copy_x, copy_y, col, style_flags); RenderString(s, pos_x - copy_x / 2, pos_y, col, style_flags); } // I cannot reason this function, returns where the string would end if it was not halted i32 RenderSubstringBox(const NVL::String& s, i32& pos_x, i32& pos_y, i32 reset_x, u32 w, u32 col, u32 style_flags, size_t s_end) { Font* f = ResolveStyleFlags(style_flags); NVL::String::const_iterator last = s.cbegin(), end = s.cend(), pos = std::find(last, end, u' '), halt = s_end > s.length() ? end : last + s_end; if (pos == end) { // Render the entire thing with hard break if we don't split at all NVL::String::const_iterator last_copy = last; i32 copy_x = pos_x, copy_y = pos_y; while (last != std::min(pos, halt)) { RenderGlyph(*last++, pos_x, pos_y, col, style_flags); if (pos_x - reset_x > w) { pos_x = reset_x; pos_y -= f->face->size->metrics.height / 64; } if (last >= halt) { RenderString(last_copy, pos, copy_x, copy_y, col, style_flags); return copy_x; }; } } else while (last != end) { // Loop through all segments i32 copy_x = pos_x, old_x = pos_x, copy_y = pos_y; RenderString(last, pos, copy_x, copy_y, col, style_flags); if (copy_x - reset_x > w) { // if walk exceeded max w copy_x = reset_x, copy_y = pos_y; // try walk again from leftmost of box, see if a soft break works; reset y from last walk RenderString(last, pos, copy_x, copy_y, col, style_flags); if (copy_x - reset_x > w) { // soft break won't work, go back to old x and hard break pos_x = old_x; while (last != pos) { RenderGlyph(*last++, pos_x, pos_y, col, style_flags); if (pos_x - reset_x > w) { pos_x = reset_x; pos_y -= f->face->size->metrics.height / 64; } if (last >= halt) return copy_x; } } else { // soft break and kill beginning space pos_x = reset_x; pos_y -= f->face->size->metrics.height / 64; last++; RenderString(last, std::min(pos, halt), pos_x, pos_y, col, style_flags); if (last >= halt) return copy_x; } } else { RenderString(last, std::min(pos, halt), pos_x, pos_y, col, style_flags); if (last >= halt) return copy_x; } last = pos; if (pos != end) pos = std::find(std::next(pos), end, u' '); } return pos_x; } void RenderSubstringMarkupBox(const NVL::Environment::MarkupString& ms, i32 pos_x, i32 pos_y, u32 w, u32 col, size_t end) { i32 init_x = pos_x; size_t acc_length = 0; // We assume markups are already sorted for (const NVL::Environment::MarkupSegment& s : ms.segments) { u32 style_flags = TEXT_NONE; const NVL::String* ruby = nullptr; HandleMarkupFlags(s, style_flags, ruby); i32 last_x = pos_x, segment_end = RenderSubstringBox(s.str, pos_x, pos_y, init_x, w, col, style_flags, end - acc_length); if (ruby != nullptr && segment_end != last_x) { RenderStringCentered(*ruby, segment_end / 2 + last_x / 2, i32(pos_y + 20), col); } acc_length += s.str.length(); if (acc_length > end) return; } } }