2V0-g: port the .fnt font pipeline over a FreeType GDI emulation

CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies
verbatim; CGraphicTextInstance is verbatim except that its D3D draws become
UI render commands (one image quad per glyph, Bar for cursor/underline).

- platform/Win32Gdi.cpp: the GDI subset those files call, over vendored
  FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp
  rules; system font lookup with substitute and CJK fallback faces
- Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by
  hand, other code pages through iconv)
- CGraphicImageTexture memory textures: glyph pages reach Godot as
  "mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache
- Util.cpp code page / font face table, SetDefaultCodePage at boot,
  DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height
- TextTag, StringCodec, Arabic, Vietnamese copied from 40250
- tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop
  and python_host_test.gd now check glyph quads instead of text commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 14:17:25 +09:00
co-authored by Claude Opus 5.5
parent 141206cb69
commit afe63bfa61
272 changed files with 137097 additions and 316 deletions
+26 -5
View File
@@ -62,11 +62,28 @@ static bool py_true(const char* expression)
return truth;
}
static bool has_text(const char* text)
// CGraphicTextInstance draws one quad per glyph from a CGraphicFontTexture page ("mem:<id>@<rev>").
static int glyph_quads()
{
int count = 0;
for (const auto& command : UIRenderCommands())
count += command.kind == UIRenderCommand::Image && command.text.rfind("mem:", 0) == 0;
return count;
}
// Some glyph page holds inked pixels (A4R4G4B4 coverage in the alpha channel).
static bool glyph_page_has_ink()
{
for (const auto& command : UIRenderCommands())
if (command.kind == UIRenderCommand::Text && command.text == text)
return true;
{
UIMemoryTexture page;
if (command.kind != UIRenderCommand::Image || command.text.rfind("mem:", 0) != 0 ||
!UIRenderMemoryTexture(command.text, &page))
continue;
for (std::uint32_t argb : page.argb)
if ((argb >> 24) != 0)
return true;
}
return false;
}
@@ -153,14 +170,18 @@ int main(int argc, char** argv)
CHECK(has_board);
// PopupDialog: open with a message, render, close with ESC through the window manager's key path.
// The message is 11 glyphs of the default font (app.SetDefaultFontName, a Tahoma cell here).
const int glyphs_before = glyph_quads();
CHECK(py("_stream.popupWindow.Open('POPUP_PROBE')\nassert _stream.popupWindow.IsShow()"));
frames(1);
CHECK(has_text("POPUP_PROBE"));
CHECK(glyph_quads() >= glyphs_before + 11);
CHECK(glyph_page_has_ink());
CHECK(py("assert _stream.popupWindow.GetChild('message').GetTextSize()[0] > 0"));
PythonBoot::UIKey(DIK_ESCAPE, true);
PythonBoot::UIKey(DIK_ESCAPE, false);
frames(1);
CHECK(py("assert not _stream.popupWindow.IsShow()"));
CHECK(!has_text("POPUP_PROBE"));
CHECK(glyph_quads() == glyphs_before);
// ... and with a click on its accept button, which runs the close event. Off-centre: the button's
// caption is a 0x0 TextLine at its exact centre and CWindow::IsIn is inclusive, so in 40250 too
+145
View File
@@ -0,0 +1,145 @@
// 批次 2V0-g: the 40250 font path (CGraphicText → CGraphicFontTexture → CGraphicDib) over the GDI
// emulation in platform/Win32Gdi.cpp, and the code-page conversions CGraphicTextInstance runs text through.
//
// Needs a system Tahoma (macOS ships /System/Library/Fonts/Supplemental/Tahoma.ttf); exit 77 (ctest
// SKIP) without one, unless MT_ASSETS_STRICT=1.
#include "EterLib/StdAfx.h"
#include "EterLib/GrpText.h"
#include "EterLib/GrpTextInstance.h"
#include "EterLib/Util.h"
#include "EterLib/ResourceManager.h"
#include "../src/platform/EterLib/UIRenderCommands.h"
#include "../src/platform/UserInterface/UserInterface.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static void code_pages()
{
// CP949 "굴림" (B1BC B8B2) → U+AD74 U+B9BC, and back.
const char gulim[] = "\xb1\xbc\xb8\xb2";
WCHAR wide[8] = {};
CHECK(MultiByteToWideChar(949, 0, gulim, -1, nullptr, 0) == 3);
CHECK(MultiByteToWideChar(949, 0, gulim, -1, wide, 8) == 3);
CHECK(wide[0] == 0xAD74 && wide[1] == 0xB9BC && wide[2] == 0);
char narrow[8] = {};
CHECK(WideCharToMultiByte(949, 0, wide, -1, narrow, sizeof(narrow), nullptr, nullptr) == 5);
CHECK(std::string(narrow) == gulim);
// UTF-8 and the Windows size rules: a too-small buffer answers 0.
CHECK(MultiByteToWideChar(CP_UTF8, 0, "\xea\xb5\xb4", 3, wide, 8) == 1 && wide[0] == 0xAD74);
CHECK(MultiByteToWideChar(CP_UTF8, 0, "abc", 3, wide, 2) == 0);
// CP1252 0x80 is the euro sign.
CHECK(MultiByteToWideChar(1252, 0, "\x80", 1, wide, 8) == 1 && wide[0] == 0x20AC);
// 40250 Util.cpp face table.
CHECK(SetDefaultCodePage(949) && std::string(GetDefaultFontFace()) == "GulimChe");
CHECK(GetCharsetFromCodePage(949) == HANGUL_CHARSET);
CHECK(SetDefaultCodePage(1252) && std::string(GetDefaultFontFace()) == "Arial");
CHECK(GetDefaultCodePage() == 1252);
}
static int glyph_quads(float* min_x, float* max_x)
{
int count = 0;
*min_x = 1e9f;
*max_x = -1e9f;
for (const auto& command : UIRenderCommands())
{
if (command.kind != UIRenderCommand::Image || command.text.rfind("mem:", 0) != 0)
continue;
++count;
for (float x : command.qx)
{
*min_x = std::fmin(*min_x, x);
*max_x = std::fmax(*max_x, x);
}
}
return count;
}
int main()
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
PackSingletons();
CResourceManager resource_manager; // CResource::Release queues the resource here
code_pages();
if (access("/System/Library/Fonts/Supplemental/Tahoma.ttf", 0) != 0)
{
std::fprintf(stderr, "port_text_test: no system Tahoma\n");
return g_failures ? 1 : (is_strict ? 1 : 77);
}
// Not in any pack: CResource::Load falls through to OnLoad(0, NULL), as for every 40250 .fnt.
CGraphicText* text = new CGraphicText("Tahoma:12.fnt");
text->AddReference();
text->Load();
CHECK(!text->IsEmpty());
CGraphicTextInstance instance;
instance.SetTextPointer(text);
instance.SetValue("Hello");
instance.Update();
int width = 0, height = 0;
instance.GetTextSize(&width, &height);
// GDI lays a 12-pixel LOGFONT cell out as Tahoma's win ascent + descent at 10 ppem: 12 rows.
CHECK(height == 12);
CHECK(width > 20 && width < 40);
CHECK(instance.PixelPositionToCharacterPosition(0) == 0);
CHECK(instance.PixelPositionToCharacterPosition(width - 1) == 4);
CHECK(instance.PixelPositionToCharacterPosition(width + 5) == -1);
UIRenderSetSize(800, 600);
UIRenderBeginFrame();
instance.SetPosition(100.0f, 50.0f);
instance.Render();
float left = 0, right = 0;
CHECK(glyph_quads(&left, &right) == 5);
CHECK(std::fabs(left - 100.0f) <= 1.5f);
// Centred: the text straddles the position.
instance.SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER);
instance.Update();
UIRenderBeginFrame();
instance.Render();
CHECK(glyph_quads(&left, &right) == 5);
CHECK(left < 100.0f && right > 100.0f && std::fabs((left + right) * 0.5f - 100.0f) <= 2.0f);
// The glyph page has ink.
bool inked = false;
for (const auto& command : UIRenderCommands())
{
UIMemoryTexture page;
if (command.text.rfind("mem:", 0) == 0 && UIRenderMemoryTexture(command.text, &page))
for (std::uint32_t argb : page.argb)
inked |= (argb >> 24) != 0;
}
CHECK(inked);
instance.Destroy();
text->Release();
if (g_failures)
{
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
return 1;
}
std::printf("port_text_test: ok\n");
return 0;
}