1. من السيليكون إلى الرياضيات: آلة تورينج الشاملة وتماثل الآلات محدودة الحالة
لم تكن أجهزة مثل نينتندو (NES - 1983)، وسوبر نينتندو (SNES - 1990)، وبلاي ستيشن 2 (2000)، وجيم كيوب (2001) مجرد حواسيب عامة الأغراض، بل كانت منظومات معقدة من الدوائر المتكاملة المخصصة (ASICs وشرائح LSI المصممة حسب الطلب). ومع ذلك، نستطيع اليوم دون امتلاك أي قطعة من ذلك السيليكون الأصلي تشغيل ألعابها التجارية على الحواسيب والهواتف الذكية الحديثة المزودة بمعالجات x86-64 أو ARM64 بدقة متطابقة على مستوى البت. إن الفرع الهندسي البرمجي الذي يجعل هذا ممكناً هو محاكاة العتاد (Hardware Emulation).
كيف يمكن لبرنامج يعمل على معمارية مختلفة تماماً أن يقلد السلوك الفيزيائي لملايين الترانزستورات ومذبذبات الكوارتز؟ يعود الأساس النظري إلى المبرهنة التأسيسية لعلوم الحاسوب التي أثبتها عالم الرياضيات البريطاني آلان تورينج (Alan Turing) عام 1936، وهي مبرهنة آلة تورينج الشاملة (Universal Turing Machine - UTM):
$$U(\langle M \rangle, x) = M(x)$$بالاقتران مع أطروحة تشيرش-تورينج (Church-Turing Thesis)، تثبت هذه المعادلة أنه إذا رمزنا لعتاد منصة الألعاب الفيزيائي بالرمز $M$، وللشفرة المصدرية للمحاكي التي تصف القواعد المنطقية لدوائر $M$ بالرمز $\langle M \rangle$، ولبيانات شريط اللعبة ومدخلات ذراع التحكم بالرمز $x$، فإن أي حاسوب مضيف كامل تورينجياً $U$ ينفذ $\langle M \rangle$ سينتج حتماً نفس سلسلة الحالات والمخرجات السمعية والبصرية $M(x)$ التي ينتجها الجهاز الأصلي.
من منظور الدوائر الرقمية، فإن أي دائرة منطقية متزامنة تعمل بنبضات ساعة رئيسية (سواء كانت المعالج المركزي CPU، أو معالج الصور PPU، أو معالج الصوت APU) تُختزل رياضياً إلى آلة حالة محدودة حتمية (Deterministic Finite State Machine - FSM). إذا كانت $S_t \in \mathcal{S}$ تمثل متجه الحالة الكلي لجميع المسجلات والرايات وذاكرة الفيديو VRAM والذاكرة العشوائية WRAM عند النبضة الزمنية $t$، وكانت $I_t \in \mathcal{I}$ تمثل متجه المدخلات الخارجية، فإن شبكة البوابات المنطقية التوافقية تحسب بين النبضتين $t$ و $t+1$ دالتين رياضيتين نقيتين:
$$S_{t+1} = \delta(S_t, I_t), \qquad O_t = \lambda(S_t, I_t)$$حيث $\delta$ هي دالة انتقال الحالة، و $\lambda$ هي دالة الخرج التي تولد بكسلات RGB وعينات الصوت PCM. إن الجبر البولياني الذي تنفذه ترانزستورات CMOS فيزيائياً عبر مستويات الجهد الكهربائي ($0\,\text{V}$ مقابل $3.3\,\text{V}/5\,\text{V}$) متماثل رياضياً بالكامل (Isomorphic) مع العمليات المنطقية الثنائية (&, |, ^, ~, <<) التي ينفذها معالج المضيف على متغيرات uint32_t و uint64_t.
لذلك فإن العقبة الكبرى أمام المحاكاة ليست قابلية الحساب، بل التعقيد الحسابي تحت قيد الزمن الحقيقي الصارم. ففي جهاز سوبر نينتندو بنظام NTSC الذي يعمل بمذبذب رئيسي تردده $21.477272\,\text{MHz}$، يتكون كل إطار صورة من $262$ خط مسح (Scanlines) يحتوي كل منها على $1364$ دورة ساعة رئيسية:
$$f_{\text{frame}} = \frac{21,477,272\,\text{Hz}}{262 \times 1364} \approx 60.0988\,\text{Hz} \implies T_{\text{frame}} \approx 16.639\,\text{ms}$$في العتاد الفيزيائي، تقوم ملايين البوابات المنطقية بحساب $\delta(S_t, I_t)$ بالتوازي المكاني في كل نانوثانية، بينما يتعين على المحاكي البرمجي تحويل هذا التوازي الهائل إلى تعليمات متسلسلة وإنهاء جميع دورات الإطار البالغة $357,368$ دورة في أقل من $16.639\,\text{مللي ثانية}$.
2. أساسيات محاكاة المعالج: من مفسر Switch إلى الترجمة الديناميكية (JIT / Dynarec)
2.1 مفسر Switch الكلاسيكي وعقوبة فشل توقع التفرع
تتمثل الطريقة المباشرة لمحاكاة معالج الضيف (Guest CPU) في تقليد دورة الجلب (Fetch) $\to$ فك التشفير (Decode) $\to$ التنفيذ (Execute) داخل حلقة while تحتوي على جملة switch (opcode) ضخمة.
غير أن هذا التصميم يعاني من اختناق معماري حاد على المعالجات الفائقة التدرج (Superscalar) الحديثة؛ إذ تُترجم جملة switch بالكامل إلى تعليمة قفز غير مباشر واحدة (jmp *[rax + rdx*8]). ونظراً لأن شفرات العمليات للضيف تتغير باستمرار، فإن وحدة توقع التفرع غير المباشر (BTB) في معالج المضيف تخطئ التوقع باستمرار عند هذه النقطة، مما يؤدي إلى إفراغ خط الأنابيب (Pipeline Flush) المكون من 14 إلى 20 مرحلة وإهدار عشرات الدورات في كل تعليمة. ورغم أن تقنية Direct Threaded Code عبر عناوين اللافتات (goto *dispatch_table[opcode]) توزع القفزات غير المباشرة على نهايات دوال المعالجة، فإنها لا تلغي عبء فك التشفير المتكرر.
2.2 الترجمة الثنائية الديناميكية (JIT / Dynarec) وحذف الرايات الميتة
لتجاوز هذا الحاجز، تعتمد المحاكيات عالية الأداء (Dolphin، PCSX2، RPCS3، QEMU TCG، و Apple Rosetta 2) على الترجمة الفورية / إعادة الترجمة الديناميكية (JIT / Dynarec): حيث تُترجم الكتل الأساسية (Basic Blocks) من شفرة آلة الضيف أثناء التشغيل إلى شفرة آلة أصلية للمضيف (x86-64 / ARM64) وتُنفذ مباشرة من ذاكرة التخزين المؤقت للشفرة.
flowchart TD
G["كتلة أساسية لشفرة آلة الضيف (MIPS / PowerPC / 65816)"] --> L["1. الرفع إلى التمثيل الوسيط SSA IR<br/>تفكيك تعليمات الضيف إلى تمثيل وسيط مستقل عن المعمارية"]
L --> O["2. تمريرات تحسين التمثيل الوسيط (IR Optimization)<br/>نشر الثوابت وحذف الرايات الميتة (Dead-Flag Elision)"]
O --> R["3. تخصيص المسجلات (Register Allocation)<br/>ربط مسجلات الضيف الأكثر استخداماً بمسجلات المضيف الفيزيائية"]
R --> E["4. توليد الشفرة وربط الكتل (Block Chaining)<br/>الكتابة في صفحات قابلة للتنفيذ وتعديل تعليمات القفز jmp مباشرة"]
E --> X["5. التنفيذ الأصلي بأقصى سرعة على معالج المضيف<br/>مصيدة حماية الصفحات تكتشف الشفرة ذاتية التعديل (SMC)"]
تشمل التحسينات الجوهرية لمحرك Dynarec ما يلي:
- حذف الرايات الميتة (Dead-Flag Elision): تقوم التعليمات الحسابية بتحديث رايات الحالة (Z, N, C, V) باستمرار، لكن معظمها يُستبدل فوراً بالتعليمة التالية دون أن يُقرأ أبداً. ومن خلال تحليل الحياة العكسي (Liveness Analysis) على تمثيل SSA، يحذف المترجم أكثر من $80\%$ من حسابات الرايات غير الضرورية.
- تخصيص المسجلات الفيزيائية: يحتفظ خوارزم Linear Scan بمسجلات الضيف الأكثر نشاطاً داخل المسجلات المادية لمعالج المضيف مباشرة، متجنباً القراءة والكتابة المتكررة في بنية
CPUStateبالذاكرة. - ربط الكتل (Block Chaining): عندما تقفز كتلة مترجمة A إلى كتلة مترجمة أخرى B، يقوم المترجم بتعديل تعليمة
jmpفي نهاية الكتلة A لتشير مباشرة إلى العنوان الأصلي للكتلة B دون العودة إلى حلقة الموزع. - اكتشاف الشفرة ذاتية التعديل (SMC): عندما تعيد اللعبة كتابة شفرتها الخاصة في الذاكرة العشوائية، يلتقط المحاكي عملية الكتابة عبر أقنعة برمجية أو حماية صفحات الذاكرة للقراءة فقط (
mprotect/VirtualProtect) ويبطل الكتل المترجمة القديمة فوراً.
| معيار المقارنة | مفسر Switch | مفسر Threaded | المترجم الديناميكي (JIT / Dynarec) |
|---|---|---|---|
| التكلفة لكل تعليمة ضيف | $20 \sim 80+$ تعليمة مضيف | $12 \sim 35$ تعليمة مضيف | $1 \sim 4$ تعليمات أصلية للمضيف |
| كفاءة توقع التفرع | سيئة جداً (نقطة اختناق BTB واحدة) | متوسطة (قفزات غير مباشرة موزعة) | ممتازة (قفزات مباشرة أصلية وربط كتل) |
| حذف الرايات الميتة | مستحيل (لا يوجد سياق بين التعليمات) | محدود للغاية | كامل (تحليل شامل على تمثيل SSA IR) |
| التعقيد وقابلية النقل | بسيط جداً، لغة C/Rust قياسية | يتطلب امتدادات خاصة بالمترجم | معقد للغاية (يتطلب واجهة خلفية لكل معمارية) |
| التحكم في دقة النبضة | سهل جداً (إيقاف عند أي دورة) | سهل | صعب (تُحسب الدورات عند حدود الكتل) |
3. ناقل الذاكرة وتقنية Fastmem: اختطاف وحدة إدارة الذاكرة (MMU) للمضيف
في منصات الألعاب، يتحكم المعالج في شرائح الصورة والصوت والـ DMA عبر قراءة وكتابة مسجلات الإدخال والإخراج المحمولة على الذاكرة (MMIO). ولو قام المحاكي بفحص كل عملية وصول للذاكرة برمجياً عبر جمل if-else للتمييز بين ذاكرة RAM ومسجلات MMIO، لانهارت سرعة الـ JIT تماماً.
تحل تقنية Fastmem هذه المشكلة عبر تسخير وحدة إدارة الذاكرة المادية (Hardware MMU) لنظام التشغيل المضيف ذي الـ 64 بت:
- يحجز المحاكي في فضاء العناوين الافتراضي للمضيف (64 بت) منطقة متصلة بحجم فضاء عناوين الضيف بالكامل (مثلاً $4\,\text{GB}$ عبر
mmap/VirtualAllocلمنصة 32 بت)، ويحفظ مؤشر بدايتها في مسجل ثابت بالمضيف (rbx). - تُربط مناطق ذاكرة RAM الفعلية ومراياها بصلاحيات قراءة وكتابة كاملة (
PROT_READ | PROT_WRITE). - تُضبط جميع الصفحات المقابلة لمسجلات العتاد MMIO عمداً لتكون محظورة الوصول (
PROT_NONE/PAGE_NOACCESS).
وبذلك يستطيع مترجم JIT تحويل أي عملية قراءة لذاكرة الضيف إلى تعليمة آلة أصلية واحدة فقط دون أي تفرع شرطي:
| |
- المسار السريع ($99.9\%$ من عمليات الوصول لذاكرة RAM): تنفذ وحدة MMU ومخزن TLB الماديان في معالج المضيف عملية التحميل خلال 3 إلى 4 دورات بتكلفة برمجية تساوي صفراً.
- المسار البطيء ($0.1\%$ من عمليات الوصول لمسجلات MMIO): عند ملامسة صفحة محمية بـ
PROT_NONE، تطلق وحدة MMU للمضيف فوراً استثناء خطأ صفحة مادي (SIGSEGVفي أنظمة POSIX أوSTATUS_ACCESS_VIOLATIONفي ويندوز). يلتقط معالج الإشارات في المحاكي هذا الاستثناء، ويفك شفرة التعليمة، ويستدعي دالة C++ الخاصة بـmmio_read/write()، ثم يحدث المسجل الهدف ويقدم مؤشر التعليماتRIP.
4. الدقة على مستوى النبضة (Cycle Accuracy) ومزامنة المهام التعاونية
لماذا صرح مهندس المحاكيات الأسطوري الراحل Near (byuu) بأن محاكاة جهاز سوبر نينتندو الذي يعمل بتردد $3.58\,\text{MHz}$ فقط بدقة نبضة تصل إلى 100% تطلبت معالجاً مكتبياً حديثاً بتردد $3\,\text{GHz}$؟
كانت المحاكيات القديمة القائمة على خطوط المسح (Scanline-based) تشغل المعالج المركزي طوال خط مسح كامل ($228$ دورة) أولاً، ثم تطلب من معالج الصور PPU رسم جميع بكسلات ذلك الخط البالغة 256 بكسل دفعة واحدة. غير أن العديد من الألعاب تقوم بتعديل المسجلات الرسومية (لوحة الألوان، التمرير، أو مصفوفات Mode 7) في منتصف رسم خط المسح تماماً (Mid-Scanline) عبر المعالج أو قناة HDMA. وبدون مزامنة دقيقة عند نبضة البكسل المحددة، تظهر تشوهات بصرية أو تتوقف اللعبة عن العمل.
flowchart TD
subgraph Inaccurate["المحاكاة على مستوى خط المسح (سريعة لكنها تفشل في تأثيرات منتصف الخط)"]
S1["1. المعالج CPU ينفذ 228 دورة متواصلة<br/>(يعدل مسجل لوحة الألوان عند الدورة 112)"] --> S2["2. معالج الصور PPU يرسم لاحقاً 256 بكسل دفعة واحدة<br/>(يطبق لوحة الألوان الجديدة خطأً منذ البكسل رقم 0!)"]
end
subgraph CycleAcc["مزامنة المهام التعاونية بدقة النبضة (Cycle-Accurate Catch-up)"]
C1["1. المعالج CPU يعمل حتى النبضة الدقيقة t = 112<br/>ويكتب في مسجل MMIO الخاص بلوحة ألوان PPU"] --> C2["2. انتقال فوري إلى المهمة التعاونية لـ PPU<br/>معالج PPU يلحق بالزمن (Catch-up) ويرسم البكسلات من 0 إلى 111"]
C2 --> C3["3. تُطبق لوحة الألوان الجديدة بدقة عند t = 112<br/>معالج PPU يكمل رسم البكسلات من 112 إلى 255 بمطابقة تامة للأصل"]
end
في المحاكيات الدقيقة على مستوى النبضة مثل bsnes / higan / Ares، تعمل كل شريحة (CPU، PPU، SMP، DSP) كـ مهمة تعاونية مستقلة في فضاء المستخدم (Coroutines / Fibers عبر مكتبة libco). وكلما أصدرت شريحة إشارة أو عملية كتابة قد تؤثر على شريحة أخرى، تقوم دالة co_switch() بتبديل السياق في نحو 10 تعليمات تجميع (Assembly) فقط لتمكين الشريحة المستهدفة من اللحاق بالزمن الرئيسي بدقة.
علاوة على ذلك، تواصل هندسة المحاكاة الحديثة التغلب على ثلاثة تحديات متقدمة:
- حسابات الفاصلة العائمة غير القياسية (Non-IEEE-754): لا يدعم معالج Emotion Engine ووحدات المتجهات في جهاز PS2 قيم اللانهاية (
Inf) أو غير الرقم (NaN)، ويصفّر الأعداد غير الطبيعية تلقائياً، مما يلزم محاكي PCSX2 باستخدام أقنعة وتشبيك SIMD لمنع انهيار محركات الفيزياء في الألعاب. - تقنية Run-Ahead لإلغاء زمن الاستجابة: من خلال محاكاة $N$ إطارات مسبقاً في الخلفية بصمت مع حفظ واسترجاع فائق السرعة لحالة الذاكرة (Savestates)، يتخطى المحاكي تأخير المخزن المؤقت الداخلي للعبة نفسها، محققاً استجابة أسرع حتى من الجهاز الأصلي المتصل بتلفاز أنبوب الأشعة المهبطية (CRT).
- مصفوفات FPGA وحفظ التراث الرقمي: إلى جانب إعادة بناء البوابات المنطقية عتادياً عبر FPGA (مشروع MiSTer) بلغات Verilog/VHDL، تمثل الشفرة المصدرية للمحاكيات الدقيقة وثائق عتادية قابلة للتنفيذ تحفظ الإرث الفني التفاعلي للبشرية من التلف الفيزيائي الحتمي لشرائح السيليكون القديمة.
