JIT giúp render chữ bitmap cực nhanh trên vi điều khiển
Raph Levien đã công bố kỹ thuật biên dịch JIT sinh mã ARM tối ưu ngay lúc chạy để render chữ bitmap trên chip RP2350, chỉ tốn khoảng 23 chu kỳ mỗi ký tự. Cách làm này cho phép giãn chữ theo tỉ lệ và kerning trên vi điều khiển vốn rất hạn chế về RAM và tốc độ.

Raph Levien vừa công bố một kỹ thuật render chữ bitmap tốc độ cao trên vi điều khiển RP2350, nằm trong dự án vic-2350 — một phiên bản làm lại của máy tính gia đình thập niên 80. Ý tưởng cốt lõi là dùng JIT (just-in-time, biên dịch ngay lúc chạy) để sinh ra mã máy tối ưu riêng cho từng chuỗi ký tự cần hiển thị.
Điều đáng chú ý là cách làm này đạt hiệu năng rất cao mà không cần bộ đệm khung hình (framebuffer) lớn — vốn là thứ xa xỉ trên một con chip chỉ có 520kB SRAM.
Minh họa render chữ bitmap cấp độ điểm ảnh
Vì sao chọn chữ bitmap và "đua tia quét"
Theo tác giả, font bitmap tuy đã lỗi thời nhưng vẫn có chỗ đứng: nó tiết kiệm tài nguyên, và ở độ phân giải thấp, một font bitmap được thiết kế kỹ cho nét sắc gọn đẹp hơn nhiều so với chữ khử răng cưa (antialiasing) bị nhòe. Ông cũng muốn có giãn chữ theo tỉ lệ và kerning (điều chỉnh khoảng cách giữa các cặp ký tự), lấy cảm hứng từ thẩm mỹ của máy Macintosh thời đầu.
Phần lớn dự án trên RP2350 dùng ô ký tự cố định vì đạt hiệu năng tốt khó hơn nhiều. Chip RP2350 có thể xuất video DVI nhờ ngoại vi HSTX, nhưng phần bộ đệm video hoàn toàn do phần mềm lo. Tác giả chọn cách race the beam: ghi đầy bộ đệm từng dòng quét thay vì dùng framebuffer. Cách này cho hình ảnh phong phú hơn và độ trễ thấp hơn, nhưng đặt ra ràng buộc rất gắt: khoảng 6 chu kỳ cho mỗi điểm ảnh.
Rust không đáp ứng được yêu cầu hiệu năng, nên tác giả viết bằng hợp ngữ ARM, và phần lớn renderer hiện được viết bằng symbolasm, một ngôn ngữ lập trình do chính ông tạo ra cho lập trình Cortex-M33 hiệu năng cực cao.
Kiến trúc renderer và đối tượng hiển thị chữ
Giao diện giữa ứng dụng và renderer là một display list (danh sách đối tượng hiển thị) được nhân đôi: một bản đang được render và quét ra màn hình, bản kia do ứng dụng dựng. Mỗi lần đồng bộ dọc (vsync), nếu ứng dụng sẵn sàng thì hai bản được hoán đổi.
Trong quá trình render, renderer duy trì một active list — tập con các đối tượng giao với dòng quét hiện tại. Các đối tượng được sắp xếp theo trục y trước khi trình chiếu để việc thêm và bớt khỏi active list diễn ra đúng thứ tự.
Đối tượng hiển thị chữ chỉ chứa chuỗi ký tự (1 byte mỗi ký tự), con trỏ tới font cùng màu chữ và màu nền. Khi đối tượng được thêm vào active list, hàm start cấp phát vùng nhớ tạm cho mã JIT và một bộ đệm trung gian 1 bit mỗi điểm ảnh, rồi gọi trình biên dịch JIT để "nở" chuỗi thành mã máy. Khi render, mã đó đổ dữ liệu vào bộ đệm 1 bit, sau đó một lượt thứ hai mở rộng ra định dạng RGB 565 (16 bit mỗi điểm ảnh) với tốc độ 1,75 chu kỳ mỗi điểm ảnh.
Font nén chặt và JIT sinh mã
Font được lưu ở dạng nén cực chặt: toàn bộ "mực" của các glyph nằm trong một ảnh bitmap rộng, và cách đóng gói đảm bảo không glyph nào vắt qua ranh giới khối 32 điểm ảnh, nhờ vậy một hàng điểm ảnh luôn đọc được bằng một lệnh nạp 32 bit duy nhất.
Thuật toán đóng gói còn phát hiện các glyph trùng lặp, vì không có yêu cầu điểm ảnh phải thuộc về duy nhất một glyph. Chẳng hạn l chính là cạnh trái của U, còn 0 giống hệt cặp () khi bỏ khoảng trắng ở giữa. Nhờ vậy kích thước bitmap font giảm từ 1140 xuống 960 byte.
Sơ đồ font nén chung lưới, đánh dấu các glyph dùng chung
Phần metadata của mỗi glyph (chưa tính kerning) gồm vị trí và chiều rộng phần mực trong bitmap, khoảng trống bên trái và độ tiến (advance) — tất cả nhồi vào một số nguyên 32 bit.
Trong mã JIT sinh ra, mã đơn giản nhất để cộng một glyph vào bộ tích lũy chỉ gồm ba lệnh: nạp 32 bit từ bitmap font, trích trường bit của glyph, rồi ghép vào vị trí tương ứng. Tác giả còn đảo thứ tự lệnh — lệnh cuối của glyph thứ i được dời xuống sau lệnh nạp của glyph i+1 — để lấp khe trễ giữa lệnh nạp và lệnh trích bit, giảm chi phí từ 4 xuống 3 chu kỳ.
Hiệu năng
Vòng lặp cơ bản tốn 23 chu kỳ mỗi glyph cho phần JIT, cộng 3 chu kỳ nhân với chiều cao glyph cho việc tạo bitmap. Ví dụ textdemo trong vic-2350 tốn khoảng 21k chu kỳ cho JIT và 42k chu kỳ để chạy mã JIT — tổng cộng chỉ khoảng 2,6% thông lượng của một lõi RP2350.
Tác giả nhận xét đây gần như là mức tối ưu có thể đạt được, và cho rằng chi phí chạy mã JIT nhỏ đến mức không đáng bận tâm: trong vic-2350, trình biên dịch JIT chạy chưa tới 100µs trước khi dòng video được quét ra. Dù thiết kế cho race-the-beam, kỹ thuật này cũng dùng được khi ghi vào framebuffer, đặc biệt là framebuffer 1 bit mỗi điểm ảnh.
Kerning bằng tích vô hướng
Kerning rất quan trọng để khoảng cách chữ trông đều, nhưng cơ chế của font OpenType khá phức tạp. Tác giả dùng một ý tưởng đơn giản hơn: mỗi glyph có hai bitvector, một cho bên trái và một cho bên phải. Để kern hai glyph, lấy AND bitvector phải của glyph trước với bitvector trái của glyph sau, rồi đếm số bit 1 (popcount) — kết quả chính là số điểm ảnh cần dịch. Đó thực chất là tích vô hướng của hai vector bit.
Bảng kern chỉ tốn 2 byte mỗi glyph và việc tính kern rất nhanh, được thực hiện lúc dựng display list chứ không phải lúc đua tia quét. Khi kern khác 0, một ký tự "khoảng trắng âm" đặc biệt được chèn vào chuỗi.
Kerning với chữ T và Y
Với font này, tác giả xây bảng kern thủ công, coi mỗi bit là một đặc điểm cụ thể — ví dụ bit 0 ứng với phần nhô ra ở góc trên bên phải của glyph trước và chỗ thiếu mực tương ứng ở góc trên bên trái của glyph sau. Ông cho rằng ý tưởng này có thể mở rộng cho font thật: thay vì bit và số nguyên kern, vector sẽ lưu giá trị liên tục đã lượng tử hóa, và có thể thêm một bước phi tuyến sau phép nhân.
Góc nhìn
Kỹ thuật này cho thấy trên vi điều khiển hiện đại, giới hạn thực sự không nằm ở sức mạnh tính toán mà ở cách phân bổ chu kỳ và bộ nhớ. Việc sinh mã máy ngay lúc chạy — thứ thường gắn với trình duyệt hay máy ảo ngôn ngữ — lại tiết kiệm hơn cả việc lưu sẵn mã đã sinh, vì RAM quý hơn cả thời gian CPU ở quy mô này.
Với kỹ sư làm nhúng và các dự án hiển thị trên chip nhỏ, đây là gợi ý đáng tham khảo: font nén chặt, bảng kern dạng bitvector và JIT theo từng chuỗi có thể đem lại giao diện chữ có giãn cách và kerning đúng chuẩn mà vẫn chừa phần lớn tài nguyên cho ứng dụng.
Nguồn: linebender.org · Quy trình biên tập
Bài viết liên quan

Công nghệ
SDF, MSDF, Slug hay Texture Atlas: Chọn thuật toán render chữ trên GPU như thế nào?
30 tháng 9, 2026

Công nghệ
Papydeck: Ba chip RP2350B đồng bộ tạo nên máy chơi game nhỏ gọn, 3.000 pixel sprite mỗi dòng
21 tháng 9, 2026

Công nghệ
Rhun: Trình soạn thảo mã nguồn viết bằng hợp ngữ, nhẹ nhưng đầy đủ tính năng
02 tháng 10, 2026