المجمع القذر الخارقة 6502

تسرد هذه المقالة بعض الحيل التي استخدمها المشاركون في مسابقة برمجة Commodore 64 . كانت قواعد المسابقة بسيطة: إنشاء ملف قابل للتنفيذ C64 (PRG) ، والذي يرسم سطرين لتشكيل الصورة أدناه. الشخص الذي يكون حجم ملفه أصغر.


تم نشر إدخالات المسابقة في التغريدات المفتوحة وفي الرسائل الخاصة التي تحتوي على بايتات فقط من ملف PRG وتجزئة MD5.

قائمة المشاركين الذين لديهم روابط إلى شفرة المصدر:


(إذا فاتني شخص ما ، فالرجاء إعلامي ، سأقوم بتحديث القائمة).

تم تخصيص باقي المقالة لبعض الحيل المجمّع التي تم استخدامها في المنافسة.

الأساسيات


تعمل الرسومات C64 افتراضيًا في وضع ترميز الأحرف 40 × 25. يقسم الإطار المعدني في ذاكرة الوصول العشوائي إلى صفيفين:

  • $0400 (ذاكرة الوصول العشوائي على الشاشة ، 40 × 25 بايت)
  • $d800 (ذاكرة الوصول العشوائي بالألوان ، 40 × 25 بايت)

لتعيين حرف ، يمكنك حفظ البايت على ذاكرة الوصول العشوائي على الشاشة ، عند $0400 (على سبيل المثال ، $0400+y*40+x ). تتم تهيئة ذاكرة الوصول العشوائي للألوان باللون الأزرق الفاتح افتراضيًا (اللون 14): هذا هو اللون الذي نستخدمه للخطوط ، أي أنه يمكن ترك ذاكرة الوصول العشوائي بالألوان بدون لمسة.

يمكنك التحكم في ألوان الحدود والخلفية باستخدام سجلات $d020 / إخراج الذاكرة في $d020 (حد) و $d021 (خلفية).

من السهل جداً رسم خطين إذا قمت ببرمجة ميل الخط الثابت مباشرةً. فيما يلي تطبيق C يرسم خطوطًا ويمسح محتويات الشاشة إلى stdout (يتم استخدام malloc() لجعل التعليمات البرمجية تعمل على جهاز كمبيوتر):

 #include <stdint.h> #include <stdio.h> #include <stdlib.h> void dump(const uint8_t* screen) { const uint8_t* s = screen; for (int y = 0; y < 25; y++) { for (int x = 0; x < 40; x++, s++) { printf("%c", *s == 0xa0 ? '#' : '.'); } printf("\n"); } } void setreg(uintptr_t dst, uint8_t v) { // *((uint8_t *)dst) = v; } int main() { // uint8_t* screenRAM = (uint_8*)0x0400; uint8_t* screenRAM = (uint8_t *)calloc(40*25, 0x20); setreg(0xd020, 0); // Set border color setreg(0xd021, 0); // Set background color int yslope = (25<<8)/40; int yf = yslope/2; for (int x = 0; x < 40; x++) { int yi = yf >> 8; // First line screenRAM[x + yi*40] = 0xa0; // Second line (X-mirrored) screenRAM[(39-x) + yi*40] = 0xa0; yf += yslope; } dump(screenRAM); } 

رموز الشاشة أعلاه هي $20 (فارغة) و $a0 (تملأ 8 × 8 كتلة). إذا قمت بتشغيل ، سترى صورة ASCII مع سطرين:

  ## .................................... ##
 .. # ..............................
 ... ## .............................. ## ...
 ..... # ............................ # .....
 ...... ## ........................ ## ......
 ........ ## .................... ## ........
 .......... # .................. # ..........
 ........... ## .............. ## ...........
 ............. # ............ # .............
 .............. ## ........ ## ..............
 ................ ## .... ## ................
 .................. # .. # ..................
 ................... ## ...................
 .................. # .. # ..................
 ................ ## .... ## ................
 .............. ## ........ ## ..............
 ............. # ............ # .............
 ........... ## .............. ## ...........
 .......... # .................. # ..........
 ........ ## .................... ## ........
 ...... ## ........................ ## ......
 ..... # ............................ # .....
 ... ## .............................. ## ...
 .. # ..............................
 ## .................................... ## 

يتم تطبيق نفسه بشكل تافه في المجمع:

 !include "c64.asm" +c64::basic_start(entry) entry: { lda #0 ; black color sta $d020 ; set border to 0 sta $d021 ; set background to 0 ; clear the screen ldx #0 lda #$20 clrscr: !for i in [0, $100, $200, $300] { sta $0400 + i, x } inx bne clrscr ; line drawing, completely unrolled ; with assembly pseudos lda #$a0 !for i in range(40) { !let y0 = Math.floor(25/40*(i+0.5)) sta $0400 + y0*40 + i sta $0400 + (24-y0)*40 + i } inf: jmp inf ; halt } 

اتضح PRG تماما حجم كبير من 286 بايت.

قبل الغوص في التحسين ، نقدم بعض الملاحظات.

أولاً ، نحن نعمل على C64 مع إجراءات ROM في المكان. هناك الكثير من الإجراءات الروتينية التي يمكن أن تكون مفيدة. على سبيل المثال ، JSR $E544 الشاشة باستخدام JSR $E544 .

ثانياً ، يمكن أن تكون حسابات العناوين على معالج 8 بت مثل 6502 مرهقة وتناول الكثير من البايتات. لا يحتوي هذا المعالج أيضًا على مُضاعِف ، لذلك تتضمن عملية حسابية مثل y*40+i عادةً مجموعة من التحولات المنطقية أو جدول بحث يأكل أيضًا بايت. لتجنب الضرب بمقدار 40 ، من الأفضل أن تقدم مؤشر الشاشة بشكل تدريجي:

  int yslope = (25<<8)/40; int yf = yslope/2; uint8_t* dst = screenRAM; for (int x = 0; x < 40; x++) { dst[x] = 0xa0; dst[(39-x)] = 0xa0; yf += yslope; if (yf & 256) { // Carry set? dst += 40; yf &= 255; } } 

نستمر في إضافة ميل الخط إلى العداد الثابت yf ، وعندما تضيف الإضافة 8 بت إشارة الحمل ، أضف 40.

فيما يلي نهج المجمّع التزايدي:

 !include "c64.asm" +c64::basic_start(entry) !let screenptr = $20 !let x0 = $40 !let x1 = $41 !let yf = $60 entry: { lda #0 sta x0 sta $d020 sta $d021 ; kernal clear screen jsr $e544 ; set screenptr = $0400 lda #<$0400 sta screenptr+0 lda #>$0400 sta screenptr+1 lda #80 sta yf lda #39 sta x1 xloop: lda #$a0 ldy x0 ; screenRAM[x] = 0xA0 sta (screenptr), y ldy x1 ; screenRAM[39-x] = 0xA0 sta (screenptr), y clc lda #160 ; line slope adc yf sta yf bcc no_add ; advance screen ptr by 40 clc lda screenptr adc #40 sta screenptr lda screenptr+1 adc #0 sta screenptr+1 no_add: inc x0 dec x1 bpl xloop inf: jmp inf } 

مع 82 بايت ، لا تزال كبيرة جداً. مشكلة واحدة واضحة هي حسابات عنوان 16 بت. اضبط قيمة screenptr غير المباشرة:

  ; set screenptr = $0400 lda #<$0400 sta screenptr+0 lda #>$0400 sta screenptr+1 

نترجم screenptr إلى السطر التالي بإضافة 40:

  ; advance screen ptr by 40 clc lda screenptr adc #40 sta screenptr lda screenptr+1 adc #0 sta screenptr+1 

بالطبع ، يمكن تحسين هذا الرمز ، لكن ماذا لو تخلصت من عناوين 16 بت على الإطلاق؟ دعونا نرى كيف نفعل ذلك.

خدعة 1. التمرير!


بدلاً من بناء خط في ذاكرة الوصول العشوائي التي تظهر على الشاشة ، نرسم فقط في سطر الشاشة الأخير Y = 24 JSR $E8EA إلى أعلى الشاشة بأكملها ، ندعو وظيفة التمرير ROM مع JSR $E8EA !

إليك كيفية تحسين xloop:

  lda #0 sta x0 lda #39 sta x1 xloop: lda #$a0 ldx x0 ; hardcoded absolute address to last screen line sta $0400 + 24*40, x ldx x1 sta $0400 + 24*40, x adc yf sta yf bcc no_scroll ; scroll screen up! jsr $e8ea no_scroll: inc x0 dec x1 bpl xloop 

هذه هي الطريقة التي يبدو التقديم:



هذه واحدة من حيلاتي المفضلة في هذا البرنامج. وجد جميع المتسابقين تقريبًا أنه بمفردهم.

خدعة 2. تعديل قانون النفس


ينتهي رمز تخزين قيم البكسل كما يلي:

  ldx x1 ; hardcoded absolute address to last screen line sta $0400 + 24*40, x ldx x0 sta $0400 + 24*40, x inc x0 dec x1 

يتم تشفير هذا بالتسلسل التالي من 14 بايت:

 0803: A6 22 LDX $22 0805: 9D C0 07 STA $07C0,X 0808: A6 20 LDX $20 080A: 9D C0 07 STA $07C0,X 080D: E6 22 INC $22 080F: C6 20 DEC $20 

باستخدام رمز التعديل الذاتي (SMC) ، يمكنك كتابة هذا بشكل أكثر إحكاما:

  ldx x1 sta $0400 + 24*40, x addr0: sta $0400 + 24*40 ; advance the second x-coord with SMC inc addr0+1 dec x1 

... الذي تم ترميزه في 13 بايت:

 0803: A6 22 LDX $22 0805: 9D C0 07 STA $07C0,X 0808: 8D C0 07 STA $07C0 080B: EE 09 08 INC $0809 080E: C6 22 DEC $22 

خدعة 3. حالة التشغيل "تشغيل"


كان من الطبيعي في المسابقة وضع افتراضات جامحة حول بيئة العمل. على سبيل المثال ، أن رسم الخط هو أول شيء يبدأ بعد تشغيل طاقة C64 ، ولا توجد متطلبات لإخراج نظيف مرة أخرى إلى سطر الأوامر BASIC. لذلك ، كل ما تجده في البيئة الأولية عند الدخول في PRG يمكن ويجب استخدامه لصالحك:

  • تؤخذ السجلات A و X و Y كأصفار
  • جميع الأعلام وحدة المعالجة المركزية تطهيرها
  • محتوى Zeropage (عناوين $00 - $ff )

بنفس الطريقة ، عند استدعاء بعض إجراءات ROM KERNAL ، يمكنك الاستفادة الكاملة من أي آثار جانبية: أعلام وحدة المعالجة المركزية المرتجعة ، قيم zeropage المؤقتة ، إلخ.

بعد التحسينات الأولى ، دعونا نبحث عن شيء مثير للاهتمام في ذاكرة الجهاز:



يحتوي Zeropage على بعض القيم المفيدة لأغراضنا:

  • $d5 : 39 / $ 27 == طول الخط - 1
  • $22 : 64/40 دولارًا == القيمة الأولية لعداد ميل الخط

سيوفر هذا بضع بايتات أثناء التهيئة. على سبيل المثال:

 !let x0 = $20 lda #39 ; 0801: A9 27 LDA #$27 sta x0 ; 0803: 85 20 STA $20 xloop: dec x0 ; 0805: C6 20 DEC $20 bpl xloop ; 0807: 10 FC BPL $0805 

نظرًا لأن $d5 يحتوي على القيمة 39 ، فيمكنك الإشارة إلى العداد x0 ، والتخلص من زوج LDA / STA:

 !let x0 = $d5 ; nothing here! xloop: dec x0 ; 0801: C6 D5 DEC $D5 bpl xloop ; 0803: 10 FC BPL $0801 

فيليب ، الفائز في المسابقة ، يأخذها إلى أقصى الحدود في رمزه. أذكر عنوان الحرف الأخير من السلسلة $07C0 (== $0400+24*40 ). هذه القيمة غير موجودة في zeropage أثناء التهيئة. ومع ذلك ، كأثر جانبي لكيفية استخدام روتين التمرير من ROM لقيم zeropage المؤقتة ، ستحتوي العناوين $D1-$D2 في إخراج الدالة على القيمة $07C0 . لذلك ، لتخزين البيكسل ، بدلاً من STA $07C0,x يمكنك استخدام الفهرس غير المباشر الأقصر الذي يتناول STA ($D1),y بايت واحد.

خدعة 4. تحميل الأمثل


يحتوي نموذج C64 PRG الثنائي النموذجي على ما يلي:

  • أول 2 بايت: عنوان التنزيل (عادةً $0801 )
  • 12 بايت من تسلسل التمهيد الأساسي

يشبه تسلسل التمهيد الرئيسي هذا (يعالج $801-$80C ):

 0801: 0B 08 0A 00 9E 32 30 36 31 00 00 00 080D: 8D 20 D0 STA $D020 

دون الخوض في تفاصيل حول تخطيط الذاكرة الرمز المميز BASIC ، هذا التسلسل أكثر أو أقل يتوافق مع "10 SYS 2061". العنوان 2061 ( $080D ) هو المكان الذي يتم فيه تشغيل برنامج رمز الجهاز الفعلي عند تنفيذ مترجم BASIC للأمر SYS.

يبدو فقط أن 14 بايت أكثر من اللازم. استخدم Philip و Matlev و Geir العديد من الحيل الصعبة للتخلص تمامًا من التسلسل الرئيسي. يتطلب هذا تحميل PRG باستخدام LOAD"*",8,1 ، منذ LOAD"*",8 يتجاهل عنوان تحميل PRG (وحدتي بايت الأولى) ويتم تحميله دائمًا عند $0801 .



تم استخدام طريقتين هنا:

  • خدعة المكدس
  • الحيلة الأساسية إعادة تعيين خدعة

خدعة المكدس


الحيله هي ان $01F8 في كومة المعالج في $01F8 قيمة تشير الى نقطة الدخول المطلوبة. يتم ذلك عن طريق إنشاء PRG الذي يبدأ بمؤشر 16 بت إلى الكود الخاص بنا وتحميل PRG بسعر $01F8 :

  * = $01F8 !word scroll - 1 ; overwrite stack scroll: jsr $E8EA 

بمجرد انتهاء تحميل الجرافة BASIC (انظر الكود بعد التفكيك ) ويريد العودة إلى المتصل باستخدام RTS ، فإنه يعود مباشرة إلى PRG لدينا.

الحيلة الأساسية إعادة تعيين خدعة


هذا أسهل في التفسير ببساطة من خلال النظر إلى PRG بعد التفكيك.

 02E6: 20 EA E8 JSR $E8EA 02E9: A4 D5 LDY $D5 02EB: A9 A0 LDA #$A0 02ED: 99 20 D0 STA $D020,Y 02F0: 91 D1 STA ($D1),Y 02F2: 9D B5 07 STA $07B5,X 02F5: E6 D6 INC $D6 02F7: 65 90 ADC $90 02F9: 85 90 STA $90 02FB: C6 D5 DEC $D5 02FD: 30 FE BMI $02FD 02FF: 90 E7 BCC $02E8 0301: 4C E6 02 JMP $02E6 

انتبه إلى السطر الأخير ( JMP $02E6 ). يبدأ تعليمة JMP من $0301 بعنوان القفز من $0302-$0303 .

عند تحميل هذا الرمز في الذاكرة بدءًا من العنوان $02E6 ، $02E6 كتابة القيمة $02E6 على عناوين $0302-$0303 . حسنًا ، هذا الموقع له معنى خاص: يحتوي على مؤشر إلى "دورة الانتظار الأساسية" (انظر بطاقة ذاكرة C64 للحصول على مزيد من التفاصيل). يؤدي تنزيل PRG إلى الكتابة فوق $02E6 ، وبالتالي ، عندما يحاول مترجم BASIC بعد إعادة تعيين دافئ الانتقال إلى حلقة الانتظار ، فإنه لا يدخل هذه الحلقة أبدًا ، بل يدخل في برنامج التقديم!

الحيل الأخرى مع إطلاق BASIC


اكتشف Petri خدعة إطلاق BASIC أخرى تسمح لك بإدخال ثوابتك في zeropage. في هذه الطريقة ، تقوم يدويًا بإنشاء تسلسل بدء BASIC المميز الخاص بك وترميز الثوابت في أرقام الأسطر لبرنامج BASIC. عند المدخلات ، أرقام الأسطر الأساسية ، مهم ، سيتم تخزين الثوابت في العناوين $39-$3A . ذكي جدا!

خدعة 5. تدفق التحكم المخصصة


فيما يلي نسخة مبسطة قليلاً من الحلقة السينية التي تطبع سطرًا واحدًا فقط ثم تتوقف عن التنفيذ:

  lda #39 sta x1 xloop: lda #$a0 ldx x1 sta $0400 + 24*40, x adc yf sta yf bcc no_scroll ; scroll screen up! jsr $e8ea no_scroll: dec x1 bpl xloop ; intentionally halt at the end inf: jmp inf 

ولكن هناك خطأ. عندما نرسم البيكسل الأخير ، لا يمكننا التمرير على الشاشة بعد الآن. وبالتالي ، هناك حاجة إلى فروع إضافية لإيقاف التمرير بعد تسجيل بكسل آخر:

  lda #39 sta x1 xloop: lda #$a0 ldx x1 sta $0400 + 24*40, x dec x1 ; skip scrolling if last pixel bmi done adc yf sta yf bcc no_scroll ; scroll screen up! jsr $e8ea no_scroll: jmp xloop done: ; intentionally halt at the end inf: jmp inf 

يشبه تدفق التحكم إلى حد كبير ما سينتج عن برنامج التحويل البرمجي C من برنامج منظم. يقدم رمز تخطي التمرير الأخير تعليمة JMP abs جديدة تستغرق 3 بايت. يبلغ طول القفزات الشرطية وحدتي بايت فقط ، نظرًا لأنهما يشفران عناوين الانتقال باستخدام معامِل نسبي 8 بت مع عنونة مباشرة.

يمكن تجنب JMP "لتخطي التمرير الأخير" من خلال تحريك مكالمة التمرير لأعلى الحلقة وتغيير بنية تدفق التحكم قليلاً. إليك كيفية قيام Philip بتنفيذها:

  lda #39 sta x1 scroll: jsr $e8ea xloop: lda #$a0 ldx x1 sta $0400 + 24*40, x adc yf sta yf dec x1 ; doesn't set carry! inf: bmi inf ; hang here if last pixel! bcc xloop ; next pixel if no scroll bcs scroll ; scroll up and continue 

هذا يلغي تماما JMP واحدة من ثلاثة بايت وتحويل JMP الآخر إلى فرع شرطي اثنين بايت ، مما يوفر ما مجموعه 4 بايت.

خدعة 6. خطوط مع ضغط قليلا


لا تستخدم بعض العناصر عداد ميل الخط ، بل تضغط البتات على ثابت 8 بت. تعتمد هذه التعبئة على حقيقة أن موضع البيكسل على طول الخط يتوافق مع نمط متكرر 8 بكسل:

 int mask = 0xB6; // 10110110 uint8_t* dst = screenRAM; for (int x = 0; x < 40; x++) { dst[x] = 0xA0; if (mask & (1 << (x&7))) { dst += 40; // go down a row } } 

هذا يترجم إلى مجمع المدمجة إلى حد ما. ومع ذلك ، عادة ما تكون خيارات عداد الميل أصغر.

الفائز


إليكم البرنامج الفائز بمسابقة 34 بايت من Philip. معظم الحيل أعلاه تعمل بشكل جيد في الكود:

 ov = $22 ; == $40, initial value for the overflow counter ct = $D5 ; == $27 / 39, number of passes. Decrementing, finished at -1 lp = $D1 ; == $07C0, pointer to bottom line. Set by the kernal scroller ; Overwrite the return address of the kernal loader on the stack ; with a pointer to our own code * = $01F8 .word scroll - 1 scroll: jsr $E8EA ; Kernal scroll up, also sets lp pointer to $07C0 loop: ldy ct ; Load the decrementing counter into Y (39 > -1) lda #$A0 ; Load the PETSCII block / black col / ov step value sta $D020, y ; On the last two passes, sets the background black p1: sta $07C0 ; Draw first block (left > right line) sta (lp), y ; Draw second block (right > left line) inc p1 + 1 ; Increment pointer for the left > right line adc ov ; Add step value $A0 to ov sta ov dec ct ; Decrement the Y counter bmi * ; If it goes negative, we're finished bcc loop ; Repeat. If ov didn't overflow, don't scroll bcs scroll ; Repeat. If ov overflowed, scroll 

ولكن لماذا أسهب في 34 بايت؟


بمجرد انتهاء المسابقة ، شارك الجميع برموزهم وملاحظاتهم - وحدثت سلسلة من المناقشات الحيوية حول كيفية تحسينها. بعد الموعد النهائي ، تم تقديم عدة خيارات أخرى:


تأكد من النظر - هناك العديد من اللؤلؤ الحقيقي.



شكرا للقراءة. وشكر خاص إلى Matlev و Phil و Geir و Petri و Jamie و Ian و David على المشاركة (آمل ألا يفوتني أي أحد - كان من الصعب حقًا تتبع جميع الإشارات على Twitter!)

سكرتير خاص بيتري دعا مسابقة بلدي "السنوي" ، لذلك ، آه ، ربما أراك في العام المقبل.

Source: https://habr.com/ru/post/ar464377/


All Articles