Bỏ qua đến nội dung chính
Về trang chủ
Tech tools-cli 9 phút đọc

WATaBoy: Biên Dịch JIT Game Boy Sang WebAssembly – Bất Ngờ Vượt Mặt Trình Giả Lập Native, Mở Ra Kỷ Nguyên Mới Cho iOS? 🚀🎮

Một dự án sinh viên đã chứng minh rằng việc biên dịch JIT các lệnh của Game Boy sang WebAssembly (Wasm) có thể nhanh hơn đáng kể so với trình giả lập chạy native, mang lại hy vọng mới cho việc giả lập console trên các nền tảng hạn chế JIT như iOS.

Tier 2 · nguồn 99% độ tin cậy Auto-priority
Nguồn gốc humphri.es

WATaBoy: Biên Dịch JIT Lệnh CPU Game Boy Sang WebAssembly Vượt Trội Hơn Trình Giả Lập Native! 🚀

Bài viết gốc: humphri.es/blog/WATaBoy

Bối Cảnh và Thách Thức 💡

Bạn có bao giờ tự hỏi tại sao trình giả lập Dolphin nổi tiếng lại không có mặt trên iOS không? Lý do chính là vì iOS hạn chế khả năng biên dịch Just-In-Time (JIT) mã nguồn thành mã máy native. Đối với các trình giả lập cần hiệu năng CPU cao như Dolphin, JIT là yếu tố then chốt để đạt được tốc độ mượt mà. Nếu không có JIT, liệu chúng ta có phải chờ đợi nhiều năm nữa cho đến khi CPU iPhone đủ mạnh để chạy Dolphin chỉ bằng một trình thông dịch?

Thật may mắn, Apple lại có một ngoại lệ cho các hạn chế JIT của họ: các trình duyệt web! Cụ thể, JavaScriptCore – engine JS của WebKit – sử dụng JIT để đạt hiệu suất cao hơn. Điều này cũng đúng với WebAssembly (Wasm). Nếu một hàm JavaScript được gọi đủ số lần, nó sẽ được tối ưu hóa và biên dịch thành mã máy native. Tương tự với Wasm.

Vậy, nếu chúng ta "đi tắt đón đầu" bằng cách không tạo mã máy native trực tiếp, mà thay vào đó tạo bytecode Wasm? Mã Wasm này cuối cùng sẽ được trình duyệt biên dịch thành mã máy native. Ý tưởng này, từng được Andy Wingo đề cập trong blog của mình, đã được một số dự án như The Jiterpreter và v86 áp dụng. Tuy nhiên, chưa có trình giả lập console nào sử dụng kỹ thuật này và so sánh hiệu suất với một trình thông dịch chạy native để xem liệu nó có nhanh hơn hay không.

Đó chính là lúc WATaBoy ra đời. Đây là dự án cuối khóa của một sinh viên đại học, tập trung vào việc xây dựng một trình giả lập Game Boy: đầu tiên là dùng trình thông dịch, sau đó là dùng JIT-to-Wasm. Mục tiêu chính là làm bằng chứng concept và đo lường hiệu suất giữa hai phương pháp. Mặc dù Game Boy không hưởng lợi từ JIT nhiều như các console thế hệ thứ sáu, nó lại phù hợp để triển khai trong khuôn khổ của một dự án cuối khóa.

Nhiều người có thể hoài nghi về việc JIT mang lại lợi ích gì cho một trình giả lập Game Boy. Tuy nhiên, blog của GameRoy đã giải thích cách JIT có thể hoạt động hiệu quả và vẫn đảm bảo độ chính xác theo chu kỳ xung nhịp (cycle-accurate) bằng cách: * Dự đoán thời điểm ngắt (interrupts) sẽ xảy ra. * Khi một khối JIT có thể bị gián đoạn, quay về sử dụng trình thông dịch. * Đánh giá lười (lazily evaluate) bất kỳ thành phần Game Boy nào không phải CPU được truy cập qua MMIO.

Các kỹ thuật tối ưu hóa này, dù ban đầu GameRoy nhắm mục tiêu x86, vẫn hoàn toàn áp dụng được cho JIT-to-Wasm của WATaBoy.

Chi Tiết Triển Khai (Dành Cho Kỹ Sư ⚙️)

WATaBoy tập trung vào việc tạo mã Wasm và liên kết động (late-linking) từ bên trong Rust. Thông thường, chúng ta sẽ dùng wasm-bindgenwasm-pack để tạo mã glue giữa Rust và JavaScript. Tuy nhiên, để làm việc ở cấp độ thấp với Wasm, WATaBoy sử dụng một phương pháp tương tự như "Rust to WebAssembly the hard way", tức là truyền dữ liệu qua ranh giới Rust-JS thông qua C ABI (sử dụng con trỏ và độ dài buffer).

Để tạo bytecode Wasm, WATaBoy sử dụng thư viện wasm-encoder. Thư viện này cho phép phát ra các byte lệnh Wasm một cách có cấu trúc, tuy có một chút mã boilerplate nhưng vẫn dễ quản lý hơn nhiều so với việc viết tay một mảng byte thô.

Thách thức chính nằm ở chỗ Wasm là một kiến trúc Harvard, không phải von Neumann. Điều này có nghĩa là chúng ta không thể trực tiếp thực thi bytecode Wasm đã tạo. Thay vào đó, chúng ta phải nhờ đến "embedder" (thường là JavaScript) để biên dịch, khởi tạo và liên kết mã Wasm mới. Quá trình này bao gồm: 1. Biên dịch & Khởi tạo: Sử dụng interface biên dịch đồng bộ để tạo và khởi tạo bytecode. 2. Liên kết: Thêm hàm từ module Wasm vừa tạo vào bảng hàm gián tiếp của module chính và lưu lại chỉ mục của nó. 3. Điều phối: Thực thi hàm bằng lệnh call_indirect của Wasm, gọi hàm thứ n trong bảng hàm gián tiếp.

WATaBoy sử dụng một hàm linkNewModule được import từ JavaScript để xử lý việc biên dịch và liên kết module Wasm mới. Để cho phép module Rust gọi các hàm này, một đoạn inline WebAssembly nhỏ được sử dụng thông qua tính năng asm_experimental_arch của Nightly Rust. Đồng thời, các cờ --export-table--growable-table được truyền cho LLD để bảng hàm gián tiếp có thể truy cập và mở rộng từ JavaScript.

Khi đã có mã Rust và Wasm được biên dịch, phần JavaScript (embedder) sẽ chịu trách nhiệm tải module Wasm chính, sau đó cung cấp hàm linkNewModule để đọc bytecode Wasm từ bộ nhớ của instance chính, biên dịch nó thành một module Wasm mới, khởi tạo nó và thêm hàm mong muốn vào bảng hàm gián tiếp của instance chính. Kết quả là, Rust có thể gọi một hàm make_and_execute_add để tạo và thực thi một hàm add được JIT-biên dịch hoàn toàn từ bên trong Wasm và JavaScript.

Kết Quả Đáng Kinh Ngạc! 📈

Để đánh giá hiệu suất, WATaBoy đã được benchmark trên MacBook Air (M2, 2022) với ba ROM Game Boy khác nhau: Pokémon Blue, The Legend of Zelda: Link’s Awakening và Tobu Tobu Girl. Mỗi phiên bản (JIT-to-Wasm, trình thông dịch trong Wasm, trình thông dịch native) đều được chạy trong 10 giây để giả lập màn hình tiêu đề và đo tổng số khung hình được giả lập.

Kết quả cho thấy một điều bất ngờ: * Khi giả lập Pokémon Blue, JIT-to-Wasm của WATaBoy đạt tốc độ ~1.2 lần nhanh hơn so với trình thông dịch chạy native! Điều này khẳng định lợi ích của JIT, ngay cả khi có thêm một lớp gián tiếp qua mã máy native. * Nó cũng nhanh hơn khoảng 1.5 lần so với trình thông dịch chạy trực tiếp trong Wasm.

So sánh hiệu suất của mỗi phương pháp so với phần cứng Game Boy gốc.

Ngoài ra, WATaBoy cũng được thử nghiệm trên ba engine trình duyệt lớn: Safari, Chrome và Firefox. Safari đã thể hiện hiệu suất vượt trội. Điều này là một tin tốt, vì iOS chỉ sử dụng WebKit (engine của Safari), cho thấy nền tảng này không hề bị tụt hậu trong trường hợp này.

So sánh hiệu suất JIT-to-Wasm của WATaBoy trên Safari, Chrome và Firefox.

Cảnh báo: Trình diễn demo có thể gây co giật cho người bị động kinh nhạy cảm với ánh sáng ⚠️.

Hướng Phát Triển và Các Hạn Chế 🚧

Đối với WATaBoy: * Các tính năng còn thiếu như hỗ trợ âm thanh và Game Boy Color (GBC) đang là ưu tiên hàng đầu. * Về hiệu suất, việc giả lập PPU (Bộ xử lý hình ảnh) vẫn chiếm phần lớn thời gian chạy. Cần cải thiện khả năng dự đoán ngắt PPU để giảm thiểu việc JIT phải quay về trình thông dịch. * Tuy kết quả cho thấy JIT-to-Wasm vượt trội hơn trình thông dịch native, cần nhìn nhận một cách khách quan rằng đây là so sánh giữa JIT cơ bản của WATaBoy với trình thông dịch cơ bản của WATaBoy. Vẫn còn nhiều kỹ thuật tối ưu hóa khác cho cả hai phương pháp (ví dụ: trình thông dịch cache, biên dịch các lệnh rẽ nhánh phức tạp hơn trong JIT) có thể làm thay đổi cục diện.

Đối với JIT-to-Wasm nói chung: * Điểm khó khăn chính hiện nay là công cụ tạo mã (codegen). Các dự án hiện tại đều sử dụng công cụ riêng, chưa tiện lợi và mạnh mẽ như DynASM hay Cranelift. Để JIT-to-Wasm được áp dụng rộng rãi hơn, cần có công cụ cho phép các nhà phát triển trình giả lập viết chuỗi WAT dễ đọc và dịch thành bytecode tại thời điểm biên dịch. * Cũng cần thừa nhận một hạn chế khác: Wasm không cho phép một số tối ưu hóa cấp thấp mà Dolphin hay dùng, chẳng hạn như "hardware fastmem". Bất kỳ truy cập bộ nhớ không hợp lệ nào cũng không thể phục hồi trong runtime Wasm.

Kết Luận 🎯

Dù kết quả này không trực tiếp chứng minh rằng một trình giả lập GameCube sẽ chạy đủ tốc độ chỉ bằng cách triển khai JIT-to-Wasm, nhưng việc JIT cơ bản đã vượt trội hơn trình thông dịch cho thấy đây là một hướng đi rất đáng để khám phá. Hy vọng với các công cụ codegen trưởng thành hơn, Wasm có thể trở thành một mục tiêu phổ biến cho việc giả lập console đa nền tảng nhanh hơn, đặc biệt là trên iOS.

Bạn có thể tự mình trải nghiệm bản đầy đủ của WATaBoy tại đây với ROM của riêng bạn. Hãy cùng chờ đợi những bước tiến mới trong lĩnh vực giả lập trên nền tảng web!