????????最近做了個使用 retro-go 的開源掌機 基于ESP32-S3的C19掌機(適配GBC外殼) - 立創開源硬件平臺 ,做完后用提供的固件發現屏幕反顯了,估計是屏幕型號不太對,隨即自己拉 retro-go 官方庫來編譯,拉取的最新的 1.45 ,記錄下適配的過程和解決的一些問題。
- 安裝 esp-idf 5.2 ,retro-go 介紹可以用idf5.3 但我用5.3報錯
- 拉取 retro-go ,在/components/retro-go/targets 下增加 一個target ,參考已經項目與自己硬件最接近版本,注意 key map 的設置格式
- 在/components/retro-go 下的 config.h 中 參考其它的target 將自己增加的target 引入進來
支持中文:
- 找一個中文的 ttf 可以用這個 https://github.com/TakWolf/fusion-pixel-font
- 用 font_converter.py 設置路徑與字符范圍及字體大小后生成c源文件
- 在 fonts.h 中增加生成的源文件內的變量名和設置枚舉與 fonts 數組
- sdkconfig 中設置 CONFIG_FATFS_API_ENCODING_UTF_8=y 開啟 fatfs 使用unicode 編碼讀取文件名
如果不開啟 utf8 會導致 rg_utf8_get_codepoint 方法后臺報 invalid utf-8 prefix,也就是無法正確得到文件名 。
參考:
Configuration Options Reference - ESP32-S3 - — ESP-IDF 編程指南 v5.5 文檔
https://elm-chan.org/fsw/ff/doc/config.html#lfn_unicode
解決開啟中文瀏覽列表會很卡的問題
????????增加中文字體后且能成功顯示中文字體后,會發現列表變得很卡,查看源碼后知道其查詢字庫是一個個遍歷的,字符數變多后肯定會卡,解決方法是字符數據定長或再加一個索引數組。不管哪種方法其本質都是希望能通過字符號+偏移量定位到具體字符數據,下面是主要代碼:
typedef struct
{char name[16];uint8_t type; // 0=monospace, 1=proportional ,2= location by mapuint8_t width; // median width of glyphsuint8_t height; // height of tallest glyphsize_t chars; // glyph countconst uint32_t *map; //索引數組uint32_t map_len;//索引數組長度uint32_t map_start_code;//索引的第一個字符碼uint8_t data[]; // stream of rg_font_glyph_t (end of list indicated by an entry with 0x0000 codepoint)
} rg_font_t;//rg_gui.c get_glyph 增加 font->type == 2 的邏輯 小于等于255 直接查詢,從map_start_code 開始使用索引
// 這只是方法的一部分
static size_t get_glyph(uint32_t *output, const rg_font_t *font, int points, int c)
{// Some glyphs are always zero widthif (!font || c == '\r' || c == '\n' || c == 0) // || c < 8 || c > 0xFFFF)return 0;if (points <= 0)points = font->height;const uint8_t *ptr = font->data;const rg_font_glyph_t *glyph = (rg_font_glyph_t *)ptr;if(font->type == 2){if (c <= 255){int times =0;while (glyph->code && glyph->code != c && times++ <=255){if (glyph->width != 0)ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;ptr += sizeof(rg_font_glyph_t);glyph = (rg_font_glyph_t *)ptr;}}else if(c >= font->map_start_code){uint32_t map_index = c - font->map_start_code;if (map_index < font->map_len){uint32_t data_index = font->map[map_index];glyph = (rg_font_glyph_t *)(ptr + data_index);}}}else{// for (size_t i = 0; i < font->chars && glyph->code && glyph->code != c; ++i)while (glyph->code && glyph->code != c){if (glyph->width != 0)ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;ptr += sizeof(rg_font_glyph_t);glyph = (rg_font_glyph_t *)ptr;}}
?
????????修改后的庫:https://github.com/longxiangam/retro-go
? ? ? ?tools 下生成字庫的 font_converter.py 腳本增加對索引的支持,使用這個腳本生成字庫時選擇生成 map 并設置 start code 生成的代碼就會生成 索引數組。
from PIL import Image, ImageDraw, ImageFont
from tkinter import Tk, Label, Entry, StringVar, Button, Frame, Canvas, filedialog, ttk, Checkbutton, IntVar
import os
import re
import uuid################################ - Font format - ################################
#
# font:
# |
# ├── glyph_bitmap[] -> 8 bit array containing the bitmap data for all glyph
# |
# └── glyph_data[] -> struct that contains all the data to correctly draw the glyph
#
######################## - Explanation of glyph_bitmap[] - #######################
# First, let's see an example : '!'
#
# we are going to convert glyph_bitmap[] bytes to binary :
# 11111111,
# 11111111,
# 11000111,
# 11100000,
#
# then we rearrange them :
# [3 bits wide]
# 111
# 111
# 111
# [9 111 We clearly reconize '!' character
# bits 111
# tall] 111
# 000
# 111
# 111
# (000000)
#
# Second example with '0' :
# 0x30,0x04,0x07,0x09,0x00,0x07,
# 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C,
#
# - width = 0x07 = 7
# - height = 0x09 = 9
# - data[n] = 0x7D,0xFB,0xBF,0x7E,0xFD,0xFB,0xFF,0x7C
#
# in binary :
# 1111101
# 11111011
# 10111111
# 1111110
# 11111101
# 11111011
# 11111111
# 1111100
#
# We see that everything is not aligned so we add zeros ON THE LEFT :
# ->01111101
# 11111011
# 10111111
# ->01111110
# 11111101
# 11111011
# 11111111
# ->01111100
#
# Next, we rearrange the bits :
# [ 7 bits wide]
# 0111110
# 1111110
# 1110111
# [9 1110111
# bits 1110111 we can reconize '0' (if you squint a loooot)
# tall] 1110111
# 1110111
# 1111111
# 0111110
# (0)
#
# And that's basically how characters are encoded using this tool# Example usage (defaults parameters)
list_char_ranges_init = "32-126, 160-255,19968-40959"
font_size_init = 12
map_start_code_init = "19968" # Default map start codefont_path = ("arial.ttf") # Replace with your TTF font path# Variables to track panning
start_x = 0
start_y = 0def get_char_list():list_char = []for intervals in list_char_ranges.get().split(','):first = intervals.split('-')[0]# we check if the user input is a single char or an intervaltry:second = intervals.split('-')[1]except IndexError:list_char.append(int(first))else:for char in range(int(first), int(second) + 1):list_char.append(char)return list_chardef find_bounding_box(image):pixels = image.load()width, height = image.sizex_min, y_min = width, heightx_max, y_max = 0, 0for y in range(height):for x in range(width):if pixels[x, y] >= 1: # Looking for 'on' pixelsx_min = min(x_min, x)y_min = min(y_min, y)x_max = max(x_max, x)y_max = max(y_max, y)if x_min > x_max or y_min > y_max: # No target pixels foundreturn Nonereturn (x_min, y_min, x_max+1, y_max+1)def load_ttf_font(font_path, font_size):# Load the TTF fontenforce_font_size = enforce_font_size_bool.get()pil_font = ImageFont.truetype(font_path, font_size)font_name = ' '.join(pil_font.getname())font_data = []for char_code in get_char_list():char = chr(char_code)image = Image.new("1", (font_size * 2, font_size * 2), 0) # generate mono bmp, 0 = black colordraw = ImageDraw.Draw(image)# Draw at pos 1 otherwise some glyphs are clipped. we remove the added offset belowdraw.text((1, 0), char, font=pil_font, fill=255)bbox = find_bounding_box(image) # Get bounding boxif bbox is None: # control character / spacewidth, height = 0, 0offset_x, offset_y = 0, 0else:x0, y0, x1, y1 = bboxwidth, height = x1 - x0, y1 - y0offset_x, offset_y = x0, y0if offset_x:offset_x -= 1try: # Get the real glyph width including padding on the right that the box will removeadv_w = int(draw.textlength(char, font=pil_font))adv_w = max(adv_w, width + offset_x)except:adv_w = width + offset_x# Shift or crop glyphs that would be drawn beyond font_size. Most glyphs are not affected by this.# If enforce_font_size is false, then max_height will be calculated at the end and the font might# be taller than requested.if enforce_font_size and offset_y + height > font_size:print(f" font_size exceeded: {offset_y+height}")if font_size - height >= 0:offset_y = font_size - heightelse:offset_y = 0height = font_size# Extract bitmap datacropped_image = image.crop(bbox)bitmap = []row = 0i = 0for y in range(height):for x in range(width):if i == 8:bitmap.append(row)row = 0i = 0pixel = 1 if cropped_image.getpixel((x, y)) else 0row = (row << 1) | pixeli += 1bitmap.append(row << 8-i) # to "fill" with zero the remaining empty bitsbitmap = bitmap[0:int((width * height + 7) / 8)]# Create glyph entryglyph_data = {"char_code": char_code,"ofs_y": int(offset_y),"box_w": int(width),"box_h": int(height),"ofs_x": int(offset_x),"adv_w": int(adv_w),"bitmap": bitmap,}font_data.append(glyph_data)# The font render glyphs at font_size but they can shift them up or down which will cause the max_height# to exceed font_size. It's not desirable to remove the padding entirely (the "enforce" option above), # but there are some things we can do to reduce the discrepency without affecting the look.max_height = max(g["ofs_y"] + g["box_h"] for g in font_data)if max_height > font_size:min_ofs_y = min((g["ofs_y"] if g["box_h"] > 0 else 1000) for g in font_data)for key, glyph in enumerate(font_data):offset = glyph["ofs_y"]# If there's a consistent excess of top padding across all glyphs, we can remove itif min_ofs_y > 0 and offset >= min_ofs_y:offset -= min_ofs_y# In some fonts like Vera and DejaVu we can shift _ and | to gain an extra pixelif chr(glyph["char_code"]) in ["_", "|"] and offset + glyph["box_h"] > font_size and offset > 0:offset -= 1font_data[key]["ofs_y"] = offsetmax_height = max(g["ofs_y"] + g["box_h"] for g in font_data)print(f"Glyphs: {len(font_data)}, font_size: {font_size}, max_height: {max_height}")return (font_name, font_size, font_data)def load_c_font(file_path):# Load the C fontfont_name = "Unknown"font_size = 0font_data = []with open(file_path, 'r', encoding='UTF-8') as file:text = file.read()text = re.sub('//.*?$|/\*.*?\*/', '', text, flags=re.S|re.MULTILINE)text = re.sub('[\n\r\t\s]+', ' ', text)# FIXME: Handle parse errors...if m := re.search('\.name\s*=\s*"(.+)",', text):font_name = m.group(1)if m := re.search('\.height\s*=\s*(\d+),', text):font_size = int(m.group(1))if m := re.search('\.data\s*=\s*\{(.+?)\}', text):hexdata = [int(h, base=16) for h in re.findall('0x[0-9A-Fa-f]{2}', text)]while len(hexdata):char_code = hexdata[0] | (hexdata[1] << 8)if not char_code:breakofs_y = hexdata[2]box_w = hexdata[3]box_h = hexdata[4]ofs_x = hexdata[5]adv_w = hexdata[6]bitmap = hexdata[7:int((box_w * box_h + 7) / 8) + 7]glyph_data = {"char_code": char_code,"ofs_y": ofs_y,"box_w": box_w,"box_h": box_h,"ofs_x": ofs_x,"adv_w": adv_w,"bitmap": bitmap,}font_data.append(glyph_data)hexdata = hexdata[7 + len(bitmap):]return (font_name, font_size, font_data)def generate_font_data():if font_path.endswith(".c"):font_name, font_size, font_data = load_c_font(font_path)else:font_name, font_size, font_data = load_ttf_font(font_path, int(font_height_input.get()))window.title(f"Font preview: {font_name} {font_size}")font_height_input.set(font_size)max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))bounding_box = bounding_box_bool.get()canvas.delete("all")offset_x_1 = 1offset_y_1 = 1for glyph_data in font_data:offset_y = glyph_data["ofs_y"]width = glyph_data["box_w"]height = glyph_data["box_h"]offset_x = glyph_data["ofs_x"]adv_w = glyph_data["adv_w"]if offset_x_1+adv_w+1 > canva_width:offset_x_1 = 1offset_y_1 += max_height + 1byte_index = 0byte_value = 0bit_index = 0for y in range(height):for x in range(width):if bit_index == 0:byte_value = glyph_data["bitmap"][byte_index]byte_index += 1if byte_value & (1 << 7-bit_index):canvas.create_rectangle((x+offset_x_1+offset_x)*p_size, (y+offset_y_1+offset_y)*p_size, (x+offset_x_1+offset_x)*p_size+p_size, (y+offset_y_1+offset_y)*p_size+p_size,fill="white")bit_index += 1bit_index %= 8if bounding_box:canvas.create_rectangle((offset_x_1+offset_x)*p_size, (offset_y_1+offset_y)*p_size, (width+offset_x_1+offset_x)*p_size, (height+offset_y_1+offset_y)*p_size, width=1, outline="red", fill='')canvas.create_rectangle((offset_x_1)*p_size, (offset_y_1)*p_size, (offset_x_1+adv_w)*p_size, (offset_y_1+max_height)*p_size, width=1, outline='blue', fill='')offset_x_1 += adv_w + 1return (font_name, font_size, font_data)def save_font_data():font_name, font_size, font_data = generate_font_data()filename = filedialog.asksaveasfilename(title='Save Font',initialdir=os.getcwd(),initialfile=f"{font_name.replace('-', '_').replace(' ', '')}{font_size}",defaultextension=".c",filetypes=(('Retro-Go Font', '*.c'), ('All files', '*.*')))if filename:with open(filename, 'w', encoding='UTF-8') as f:f.write(generate_c_font(font_name, font_size, font_data))def generate_c_font(font_name, font_size, font_data):normalized_name = f"{font_name.replace('-', '_').replace(' ', '')}{font_size}"max_height = max(font_size, max(g["ofs_y"] + g["box_h"] for g in font_data))memory_usage = sum(len(g["bitmap"]) + 7 for g in font_data) # 7 bytes for header# Calculate map data if enabledgenerate_map = generate_map_bool.get()map_start_code = int(map_start_code_input.get()) if generate_map else 0map_data = []if generate_map:# Find the range for the mapchar_codes = [g["char_code"] for g in font_data]max_char = max(char_codes)map_size = max_char - map_start_code + 1map_data = [0] * map_size # Initialize with zerosdata_index = 0for glyph in font_data:map_index = glyph["char_code"] - map_start_codeif 0 <= map_index < map_size:map_data[map_index] = data_indexdata_index += 7 + len(glyph["bitmap"]) # 7 bytes header + bitmap sizememory_usage += map_size * 4 # Each map entry is 4 bytes (uint32_t)file_data = "#include \"../rg_gui.h\"\n\n"file_data += "// File generated with font_converter.py (https://github.com/ducalex/retro-go/tree/dev/tools)\n\n"file_data += f"// Font : {font_name}\n"file_data += f"// Point Size : {font_size}\n"file_data += f"// Memory usage : {memory_usage} bytes\n"file_data += f"// # characters : {len(font_data)}\n"if generate_map:file_data += f"// Map start code : {map_start_code}\n"file_data += f"// Map size : {len(map_data)} entries\n"file_data += "\n"font_type = 1;if generate_map:file_data += f"static const uint32_t font_{normalized_name}_map[] = {{\n"for i in range(0, len(map_data), 8):line = map_data[i:i+8]file_data += " " + ", ".join([f"0x{val:04X}" for val in line]) + ",\n"file_data += "};\n\n"font_type = 2;file_data += f"const rg_font_t font_{normalized_name} = {{\n"file_data += f" .name = \"{font_name}\",\n"file_data += f" .type = {font_type},\n"file_data += f" .width = 0,\n"file_data += f" .height = {max_height},\n"file_data += f" .chars = {len(font_data)},\n"if generate_map:file_data += f" .map_start_code = {map_start_code},\n"file_data += f" .map = font_{normalized_name}_map,\n"file_data += f" .map_len = sizeof(font_{normalized_name}_map) / 4,\n"file_data += f" .data = {{\n"for glyph in font_data:char_code = glyph['char_code']header_data = [char_code & 0xFF, char_code >> 8, glyph['ofs_y'], glyph['box_w'],glyph['box_h'], glyph['ofs_x'], glyph['adv_w']]file_data += f" /* U+{char_code:04X} '{chr(char_code)}' */\n "file_data += ", ".join([f"0x{byte:02X}" for byte in header_data])file_data += f",\n "if len(glyph["bitmap"]) > 0:file_data += ", ".join([f"0x{byte:02X}" for byte in glyph["bitmap"]])file_data += f","file_data += "\n"file_data += "\n"file_data += " // Terminator\n"file_data += " 0x00, 0x00,\n"file_data += " },\n"file_data += "};\n"return file_datadef select_file():filename = filedialog.askopenfilename(title='Load Font',initialdir=os.getcwd(),filetypes=(('True Type Font', '*.ttf'), ('Retro-Go Font', '*.c'), ('All files', '*.*')))if filename:global font_pathfont_path = filenamegenerate_font_data()# Function to zoom in and out on the canvas
def zoom(event):scale = 1.0if event.delta > 0: # Scroll up to zoom inscale = 1.2elif event.delta < 0: # Scroll down to zoom outscale = 0.8# Get the canvas size and adjust scale based on cursor positioncanvas.scale("all", event.x, event.y, scale, scale)# Update the scroll region to reflect the new scalecanvas.configure(scrollregion=canvas.bbox("all"))def start_pan(event):global start_x, start_y# Record the current mouse positionstart_x = event.xstart_y = event.ydef pan_canvas(event):global start_x, start_y# Calculate the distance moveddx = start_x - event.xdy = start_y - event.y# Scroll the canvascanvas.move("all", -dx, -dy)# Update the starting positionstart_x = event.xstart_y = event.yif __name__ == "__main__":window = Tk()window.title("Retro-Go Font Converter")# Get screen width and heightscreen_width = window.winfo_screenwidth()screen_height = window.winfo_screenheight()# Set the window size to fill the entire screenwindow.geometry(f"{screen_width}x{screen_height}")p_size = 8 # pixel size on the renderercanva_width = screen_width//p_sizecanva_height = screen_height//p_size-16frame = Frame(window)frame.pack(anchor="center", padx=10, pady=2)# choose font button (file picker)choose_font_button = ttk.Button(frame, text='Choose font', command=select_file)choose_font_button.pack(side="left", padx=5)# Label and Entry for Font heightLabel(frame, text="Font height").pack(side="left", padx=5)font_height_input = StringVar(value=str(font_size_init))Entry(frame, textvariable=font_height_input, width=4).pack(side="left", padx=5)# Variable to hold the state of the checkboxenforce_font_size_bool = IntVar() # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Enforce size", variable=enforce_font_size_bool).pack(side="left", padx=5)# Label and Entry for Char ranges to includeLabel(frame, text="Ranges to include").pack(side="left", padx=5)list_char_ranges = StringVar(value=str(list_char_ranges_init))Entry(frame, textvariable=list_char_ranges, width=30).pack(side="left", padx=5)# Variable to hold the state of the checkboxbounding_box_bool = IntVar(value=1) # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Bounding box", variable=bounding_box_bool).pack(side="left", padx=10)# Variable to hold the state of the map generation checkboxgenerate_map_bool = IntVar() # 0 for unchecked, 1 for checkedCheckbutton(frame, text="Generate map", variable=generate_map_bool).pack(side="left", padx=5)# Label and Entry for Map start codeLabel(frame, text="Map start code").pack(side="left", padx=5)map_start_code_input = StringVar(value=str(map_start_code_init))Entry(frame, textvariable=map_start_code_input, width=6).pack(side="left", padx=5)# Button to launch the font generation functionb1 = Button(frame, text="Preview", width=14, height=2, background="blue", foreground="white", command=generate_font_data)b1.pack(side="left", padx=5)# Button to launch the font exporting functionb1 = Button(frame, text="Save", width=14, height=2, background="blue", foreground="white", command=save_font_data)b1.pack(side="left", padx=5)frame = Frame(window).pack(anchor="w", padx=2, pady=2)canvas = Canvas(frame, width=canva_width*p_size, height=canva_height*p_size, bg="black")canvas.configure(scrollregion=(0, 0, canva_width*p_size, canva_height*p_size))canvas.bind("<MouseWheel>", zoom)canvas.bind("<ButtonPress-1>", start_pan) # Start panningcanvas.bind("<B1-Motion>",pan_canvas)canvas.focus_set()canvas.pack(fill="both", expand=True)window.mainloop()
?