From cf985485e43be97c9f865cd848b83f7a8ecad58a Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 15 Apr 2026 13:28:32 -0500 Subject: [PATCH 1/3] gc-font-tool: Hack in a few additional rendering parameters. --- docs/gc-font-tool.cpp | 166 +++++++++++++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/docs/gc-font-tool.cpp b/docs/gc-font-tool.cpp index c43496d2fa..4a89c50975 100644 --- a/docs/gc-font-tool.cpp +++ b/docs/gc-font-tool.cpp @@ -13,7 +13,7 @@ // GNU General Public License for more details. // Compile with: -// g++ -O2 -std=c++11 $(freetype-config --cflags --libs) gc-font-tool.cpp +// g++ -O2 -std=c++20 $(pkg-config --cflags --libs freetype2) gc-font-tool.cpp // Yay0 // =============== @@ -75,13 +75,16 @@ // // Font data is encoded in 2 bit greyscale and in 8x8 blocks. +#include #include #include #include #include #include -#include +#include +#include #include +#include #include #include @@ -94,7 +97,6 @@ using std::uint32_t; // Font parameters const int FNT_CELL_SIZE = 24; -const int FNT_RENDER_SIZE = FNT_CELL_SIZE * 5 / 6; const int FNT_CELLS_PER_ROW = 21; const int FNT_PIXMAP_WIDTH = 512; // Must be >= CELL_SIZE * CELLS_PER_ROW @@ -762,17 +764,6 @@ static void write_le32(std::vector& output, uint32_t value) write_le16(output, static_cast(value >> 16)); } -// Clamps an integer between two values -static int clamp(int value, int min, int max) -{ - if (value < min) - value = min; - else if (value > max) - value = max; - - return value; -} - // Compresses a file using the Yay0 format static std::vector yay0_compress(const std::vector& input) { @@ -1110,11 +1101,20 @@ static std::vector fnt_to_bmp(const std::vector& input) return bitmap; } +struct render_options +{ + FT_UInt render_height = FNT_CELL_SIZE * 5 / 6; + int global_width_bias = 0; + std::map codepoint_width_biases; + bool dither = false; +}; + // Generates a GameCube font file static std::vector generate_fnt( font_type type, const std::vector& widths, - const image2& pixmap) + const image2& pixmap, + const render_options& render_opts) { std::vector out; @@ -1152,8 +1152,8 @@ static std::vector generate_fnt( // out_pixmap = output vector to store pixmap (unscrambled) static void freetype_to_fnt_data( const std::vector& font_buf, - const uint16_t* font_table, - unsigned font_table_size, + std::span font_table, + const render_options& render_opts, std::vector& out_widths, image8& out_pixmap) { @@ -1166,7 +1166,7 @@ static void freetype_to_fnt_data( throw font_error("error reading font data"); // Set size to render glyphs at - if (FT_Set_Pixel_Sizes(face, 0, FNT_RENDER_SIZE) != 0) + if (FT_Set_Pixel_Sizes(face, 0, render_opts.render_height) != 0) throw font_error("error selecting font size (is the font scalable?)"); // Get descender size in pixels (negative value) @@ -1174,16 +1174,16 @@ static void freetype_to_fnt_data( // Resize output vectors const unsigned cpr_squared = FNT_CELLS_PER_ROW * FNT_CELLS_PER_ROW; - const unsigned pages = (font_table_size + cpr_squared - 1) / cpr_squared; + const unsigned pages = (font_table.size() + cpr_squared - 1) / cpr_squared; out_widths.clear(); - out_widths.resize(font_table_size); + out_widths.resize(font_table.size()); out_pixmap.data.clear(); out_pixmap.data.resize(FNT_PIXMAP_WIDTH * FNT_PIXMAP_WIDTH * pages); out_pixmap.width = FNT_PIXMAP_WIDTH; // Render each glyph in the list - for (unsigned i = 0; i < font_table_size; i++) + for (unsigned i = 0; i < font_table.size(); i++) { unsigned glyph_index = FT_Get_Char_Index(face, font_table[i]); @@ -1195,8 +1195,19 @@ static void freetype_to_fnt_data( if (FT_Load_Glyph(face, glyph_index, FT_LOAD_RENDER) != 0) throw font_error("error loading glyph"); + // Global width adjustment. + int width_bias = render_opts.global_width_bias; + + // Per-codepoint width adjustment. + const auto bias_it = render_opts.codepoint_width_biases.find(font_table[i]); + if (bias_it != render_opts.codepoint_width_biases.end()) + { + width_bias += bias_it->second; + } + // Record width - out_widths[i] = clamp(face->glyph->metrics.horiAdvance >> 6, 0, FNT_CELL_SIZE); + const int width = face->glyph->metrics.horiAdvance >> 6; + out_widths[i] = std::clamp(width + width_bias, 0, FNT_CELL_SIZE); // Calculate cell offset within final image const unsigned cell_page = i / cpr_squared; @@ -1209,7 +1220,7 @@ static void freetype_to_fnt_data( // Copy glyph image const FT_Bitmap* bitmap = &face->glyph->bitmap; - const int xStart = face->glyph->bitmap_left; + const int xStart = face->glyph->bitmap_left + width_bias; const int yStart = FNT_CELL_SIZE + descender - face->glyph->bitmap_top; const int xMax = xStart + bitmap->width; const int yMax = yStart + bitmap->rows; @@ -1232,34 +1243,31 @@ static void freetype_to_fnt_data( } // Converts a freetype font to a GameCube compressed font -static std::vector freetype_to_fnt(const std::vector& font_buf, font_type type, bool dither) +static std::vector freetype_to_fnt(const std::vector& font_buf, font_type type, const render_options& render_opts) { // Get font table from font type - const uint16_t* font_table; - unsigned font_table_size; + std::span font_table; if (type == font_type::windows_1252) { font_table = windows_1252_font_table; - font_table_size = sizeof(windows_1252_font_table) / 2; } else { font_table = shift_jis_font_table; - font_table_size = sizeof(shift_jis_font_table) / 2; } // Generate pixmap std::vector widths; image8 pixmap; - freetype_to_fnt_data(font_buf, font_table, font_table_size, widths, pixmap); + freetype_to_fnt_data(font_buf, font_table, render_opts, widths, pixmap); // Dither image - if (dither) + if (render_opts.dither) dither_4colour(pixmap); // Scramble pixmap, generate fnt header and compress - return yay0_compress(generate_fnt(type, widths, i2encode(pixmap))); + return yay0_compress(generate_fnt(type, widths, i2encode(pixmap), render_opts)); } static void usage() @@ -1325,20 +1333,74 @@ static void write_file(const std::string& filename, const std::vector d int main(int argc, char* argv[]) { - // Get arguments - if (argc != 4) - { - usage(); - return 1; - } + render_options render_opts; try { + int arg_pos = 1; + for (; arg_pos < argc; ++arg_pos) { + + std::string_view arg{argv[arg_pos]}; + + if (!arg.starts_with("--")) + break; + + arg = arg.substr(2); + + // "--" can end argument processing. + if (arg.empty()) + { + ++arg_pos; + break; + } + + const auto equal_pos = arg.find('='); + + if (equal_pos == arg.npos) + throw std::invalid_argument{"Expected equal sign in -- argument."}; + + const auto left_side = arg.substr(0, equal_pos); + const auto right_side = arg.substr(equal_pos + 1); + + if (left_side == "render-height") + { + render_opts.render_height = std::stoi(std::string{right_side}); + continue; + } + + if (left_side == "global-width-bias") + { + render_opts.global_width_bias = std::stoi(std::string{right_side}); + continue; + } + + if (left_side == "codepoint-width-bias") + { + const auto pos_neg_pos = right_side.find_first_of("-+"); + + if (pos_neg_pos == right_side.npos) + throw std::invalid_argument{"Expected - or + sign."}; + + const uint32_t code_point = std::stoi(std::string{right_side.substr(0, pos_neg_pos)}, nullptr, 16); + const auto bias = std::stoi(std::string{right_side.substr(pos_neg_pos)}); + + render_opts.codepoint_width_biases[code_point] = bias; + continue; + } + + arg_pos = argc; + break; + } + + if (argc - arg_pos != 3) { + throw std::invalid_argument{"Unexpected number of arguments."}; + } + // Read input file - std::vector input = read_file(argv[2]); + std::vector input = read_file(argv[arg_pos + 1]); // Do operation - const std::string mode = argv[1]; + const std::string mode = argv[arg_pos + 0]; char mode_char = 0; if (mode.length() == 2 && mode[0] == '-') @@ -1351,10 +1413,20 @@ int main(int argc, char* argv[]) { case 'c': result = yay0_compress(input); break; case 'd': result = yay0_decompress(input); break; - case 'a': result = freetype_to_fnt(input, font_type::windows_1252, true); break; - case 's': result = freetype_to_fnt(input, font_type::shift_jis, true); break; - case 'b': result = freetype_to_fnt(input, font_type::windows_1252, false); break; - case 't': result = freetype_to_fnt(input, font_type::shift_jis, false); break; + case 'a': + render_opts.dither = true; + result = freetype_to_fnt(input, font_type::windows_1252, render_opts); + break; + case 's': + render_opts.dither = true; + result = freetype_to_fnt(input, font_type::shift_jis, render_opts); + break; + case 'b': + result = freetype_to_fnt(input, font_type::windows_1252, render_opts); + break; + case 't': + result = freetype_to_fnt(input, font_type::shift_jis, render_opts); + break; case 'v': result = fnt_to_bmp(input); break; default: usage(); @@ -1362,7 +1434,7 @@ int main(int argc, char* argv[]) } // Write output file - write_file(argv[3], result); + write_file(argv[arg_pos + 2], result); return 0; } catch (const std::ios_base::failure& e) @@ -1370,6 +1442,12 @@ int main(int argc, char* argv[]) std::cerr << "gc-font-tool: io error: " << std::strerror(errno) << std::endl; return 1; } + catch (const std::invalid_argument& e) + { + std::cerr << "invalid_argument: " << e.what() << std::endl; + usage(); + return 1; + } catch (const std::runtime_error& e) { std::cerr << "gc-font-tool: " << e.what() << std::endl; From 9c22edb2445e9094d2ed39601bbac270de285719 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 15 Apr 2026 14:22:12 -0500 Subject: [PATCH 2/3] gc-font-tool: Add hack to work around pixels bleeding into other characters in CleanRip. --- docs/gc-font-tool.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/gc-font-tool.cpp b/docs/gc-font-tool.cpp index 4a89c50975..9c09fe0e46 100644 --- a/docs/gc-font-tool.cpp +++ b/docs/gc-font-tool.cpp @@ -1230,7 +1230,12 @@ static void freetype_to_fnt_data( for (int x = xStart; x < xMax; x++) { // Clip pixels outsize the cell - if (y < 0 || x < 0 || x >= FNT_CELL_SIZE || y >= FNT_CELL_SIZE) + if (x >= FNT_CELL_SIZE || y >= FNT_CELL_SIZE) + continue; + + // Rendering at y index 0 is causing bleed into other characters in CleanRip. + // Using 1 here instead of 0 seems hacky, but it does avoid that issue. + if (y < 1 || x < 0) continue; // Copy pixel From 452a6140cdbc46fcab2d7404d7abb85cca83b75e Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 15 Apr 2026 14:26:23 -0500 Subject: [PATCH 3/3] Data/Sys: Update "font_western.bin" to be based on a public domain font called, "Metropolis". Source: https://github.com/dw5/Metropolis "Metropolis-SemiBold.ttf" with some minor width tweaks seems to fit nicely. Render parameters: ./gc-font-tool --render-height=22 --global-width-bias=1 --codepoint-width-bias=20+6 b Metropolis-SemiBold.ttf font_western.bin --- Data/Sys/GC/font-licenses.txt | 4 ++-- Data/Sys/GC/font_western.bin | Bin 6478 -> 7409 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Data/Sys/GC/font-licenses.txt b/Data/Sys/GC/font-licenses.txt index d5b41fbe1f..7f63c5e93a 100644 --- a/Data/Sys/GC/font-licenses.txt +++ b/Data/Sys/GC/font-licenses.txt @@ -1,8 +1,8 @@ -The two fonts in this directory (font_western.bin and font_japanese.bin) were +The font in this directory (font_japanese.bin) was generated using gc-font-tool which can be found in the docs/ directory in the dolphin source code. -Both fonts are based on Droid Sans +The font is based on Droid Sans Copyright 2006-2014, Google Corporation Licensed under the Apache License 2.0 diff --git a/Data/Sys/GC/font_western.bin b/Data/Sys/GC/font_western.bin index 863f877e9f33bc571f05d56605306680a6c68a55..7550e79d1f7e2288c56bada464a23bed5cef65f0 100644 GIT binary patch literal 7409 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 NG5dAx|NZt`_z&8foihLc literal 6478 zcmd^DeV7x~xqr`0HYbxwawfakkL*V#>@M=P$t(i8=p-}3t%!)b*mA8O5RkSAY8NVA z7MdkNYEe-B4@AGyKpE$eia(jf9`bv9f>MXrzOgvwF>_1u}^=C|%QQjN=hFUM)|?ikT>rLjxBnP=k-Z#JxnS-$?gcI|e2aOYV3 z2^v-J6i#7;Dy--W`&L6Axdd(Y+u7rc_MP`o*EtT&g;kHs4Py#Ca1czAOBeR}pt{6|M}UtX#mA74K~H?!Nr+jf00aVEL* z)3R;DU88?HdtY+aZx0O7LWmMNG*&82(o-T^V`BU~{gvqxzVFrdpZ;VdIsVqfm*v^J zvSZ0HiGHguRVWRYB4aa7<&>PijMn${9DeGVzqS7;)qU4D)Q>(IZhPXZ#h@I>cbw~(&@p4!lkAW{LOLwsExv9J0m%3q<7o}ZDUa=Cu z=|S-f#GTG{>|UrM8wF6(F;Jq8soA%j;{FJ0%3r8_35Hyr=;W0K#Jos4DWR+)t0%sHuqM5wcq^0`%#pIo&9J)Le4`j zW$|7V@%nDqyT)hk)I&N#tMMqncSgOF$XEW{e>@MqdHTWY!T;1>@l|E*<@M&n`gO$Avd))h*>9qMx??7njap|?U zkMq>r4vO?y&u5K0xg~LY$31Is#wXn(-72AuJG~=ZcT00!C7)+;1fx~@S_KI{*BEfM zpoPxm^A32$>z-2tu54_Sxth43dV%iV&x<#E@#R=DbwCmY4i@-y5+ zhY1>vJMoEhNLmY_s%!t)*PIed1F4EJv<#2BPT9AcwATFxIvr1;g$Ex+m#gdI3JkTR zdKBl6U+6Pp@!YKGs0%{X6%hLI(2Q*W@eWIfdt}CijAHZmwmaVLQ8v0Eb}-?;kLz0a~{a7wuL}xeB9j1$E4Tq!rrgpd)BPqbR7((N2LW^r4W+lnL-sO zpG?9p|MF1npb&c@_DoR7F;4uK=}N8a)E7{@YmP0)k48cEy*S)-J-@i_F0tpsgoK_1 ziBwMGI^&-50K5yi%7jSE-~Z(Sh;jP5Sozxkl?7N`7Gl`-kgfK1B%@`ZKnx8tG{WrX z8-}Ncrr5E4tK_TNWsS^Awj#@G9ZCllJ*^>Sy`xS@t$rUubLBnzt|pMOCO~l~=1f4y zn~Ub!uJ@u3+glYRFT&1YZ~Gh#l-3Z}_j)JS8BJMIjr(vfPaV(2=pQ0et9~zmN4=px z$l_*x^r@lwDQQFF3LV*6Kdgil{_3*_1k#YGSP<)Vd<3zc+!seVw%_Snj|`bojeR?t zxIV74EaBMXNCU8afTL+ap#wpyd>C?x(^Y$|+nwHLx549!&odn<=fc*+>6EBVf3a%% zji>Q-6Rz3iaM2rUjn%#)Zdf6ge?eHyxc}g~F`lk?-tuVw^eC~c;=h7!1StQ`VI3bV zb5)%1qIq852cYPT3lD#Qe)604r~9l4A=I=VLcNXd&7d5839apUi@W(%>RroI=^~8s z^cQ~s7`y}p8-kVJedg&H6UI4LTpJ~GKf@<2GngLupV` zn7Hj-0O{e%wRJ+X?;l?yBAJIKFSzr6)I{#=5Vu$8$qJh1C{F zgV0`gjC`f;?mA+5x1uCnk`S3&N7ub8=ITyVY>>)cf1f+!MY;eX*T*-V^CHeuvHrFf z7B%sdAG1b8!u$zcvU6MK5$ij@-Q4&odq*Pt-ZW(2b*24>)u34R{&??5$P$`2ir>`6 zb;K=u^9pY1A1SMoU+q)q*kb@k+-}+1{fJH0+7@n>^ zwNsD~dfIYcB-xmtWEI!FrtuF6yj*_uf=_?94c*}W(-qbZ#iXP|FwcWqul6d=N{7(B zbW;p^rB#r7;9Q-xH-o!+9>tpTY}vzjs0ZB;j3VKjjw(i*#@1i}+bq-WD6$WrWe0|^ zyY&$%?_6vs-Z?F?rqi_x$3MM=RGgQXOSB&HEk{iaiQ0R-XQdP9grPKbPH_s9Y5Tr7 zeendZxCWOka5w1ndrtr^FroMBWJ|vTg<&pA+?^c%IjBeJxvZhmb+12sg5`@Z6 zOnVRA+?XARIUfNfqga|3HdV~69`*X0ze?Ud9dq?Xfh@a&A@Yh-pv?7x;u>!r`{_Bz zRXy*l0mb#QP>UUS}k{m`f9Y^gQ! znp;4*DHWH!^FWEW-y4IF*thtec&G3dD2ql}{Jz9E`mruluGSYyg8$N~&gcfK?Eg3K zt=Gt_wo9n=2`F9!Y!5ZB+_liPpQqlCld1ZlLM;817fM0U& zUVs*M_9*sM_nrV~XySg)s+0(7Z+J+0%sTm_Td(~QoAIWtI}YI6JnhQ+xXoPOj(X>3 z5;9%uo>OsB;(ed#$mFYiK|+$??A@>{_yIcTS>btk292T%FSE5n?1*(JUIp!}5U+cd zkGCovy_}ofVTkx8bx@X@)AOEi{&wA3tn3-P^;S>}fGZf@mHnxTQFGOsC(GJEdFmIS z)Yh25t9%eKF9C(?W6vcv0cVF0|L!K3)hc%Ou4cPGfZAV)T3c$P@VK)*JqOmJVe>Fz z8VqpveKnoaLx-3WzIBlOK8rA`(@asjpW#jlO=p=pom)tIC%Bea6(R|0lx>w5A5?Yk*_c4V4@# zsgbm@pO%$b?4r|AW;TmrnYj#$Wo3fXZnHT=tHX}XqSfh;Y?4hB?Y45W#U|PWn-z<; zvNCH~h0P*ho0V9t0v53dBA7&rty(mfgUMvESgbA^5d@nPTSc?eDu@`{Z1yq}##Taz zV0W?$4u>R4PQh9x+H6*vwJA`ey63|d7-)eup^c|9%|KQVv!0_|nvW8UD4rnK>4xAo z__P?U$#l7oPgoDBR<0ycnU4x#R1}ZZ;7KJ{qeEKMaxQEI3^HixV4$i^ZJAkTXteem zm* {*_X^cni?WWsr86aK8-456~f|7$s2aNTN=6~p5(Zay@BXjzt&^pw1j=8OqC;P8($(0jm|j& zh{cQK{C4WEU3+b_TglU;B-cWcFX@M~FevV*p}~YIbn+#!0LQ@OFvR{^4|I-O9jUDB zRY5B$DU%$a+DMSoO=8~aLkD<@16+`k6L^vXk4NM4imvH64-voyH@n}^BRH>>b)!6A zp*#2%S@pH4svmsR(bq066G?mI#S!il{|0`LDtaD}iaE1CP!Rw{28)LeuzxZMzG!km zFrj4aGG}um0h?3A?)7a7RAza1i|D!hVID-ERpo*S2T9`+q>hOyDq61w66J}xQBTeD zI6N9?QLTuIybvV$QZOZYYs@7%XQj0NvT0_Q)!+<%Fv}Z!9$O3ua{tyL(y6T z1|06L^UXCXQM=W=me-1b0Ctc}AgdbcQ8lcps!x^qV`7-79<^mlml{xIRaAMEYlk7o zp&*|@5KL($OH)ZzIGq$2tZ`kK_i%(KOYk0kh#xj}i7E*YpGWo)m4wBkgDUa)9e8jG z27D3axgtL)hGYVMyqZP4U{vB}%;e60`v{HuSBA*3okEmwUv_Lfx zUl!9TeJ&KCTjUu(CTm+%o`K10PPow8{x>77@ zDUn7-3cL|WX*n%mgfT8Dv|AF#wM3XNAPX)isj!&mSqG;-Z%G-Uo?1-hM1W^}!Ae3b z`7B7ccpErHJr*>yd@!u~uW92JPv%lZoOe$#H_RimTPzIN+m35RO(&DdWJIqkKyo&v z8(+}#Vlt&%BA|_^Jwh0?oLH(k#%l@D(QS2sLN$t#cPO00Q*>h0)@=5Kd0T)wOpe;i z$&|Gkfw=T911!NV_jV1@taMKxzHwklH&Fg$d8!!tPAwV4Ebc7$s>L9NoL9C4!w;~` z8Vy-}AZnzI*RsFKoJn6yzkEy#5G6>C;Q|Rex;%bjq5t05g&4I(UE+wcMJk(?CVGT+_7jMdI?svLFL9JhX2j(*IKenMp`{n-1M81R`h8 zH`KN<6;(yBLkn^ch9H)foGwmFF;!rlxBfb_MrS8BiqKHUFjXgt4GogtU)#-6$uHT& z8crT!=hI8lkK(o#V&Fxq+FI10=T&h89MIA>l083oOpR{O4yelKfM$}5fj6NjAN4t9 z2nOU9IgG#F)xeIyd|g+U(LgeqAvb8OswjHxzfOOU{&{*cmM2sH$hNZ?N2SlX? zyVE~2JfCNmrS>0`3;F_1o)Wg~1n_C5te-k+z+Vz`gm)mqiz71a0nCj66{$H1Dxn$F zpk;Ox4aFeGi89Gey6SL?JYSu!wzkN2T-bD|4eD)B|4dtPixC)TGn6{Cliy8<-|q{M zC}AnGYEHY>5}e;QKhUbp54N<~)$FE1TU&kJmaRAH^KGiqhE*aT!8Oe2rd-I(pn`QD z{7k3r@s;8-Cv}KXEiPhGMJ}gmJy=Jh(3z2=*WlgUgR><)pAB6rOSLt#BkAXAm8Ddn z#XI4q4OmyAGu&d@mu*`rfSy5SU@bn@?LgV z_U3GTc7E2#?&f3sfAWK1piCtFbMdEp$5QX8a3TBK>=Wjwm}c#+e;P=?n$5F3-{=V( zow8p-<$fP0gFmP%0gg%6B;niY$z-R`H?tZQR(@njV;z1bQ7h*N=20XiJGyemk7UiPhR7gyb)GL)G2NnehwD!M8FYi(*9uj4L56xEW#8| zicyOPGyhr5%9es7nBu2Q%%tLg5`{We5%B-;hLTq2scX1doYWHgaPN@d(|7i}hlIr>d)@<&N?BJW6vaWf?+IbETQ+>Sa~UT0hr(>o7?kSMorU%B1D* z$`1o!PCO*8K)^`zgy+36pW=)|ONNa19NFc~)y2@kF%J{OSPD1lh6UU*%U|$sP&N Og1M2k;{Wf*!|*@iX1>w@