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 -8
View File
@@ -20,6 +20,10 @@ const KEY_TO_DIK := {
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
}
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
# "@<revision>" of a command's name moves on (a glyph was added to the page).
var _memory_textures := {}
# True when this surface started system.py itself (run_app) and so owns the interpreter's lifetime.
var _owns_app := false
@@ -106,20 +110,34 @@ func _draw() -> void:
var segment := _clip_line(p1, p2, clip)
if segment.size() == 2:
draw_line(segment[0], segment[1], color)
elif command["kind"] == "text":
var font: Font = ThemeDB.fallback_font
if font != null:
var baseline := p1 + Vector2(0, font.get_ascent(14))
if clip.has_point(p1):
draw_string(font, baseline, command["text"], HORIZONTAL_ALIGNMENT_LEFT,
clip.end.x - p1.x, 14, color)
elif command["kind"] == "image":
var texture: Texture2D = UiAssets.load_tex(AssetRoot.path(), command["text"])
var texture: Texture2D = _texture(command["text"])
if texture != null:
var clipped := _clip_quad(command["quad"], command["uv"], clip)
if clipped[0].size() >= 3:
draw_polygon(clipped[0], PackedColorArray([color]), clipped[1], texture)
func _texture(name: String) -> Texture2D:
if not name.begins_with("mem:"):
return UiAssets.load_tex(AssetRoot.path(), name)
var at := name.find("@")
var key := name.substr(0, at) if at >= 0 else name
var revision := int(name.substr(at + 1)) if at >= 0 else 0
var entry: Array = _memory_textures.get(key, [])
if not entry.is_empty() and entry[0] == revision:
return entry[1]
var image: Image = Metin2PythonHost.memory_texture(name, -1)
if image == null:
_memory_textures.erase(key)
return null
var texture: ImageTexture = entry[1] if not entry.is_empty() else null
if texture != null and texture.get_size() == Vector2(image.get_size()):
texture.update(image)
else:
texture = ImageTexture.create_from_image(image)
_memory_textures[key] = [revision, texture]
return texture
# 40250's image quad (TL, TR, BL, BR) clipped to the viewport. The quad is a
# parallelogram, so texture coordinates are an affine function of position and are recomputed for
# the clipped outline. Returns [points, uvs]; empty points when nothing is visible.