Görüntü Yapısı

Emülatörümüzün temel fonksiyonlarını tamamladıktan sonra, ekrana görüntü vereceğimiz yapımızın tanımına geçebiliriz. Öncelikle kullanacağımız yapıları modülümüze ekleyelim:

use minifb::{Key, Scale, Window, WindowOptions};

minifb daha önce de bahsettiğimiz gibi en yaygın işletim sistemlerinde bir pencere açıp içerisine bir şeyler çizebileceğimiz basit bir paket. Aynı zamanda basılan tuşları da alabildiği için bizim için biçilmiş kaftan. Yapıları kullanıma aldıktan sonra bazı sabitleri tanımlayalım:

const WIDTH: usize = 64;
const HEIGHT: usize = 32;
const FOREGROUND_COLOR: u32 = 0x5294e2;
const BACKGROUND_COLOR: u32 = 0x282c34;

CHIP-8 64x32 piksel boyutunda bir ekrana sahip. Bu nedenle genişlik (WIDTH) ve yükseklik (HEIGHT) olarak iki sabit tanımlaması yaptık. Ayrıca arka plan rengi ve ön plan rengi olmak üzere iki 32-bit cinsinden sayı tanımladık. minifb 32-bit cinsinden olan değerleri ekrana cizebilir, isterseniz burada farklı renkler seçebilirsiniz.

Rust'ta sabit tanımlamaları const anahtar kelimesiyle yapılır. Tüm sabitler 'static ömrüne sahiptir.

Artık görüntü yapımızı tanımlayabiliriz:

pub struct Display {
    buffer: [[u8; WIDTH]; HEIGHT],
    window: Window,
}

CHIP-8 siyah beyaz bir ekrana sahip olduğundan görüntü buffer'ı 8-bitlik 64x32 boyutunda bir matris. Bu buffer içerisinde dolu pikseller için 1, boş pikseller için 0 değerini kullanacağız. window alanı için de minifb'nin Window tipini kullanıyoruz.

Görüntü Metodlarının Uygulanması

impl Display {
}

Bloğunu oluşturarak görüntü metodlarını eklemeye başlayalım.

    pub fn new() -> Self {
        Self {
            buffer: [[0; WIDTH]; HEIGHT],
            window: Window::new(
                "Rust ile CHIP-8",
                WIDTH,
                HEIGHT,
                WindowOptions {
                    scale: Scale::X16,
                    ..WindowOptions::default()
                },
            )
            .expect("Pencere oluşturulurken hata oluştu"),
        }
    }

new metodumuz yeni bir Display instance'ı oluşturmaya yarıyor. Burada kullanmış olduğumuz Self; Dislay ile aynı anlama geliyor. İsterseniz impl bloğunda tekrar tekrar tip ismini yazmak yerine Self anahtar kelimesini kullanabilirsiniz. Unutmayın ki birinci harf büyük olmalı ve bu anahtar kelime, metod argümanlarında kullanılan self ile karıştırılmamalıdır.

WindowOptions yapısı tamamen açık (public) alanlara sahip bir yapı. Bu nedenle bu yapı için herhangi bir yardımcı metod olmadan direkt instance oluşturabiliriz. Biz varsayılan (Default) özelliği eklenmiş bu yapının, sadece scale alanını değiştireceğimizden, geri kalan alanların varsayılan değeri alması için ..WindowOptions::default() yazımını kullandık. Rust'ta kullanılan bu yazım, yapının tanımlanmamış diğer alanları için varsayılan değerleri almasını sağlıyor. 64x32 piksel modern bir bilgisayarda çok küçük olacağı için, scale ile belirlediğimiz değerle, minifb paketi görütüyü belirlenen oranda arttırıyor (bizim belirlediğimiz X16 oranında).

Metodumuzda şu an için herhangi bir hata yönetimi yapmadığımızdan dolayı, pencere oluşturulurken karşılaşacağımız olası bir hata durumunda expect ile panikleyip çıkıyoruz.

Yardımcı Metodlar

    pub fn is_open(&self) -> bool {
        self.window.is_open() && !self.window.is_key_down(Key::Escape)
    }

    pub fn clear(&mut self) {
        self.buffer = [[0; WIDTH]; HEIGHT];
    }

is_open metodu adından da anlaşılabileceği gibi, penceremizin açık olup olmadığını kontrol ediyor. Aynı zamanda Esc tuşunun basılı olup olmadığını da kontrol eden bu metod bool dönüyor. Bu sayede oyuncu istediği zaman Esc tuşuna basarak pencereyi kapatabilir ve emülatörü sonlandırabilir.

clear metodu, yapımız içerisinde yer alan buffer'ı temizlemeye yarıyor.

Draw Instruction'ı ile Çizim

CHIP-8 draw instruction'ı daha önce belirlediğimiz gibi:

    /// 0xDxyn DRW: `x` ve `y` registerinden başayarak `n` adet byte sprite'ı ekranda
    /// gösterir. Çakışma (collision) durumu `F` registerinde tutulur.
    Draw(Register, Register, u8),

Şeklinde yapıyor. Aynı zamanda CHIP-8 sadece bu instruction ile collision (spriteların birbiriyle çakışma) durumunu kontrol edebiliyor. Bu işlemi şu şekilde yapabiliriz:

    pub fn draw(&mut self, x: usize, y: usize, sprite: &[u8]) -> u8 {
        let mut collision = 0;
        let mut xi: usize;
        let mut yj: usize;

        for (j, sprite) in sprite.iter().enumerate() {
            for i in 0..8 {
                xi = (x + i) % WIDTH;
                yj = (y + j) % HEIGHT;

                if sprite & (0x80 >> i) != 0 {
                    if self.buffer[yj][xi] == 1 {
                        collision = 1
                    }
                    self.buffer[yj][xi] ^= 1;
                }
            }
        }

        self.draw_screen();
        collision
    }

sprite bellekten gelen 8-bitlik verilere sahip bir slice. Öncelikle for döngümüz her bir sprite için j sayacıyla çalışıyor. Ardından ikinci for döngüsünde 8-bitlik verinin her bir bitini kontrol etmeye başlıyoruz. xi ve yj ile yazılacak bit'in ekrandaki pozisyonu belirleniyor. Ardından her bir bit: 0x80: 10000000 üzerinde çalıştığımız bit kadar (i) sağa kaydırılarak sprite ile XOR işlemi sayesinde herhangi bir çizilecek piksel varsa 1, yoksa 0 sonucu elde ediliyor. Ardından daha önce aynı pozisyonda herhangi sprite çizildiyse çakışma değeri olan collision 1 yapılıyor.

Son olarak CHIP-8'de spritelar ekrana XOR ile çizildiği için, üzerinde çalıştığımız piksel 1 ile XOR işlemi ile belirleniyor.

buffer'ın Pencereye Çizimi

minifb &[u32] tipinden bir yapıyı ekrana çizebilir. Bu nedenle [[0; WIDTH]; HEIGHT] tipinden olan matrisimizi [u32] tipden bir array'a çevirmemiz gerekli. Bu işlemi daha önce sabit olarak belirlediğimiz arkaplan ve önplan rengini de kullanarak şu şekilde yapabiliriz:

    pub fn draw_screen(&mut self) {
        let mut buffer = [0; WIDTH * HEIGHT];
        let mut loc = 0;
        for y in 0..HEIGHT {
            for x in 0..WIDTH {
                buffer[loc] = if self.buffer[y][x] == 1 {
                    FOREGROUND_COLOR
                } else {
                    BACKGROUND_COLOR
                };
                loc += 1;
            }
        }
        self.window.update_with_buffer(&buffer).unwrap();
    }

Referanstan-referansa Dönüştürme ile Pencere Alanının Alımı

İleride yazacağımız emülasyon döngüsü için, Display yapımız içerisinde yer alan window alanına ulaşabilmemiz gerekli. Bu işlemi Rust'ın referanstan-referansa çevirme işlemi yapan AsMut özelliği ile yapacağız. Display yapımıza AsMut özelliğini katalım ve as_mut çağrıldığında Window tipini dönelim:

impl AsMut<Window> for Display {
    fn as_mut(&mut self) -> &mut Window {
        &mut self.window
    }
}