e-tipsmemo

ごった煮

NES Emulator in Rust : 5.1 Sprite timing

e-tipsmemo.hatenablog.com
以前にBackgroundが実装されていたがその時にSpriteも実装していた。(前後関係がおかしいままであるが)
Sprite描写の実装は参考サイトとは少し違う
(参考サイトの実装方法ではSprite zero hitのタイミングが数pixelずれる気がしたので)


描写タイミング

Ppuの1Cycleに対応する関数内で、PPUが走査しているy座標に含まれるSpriteを探す処理を行う(sprite_evaluation)

    pub fn run<'a>(nes: &'a Nes) -> impl Generator<Yield = PpuStep, Return = !> + 'a {
        let mut sprite_evaluation = Ppu::sprite_evaluation(nes);
        let mut run_renderer = Ppu::renderer(nes);

        move || loop {
            loop {
                match Pin::new(&mut sprite_evaluation).resume(()) {
                    GeneratorState::Yielded(PpuStep::Cycle) => {
                        break;
                    }
                    GeneratorState::Yielded(step) => {
                        yield step;
                    }
                }
            }
            loop {
                match Pin::new(&mut run_renderer).resume(()) {
                    GeneratorState::Yielded(PpuStep::Cycle) => {
                        break;
                    }
                    GeneratorState::Yielded(step) => {
                        yield step;
                    }
                }
            }

            yield PpuStep::Cycle;
        }
    }


あるy座標に描写される予定のスプライト情報(座標、スプライト番号、反転や優先度)は、Secondary OAMにストアされる。(最大8個)

PPU Frame Timingの下に、「Sprite evaluation for next scanline」というのがあり、すべてのVisible scanlineでこの処理が行われている。

Sprite Zero Hit

このエミュレータではSprite evaluationのときに、同時にスプライトがPrimary OAMの0番目であるフラグを持っておかないと、実際の描写時に、Sprite Zero Hitさせることができない。(と思う)

実装

    fn sprite_evaluation<'a>(nes: &'a Nes) -> impl Generator<Yield = PpuStep, Return = !> + 'a {
        move || loop {
            for frame in 0.. {
                let frame_is_odd = frame % 2 != 0;
                for y in 0u16..cparams::FRAME_H as u16 {
                    let should_skip_first_cycle = frame_is_odd && y == 0;
                    if !should_skip_first_cycle {
                        // The first cycle of each scanline is idle (except
                        // for the first cycle of the pre-render scanline
                        // for odd frames, which is skipped)
                        // yield PpuStep::Cycle;
                    }
                    //x = 0;
                    let oam = nes.ppu.oam();
                    nes.ppu.status.update(|s| s & !PpuStatus::OVERFLOW);

                    if y < 240 || y == 261 {
                        let mut oam_buf = [0xFF; 32];
                        let mut sprite_zero_flag = [false; 8];
                        let mut oam_buf_index = 0; //0 ~ 7

       ......//Cycles

                        //evaluate sprite
                        for (i, _) in (64..=255).step_by(2).enumerate() {
                            //Cycle 65-256
                            //i = 0 ~ 63?
                            yield PpuStep::Cycle;
                            yield PpuStep::Cycle;

                            if i < 64 {
                                let sprite_y = oam[i * 4].get() as u16;
                                if sprite_y <= y && y < sprite_y + 8 {
                                    if oam_buf_index < 8 {
                                        let y = oam[i * 4 + 0].get();
                                        let x = oam[i * 4 + 3].get();
                                        oam_buf[oam_buf_index * 4 + 0] = oam[i * 4 + 0].get();
                                        oam_buf[oam_buf_index * 4 + 1] = oam[i * 4 + 1].get();
                                        oam_buf[oam_buf_index * 4 + 2] = oam[i * 4 + 2].get();
                                        oam_buf[oam_buf_index * 4 + 3] = oam[i * 4 + 3].get();
                                        sprite_zero_flag[oam_buf_index] = i == 0;
                                        oam_buf_index = oam_buf_index + 1;
                                    } else {
                                        nes.ppu.status.update(|s| s | PpuStatus::OVERFLOW);
                                    }
                                }
                            }
                        }

       ......//Cycles

                        for _ in 320..cparams::FRAME_W - 1 {
                            yield PpuStep::Cycle;
                        }

                        nes.ppu.secondary_oam.set(oam_buf);
                        nes.ppu.sprite_zero_flag.set(sprite_zero_flag);

                    } else {
                        for _ in 0..cparams::FRAME_W - 1 {
                            nes.ppu.secondary_oam.set([0xFF; 32]);
                            nes.ppu.sprite_zero_flag.set([false; 8]);
                            yield PpuStep::Cycle;
                        }
                    }

対象のy座標にスプライトが入っていたら、oamから コピーして、最後にsecondary_OAMに入れる。
run_renderer ではそれを使用してBackgroundと描写する。

動作

まだrenderer側にspriteを表示する記事を書いていないが、実際は実装してそれなりに動作している。

giko008.nesやgiko009.nesが実行して、キャラを動かせる。
f:id:katakanan:20210929215646p:plain

ドンキーコングはかなりよさそう。
しかしピーチが欠けている。
f:id:katakanan:20210929215801p:plain

(Sprite Zero hitも実装していとマリオが動く)
マリオのキノコが透明なところがあったり、雲が赤色だったりするがゲームとしてプレイはできるようになった。
f:id:katakanan:20210929215940p:plain
前後関係がおかしい
f:id:katakanan:20210929220016p:plain
正しい表示
f:id:katakanan:20211012211844p:plain

感想

それっぽくマリオが動いてしまったので正しい色を表示するPPUのデバッグをやる気がなくなってきた。。。