1, 2, 3  Next
Game Boy STAT IRQs 
Author Message
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Game Boy STAT IRQs
Fork from this discussion: viewtopic.php?p=25527#p25527I'm attempting to emulate this properly. But I'm hitting some serious roadblocks; most games are completely broken now. Could really use a hand reviewing this code.Current notes:I'm not aiming for perfected timing right now. I know that various properties cause the exact timing windows for various modes to change. We're not there yet, we're still in STAT IRQ 101 territory. Let's just pretend the timings are consistent. I think that's enough to get most software running.Current concerns:When and where is the test done for LY=LYC? Is that during mode 3?Reading from $ff41 ... I take it mode 1 (Vblank) lasts the entire time from LY=144 to LY=153, correct? If so, then the LY=LYC test won't ever happen during Vblank if the above question's answer is yes.How and when does IRQ lowering take place? Before, I was always raising IRQs, and they'd stay pending until acknowledged. But if the STAT shared IRQ line goes to zero before that happens, does that prevent the IRQ from firing? I am taking it that the STAT IRQ line exists in the PPU, and that the IF bit in the CPU is a separate affair.What happens when the display is disabled? It looks as though not even Vblank IRQs are supposed to trigger? Does $ff41 still report we're in mode 1? Do LYC IRQs still work?Humble request:Sometimes I don't understand very technical descriptions well. If at all possible, please demonstrate what you mean with code changes to the referenced code below. If not possible, then I understand. You're doing me the favor here either way.

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 03:41
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
Here is my previous STAT IRQ emulation:

Code:
auto PPU::main() -> void {
  status.lx = 0;
  interface->lcdScanline();  //Super Game Boy notification

  if(status.display_enable) {
    //LYC of zero triggers on LY==153
    if((status.lyc && status.ly == status.lyc) || (!status.lyc && status.ly == 153)) {
      if(status.interrupt_lyc) cpu.interrupt_raise(CPU::Interrupt::Stat);
    }

    if(status.ly <= 143) {
      scanline();
      if(status.interrupt_oam) cpu.interrupt_raise(CPU::Interrupt::Stat);
    }

    if(status.ly == 144) {
      if(status.interrupt_vblank) cpu.interrupt_raise(CPU::Interrupt::Stat);
      else if(status.interrupt_oam) cpu.interrupt_raise(CPU::Interrupt::Stat);  //hardware quirk
      cpu.interrupt_raise(CPU::Interrupt::Vblank);
    }
  }

  add_clocks(92);

  if(status.ly <= 143) {
    for(auto n : range(160)) {
      if(status.display_enable) run();
      add_clocks(1);
    }

    if(status.display_enable) {
      if(status.interrupt_hblank) cpu.interrupt_raise(CPU::Interrupt::Stat);
      cpu.hblank();
    }
  } else {
    add_clocks(160);
  }

  add_clocks(204);

  if(++status.ly == 154) {
    status.ly = 0;
    scheduler.exit(Scheduler::Event::Frame);
  }
}


Code:
auto PPU::mmio_read(uint16 addr) -> uint8 {
  if(addr == 0xff41) {  //STAT
    uint mode;
    if(status.ly >= 144) mode = 1;  //Vblank
    else if(status.lx < 80) mode = 2;  //OAM
    else if(status.lx < 252) mode = 3;  //LCD
    else mode = 0;  //Hblank

    return (status.interrupt_lyc << 6)
         | (status.interrupt_oam << 5)
         | (status.interrupt_vblank << 4)
         | (status.interrupt_hblank << 3)
         | ((status.ly == status.lyc) << 2)
         | (mode << 0);
  }
  ...
}

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 03:42
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
Here is my current STAT IRQ emulation:

Note that add_clocks was renamed to wait.

Altered Space is mostly fixed (some new glitches now); Kirby's Dream Land 2 is completely busted now.

Code:
auto PPU::main() -> void {
  status.lx = 0;
  interface->lcdScanline();  //Super Game Boy notification

  if(status.display_enable && status.ly <= 143) {
    mode(2);
    scanline();
    wait(92);

    mode(3);
    for(auto n : range(160)) {
      run();
      wait(1);
    }

    mode(0);
    cpu.hblank();
    wait(204);
  } else {
    mode(1);
    wait(456);
  }

  status.ly++;

  if(status.ly == 144) {
    cpu.interrupt_raise(CPU::Interrupt::Vblank);
    scheduler.exit(Scheduler::Event::Frame);
  }

  if(status.ly == 154) {
    status.ly = 0;
  }
}

auto PPU::mode(uint mode) -> void {
  status.mode = mode;
  bool irq = status.irq;

  if(status.mode == 0) {  //hblank
    status.irq = status.interrupt_hblank;
  }

  if(status.mode == 1) {  //vblank
    status.irq = status.interrupt_vblank || (status.interrupt_lyc && coincidence());
  }

  if(status.mode == 2) {  //oam
    status.irq = status.interrupt_oam || (status.interrupt_lyc && coincidence());
  }

  if(status.mode == 3) {  //render
    status.irq = false;
  }

  if(!irq && status.irq) {
    cpu.interrupt_raise(CPU::Interrupt::Stat);
  } else if(!status.irq) {
    cpu.interrupt_lower(CPU::Interrupt::Stat);
  }
}

auto PPU::coincidence() -> bool {
  //LYC of zero triggers on LYC=153
  return (status.lyc && status.ly == status.lyc) || (!status.lyc && status.ly == 153);
}


Code:
auto PPU::mmio_read(uint16 addr) -> uint8 {
  if(addr == 0xff41) {  //STAT
    return (status.interrupt_lyc << 6)
         | (status.interrupt_oam << 5)
         | (status.interrupt_vblank << 4)
         | (status.interrupt_hblank << 3)
         | ((status.ly == status.lyc) << 2)
         | (status.mode << 0);
  }
  ...
}

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 03:43
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
Here is the shared CPU handling of STAT IRQs shared between the two versions:

Code:
auto CPU::main() -> void {
  interrupt_test();
  exec();  //execute one CPU instruction
}

auto CPU::interrupt_raise(CPU::Interrupt id) -> void {
  if(id == Interrupt::Vblank) {
    status.interrupt_request_vblank = 1;
    if(status.interrupt_enable_vblank) r.halt = false;
  }

  if(id == Interrupt::Stat) {
    status.interrupt_request_stat = 1;
    if(status.interrupt_enable_stat) r.halt = false;
  }

  if(id == Interrupt::Timer) {
    status.interrupt_request_timer = 1;
    if(status.interrupt_enable_timer) r.halt = false;
  }

  if(id == Interrupt::Serial) {
    status.interrupt_request_serial = 1;
    if(status.interrupt_enable_serial) r.halt = false;
  }

  if(id == Interrupt::Joypad) {
    status.interrupt_request_joypad = 1;
    if(status.interrupt_enable_joypad) r.halt = r.stop = false;
  }
}

auto CPU::interrupt_lower(CPU::Interrupt id) -> void {
  if(id == Interrupt::Stat) {
    status.interrupt_request_stat = 0;
  }
}

auto CPU::interrupt_test() -> void {
  if(!r.ime) return;

  if(status.interrupt_request_vblank && status.interrupt_enable_vblank) {
    status.interrupt_request_vblank = 0;
    return interrupt_exec(0x0040);
  }

  if(status.interrupt_request_stat && status.interrupt_enable_stat) {
    status.interrupt_request_stat = 0;
    return interrupt_exec(0x0048);
  }

  if(status.interrupt_request_timer && status.interrupt_enable_timer) {
    status.interrupt_request_timer = 0;
    return interrupt_exec(0x0050);
  }

  if(status.interrupt_request_serial && status.interrupt_enable_serial) {
    status.interrupt_request_serial = 0;
    return interrupt_exec(0x0058);
  }

  if(status.interrupt_request_joypad && status.interrupt_enable_joypad) {
    status.interrupt_request_joypad = 0;
    return interrupt_exec(0x0060);
  }
}

auto CPU::interrupt_exec(uint16 pc) -> void {
  op_io();
  op_io();
  op_io();
  r.ime = 0;
  op_write(--r[SP], r[PC] >> 8);
  op_write(--r[SP], r[PC] >> 0);
  r[PC] = pc;
}


Code:
auto CPU::mmio_read(uint16 addr) -> uint8 {
  if(addr == 0xff0f) {  //IF
    return 0xe0
         | (status.interrupt_request_joypad << 4)
         | (status.interrupt_request_serial << 3)
         | (status.interrupt_request_timer << 2)
         | (status.interrupt_request_stat << 1)
         | (status.interrupt_request_vblank << 0);
  }
  if(addr == 0xffff) {  //IE
    return 0xe0
         | (status.interrupt_enable_joypad << 4)
         | (status.interrupt_enable_serial << 3)
         | (status.interrupt_enable_timer << 2)
         | (status.interrupt_enable_stat << 1)
         | (status.interrupt_enable_vblank << 0);
  }
  ...
}


Code:
auto CPU::mmio_write(uint16 addr, uint8 data) -> void {
  if(addr == 0xff0f) {  //IF
    status.interrupt_request_joypad = data & 0x10;
    status.interrupt_request_serial = data & 0x08;
    status.interrupt_request_timer = data & 0x04;
    status.interrupt_request_stat = data & 0x02;
    status.interrupt_request_vblank = data & 0x01;
    return;
  }
  if(addr == 0xffff) {  //IE
    status.interrupt_enable_joypad = data & 0x10;
    status.interrupt_enable_serial = data & 0x08;
    status.interrupt_enable_timer = data & 0x04;
    status.interrupt_enable_stat = data & 0x02;
    status.interrupt_enable_vblank = data & 0x01;
    return;
  }
  ...
}

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 03:47
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
Here's my WIP code:

It seems to have fixed my regressions in Mega Man II and Kirby's Dream Land II, while still allowing Altered Space to work. I've added comment notes for the changes that were necessary. No idea if this is even remotely correct or not.

Really need to understand how mode works when the display is disabled ... not really even sure if LY even bothers to run when the screen is disabled :/

Code:
auto PPU::main() -> void {
  status.lx = 0;
  interface->lcdScanline();  //Super Game Boy notification

  if(status.ly <= 143) {
    //NOTE!! scanline() and run() check display_enable; so we can plot blank pixels when the screen is off
    mode(2);
    scanline();
    wait(92);

    mode(3);
    for(auto n : range(160)) {
      run();
      wait(1);
    }

    mode(0);
    if(status.display_enable) cpu.hblank();
    wait(204);
  } else {
    mode(1);
    wait(456);
  }

  status.ly++;

  //NOTE!! without the && status.display_enable test; Kirby's Dream Land 2 breaks completely
  if(status.ly == 144 && status.display_enable) {
  //mode(1);
    cpu.interrupt_raise(CPU::Interrupt::Vblank);
  }

  if(status.ly == 154) {
    status.ly = 0;
    scheduler.exit(Scheduler::Event::Frame);
  }
}

auto PPU::mode(uint mode) -> void {
  if(!status.display_enable) mode = 1;
  status.mode = mode;
  bool irq = status.irq;

  if(status.mode == 0) {  //hblank
    status.irq = status.interrupt_hblank;
  }

  if(status.mode == 1) {  //vblank
    status.irq = status.interrupt_vblank || (status.interrupt_lyc && coincidence());
  }

  if(status.mode == 2) {  //oam
    status.irq = status.interrupt_oam || (status.interrupt_lyc && coincidence());
  }

  if(status.mode == 3) {  //render
    status.irq = false;
  }

  //NOTE!! without the !irq&& bit, Altered Space will break completely
  if(!irq && status.irq) {
    cpu.interrupt_raise(CPU::Interrupt::Stat);
  } else if(!status.irq) {
  //NOTE!! if we enable the below line, Mega Man's sprite will flicker in Mega Man II weapon select screen
  //cpu.interrupt_lower(CPU::Interrupt::Stat);
  }
}

auto PPU::coincidence() -> bool {
  //LYC of zero triggers on LYC=153
  //NOTE!! if we use the first line; Kirby's Dream Land 2's intro first scanline will be wrong
  //return status.ly == status.lyc;
  return (status.lyc && status.ly == status.lyc) || (!status.lyc && status.ly == 153);
}


Code:
auto PPU::mmio_read(uint16 addr) -> uint8 {
  if(addr == 0xff41) {  //STAT
    //NOTE!! doesn't seem to make a difference whether we use status.mode or mode;
    //even though mode has different timings and doesn't take display_enable into account
    uint mode;
    if(status.ly >= 144) mode = 1;  //Vblank
    else if(status.lx < 80) mode = 2;  //OAM
    else if(status.lx < 252) mode = 3;  //LCD
    else mode = 0;  //Hblank
    return (status.interrupt_lyc << 6)
         | (status.interrupt_oam << 5)
         | (status.interrupt_vblank << 4)
         | (status.interrupt_hblank << 3)
         | ((status.ly == status.lyc) << 2)
//       | (mode << 0);
         | (status.mode << 0);
  }
  ...
}

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 04:22

Joined: 2016-01-06 01:01
Posts: 67
 Re: Game Boy STAT IRQs
I've added a new test, which is verified on real hardware, but only passes in Gambatte.

Source:
https://github.com/Gekkio/mooneye-gb/bl ... blocking.s
Binary:
http://gekkio.fi/files/mooneye-gb/night ... locking.gb

The test is not sensitive to fine-grained timing, but simply expects the right sequence of things and the right behaviour of STAT IRQ.
It works exactly how I described earlier, and even though it runs through all lines 0-144 and all STAT interrupts are enabled, you never see more than the initial STAT interrupt because of the internal STAT IRQ line behaviour.
This test won't help you fix higan, but it should be useful to validate when you've implemented the right behaviour.

byuu wrote:
Sometimes I don't understand very technical descriptions well. If at all possible, please demonstrate what you mean with code changes to the referenced code below. If not possible, then I understand. You're doing me the favor here either way.


Sorry, I know I tend to do "technical brain dumps" when describing things :/
It's just difficult to describe things in code, because I usually don't immediately see how the behaviour should be implemented.


2016-06-05 07:38

Joined: 2016-01-06 01:01
Posts: 67
 Re: Game Boy STAT IRQs
Here's my idea of how it should work. Note that this is not PPU::mode() but PPU::update_status_irq()

Code:
auto PPU::update_status_irq() -> void {
  bool irq = status.irq;

  status.irq = false;

  if (status.interrupt_hblank) {
    status.irq |= (status.mode == 0);
  }
  if (status.interrupt_vblank) {
    status.irq |= (status.mode == 1);
  }
  if (status.interrupt_oam) {
    status.irq |= (status.mode == 2);
  }
  if (status.interrupt_lyc) {
    status.irq |= coincidence();
  }

  if (!irq && status.irq) {
    cpu.interrupt_raise(CPU::Interrupt::Stat);
  }
}


This code should be tidied up more, but I think it might be easier to understand the idea in this form.

Now, you need to call update_status_irq() whenever any value it depends on is changed. So, you need to call it:
  • after any status.mode change
  • after any status.interrupt_hblank change
  • after any status.interrupt_vblank change
  • after any status.interrupt_oam change
  • after any status.interrupt_lyc change
  • after any status.lyc change
  • after any status.ly change


2016-06-05 07:54

Joined: 2016-01-06 01:01
Posts: 67
 Re: Game Boy STAT IRQs
Here's a tidied up version:

Code:
auto PPU::calc_status_irq() -> bool {
  return (status.interrupt.hblank && status.mode == 0)
    || (status.interrupt.vblank && status.mode == 1)
    || (status.interrupt.oam && status.mode == 2)
    || (status.interrupt_lyc && coincidence());
}

auto PPU::update_status_irq() -> void {
  bool irq = status.irq;
  status.irq = calc_status_irq();

  if (!irq && status.irq) {
    cpu.interrupt_raise(CPU::Interrupt::Stat);
  }
}


2016-06-05 07:58
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
Thanks, gekkio!

> Sorry, I know I tend to do "technical brain dumps" when describing things :/

No worries, not your fault at all. Limitation on my part.

> Now, you need to call update_status_irq() whenever any value it depends on is changed. So, you need to call it:

At first I tried calling it at the top of tick() [whenever time passed], but that hit the first "FAIL: MODE=1 INTR" case.

I put it inside the while(clocks--) loop, and now I at least get your usual fail screen.

Registers

A:03 F:20
B:00 C:13
D:00 E:D8
H:02 L:49

I don't know what the registers are supposed to be or why.

But this is with your update_status_irq() function :/

On the bright side, all the regular DMG games still work, so this is good :D




EDIT: the failure is due to this:
auto PPU::coincidence() -> bool { return (status.lyc && status.ly == status.lyc) || (!status.lyc && status.ly == 153); }

If I change it to the much simpler:
auto PPU::coincidence() -> bool { return status.ly == status.lyc; }

Then your test passes.

I know the behavior's more complicated, something like it only being the case on the second half of LY=153 that the LY~="0" thing happens. But I really need more precise information on that to emulate it properly.

As a quick hack, this gets the KDL2 first scanline working, as well as passes your test:
Code:
auto PPU::coincidence() -> bool {
  uint ly = status.ly, lx = status.lx;
  if(ly == 153 && lx >= 80) ly = 0;  //lx can be anywhere from 0 - ~90 or so and KDL2 works
  return ly == status.lyc;
}

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 09:18
User avatar

Joined: 2014-09-25 13:52
Posts: 8242
 Re: Game Boy STAT IRQs
More bad news, this new behavior fails to run GBVideoPlayer.

But at this point, I'm just going to leave that broken. It destroyed way too many games, caused nasty SGB flickering, etc. Wasn't really worth it.

_________________
What the hell's going on? Can someone tell me please?
Why I'm switching faster than the channels on TV.
I'm black, then I'm white. No, something isn't right.
My enemy's invisible, I don't know how to fight.


2016-06-05 09:46
1, 2, 3  Next