What causes a ROM to skip over an "infinite loop" instruction? Interrupts?

Started by RetroRain, March 06, 2016, 08:26:12 AM

Previous topic - Next topic

RetroRain

Bearing in mind, we're talking about the NES here.

Let's say I switch to bank 05, and I jump to $8000.

Then, at $8000, I have this code:

JMP $8000

This produces an infinite loop.

Apparently, with the Teenage Mutant Ninja Turtles ROM, having observed the code in the trace logger (from what I could observe, as the trace logger doesn't log "skipped" lines), it executes that JMP $8000, but then continues to carry on other game code.

In other words, my JMP $8000 is not producing the infinite loop that I want (I want it for testing purposes).

I remember seeing in the Super Mario Bros. disassembly that there is an Infinite Loop in the code.  The loop seems to be infinite, and yet all of the other code of the game is executed.

The only thing that could cause these things to happen in both Teenage Mutant Ninja Turtles and Super Mario Bros. are Interrupts correct?

And if not in the case of TMNT, what is causing my Infinite Loop to simply be executed once and then bypassed?  When tested with other ROMs, such as Super Mario Bros. 2 (USA) for instance, when I switch a bank and then execute an infinite loop (to make sure the proper bank is loaded), it executes the infinite loop, and doesn't bypass it, which is what I want.

In TMNT, in the hardwired bank ($C000 - $FFFF), I load a random bank in, from 0 to 6 for instance, and then jump to the beginning of that bank ($8000), and then execute an infinite loop at that point (JMP $8000), and yet it is executing that instruction, and then bypassing it.  Logically, it should not be doing that.  Unless of course logically speaking I'm not taking into consideration that there is some other factor that is causing that, such as Interrupts.

The way TMNT handles things is very different from the way other ROMs do things, as I'm observing first-hand.

Having done a lot of testing, it seems that the bank switching that I thought wasn't happening, actually does happen, but is then bypassed, which gives the illusion that the bankswitch never happened, because the switching happens so fast.  But the fact that bank switching happens regardless of an infinite loop puzzles me.

So, if all of this is indeed because of interrupts, how do I go about using that to my advantage?  In other words, how do I get the game to not bypass my infinite loop, for instance?

This is very important to me in figuring out why some ROMs work the way they do, and would help me greatly in my current project that is on ice because of this dilemma.  I have to switch to a bank that has free space, and KEEP that bank there until my code is done being executed, and then any other switching can happen.

Thank you for reading and for any information you could provide.
My YouTube Channel: RetroRainZX85 - https://www.youtube.com/channel/UCdHK6fSwUlcM-q8_EgZQfdw


Disch

Quote from: Rockman on March 06, 2016, 08:26:12 AM
So, if all of this is indeed because of interrupts, how do I go about using that to my advantage?  In other words, how do I get the game to not bypass my infinite loop, for instance?

Well... you probably don't want to bypass an infinite loop, because an infinite loop will just cause your game to deadlock.  But there are basically 2 kinds of interrupts on the NES:  NMIs and IRQs

NMIs can be enabled or disabled by setting/clearing the high bit of $2000.  When enabled, an NMI will occur at the start of every VBlank.  Games use NMIs to not only sync up their drawing, but also as a base clock to drive their logic.  So TMNT as you're describing it, probably does something like this:

- NMI triggers
- Disable NMI
- Do your drawing & logic updates
- Enable NMIs
- Infinite Loop, waiting for NMI
- NMI triggers
- Rinse, repeat

This will ensure the game updates at an even 60 times per second (or less if there's too much to do in one frame, which is when you get the slowdown)



The other kind of interrupts are IRQs, and on the NES they're typically used for raster effects like splitting the screen.  There are a bunch of different kind of IRQs (typically mapper controlled), but the most common is the MMC3 IRQ counter which lets you count scanlines, and will generate an IRQ when the desired scanline is being rendered.  This will allow your code to cut in and change the scroll, resulting in a split screen.

The NES also generates audio IRQs and DMC IRQs -- but those are not nearly as useful and are very rarely used.  Typically the useful IRQs come from the mapper.

Regardless of where the IRQ comes from, though, you can stop all IRQs by setting the 'I' status flag, which will prevent IRQs from interrupting your code.  You can do this with a SEI command.  As long as I is set, an IRQ will never cut into your code.

Note that the I flag will NOT stop NMIs.  The only way to stop NMIs is to disable them via $2000.

Dr. Floppy

Almost every NES game I've analyzed (which is admittedly nowhere near the 800-title strong library) contains an infinite loop at the end of the main program sequence. This loop is predicated upon the value of some RAM slot (usually in zero-page) being zero. A common setup:

A5 FF
F0 FC
A9 00
85 FF
4C 00 C0


The first two commands repeat ad infinitum until the electron gun reaches the end of Scanline 239, which triggers the Non Maskable Interrupt and yanks the Program Counter out of that loop. Towards the end of the NMI Routine, just before all the traditional AXY-juggling, you'll see something like:

A9 01
85 FF


Thus, when the NMI Routine finishes and plops the Program Counter back into the not-quite-infinite loop, the critical RAM slot will contain a non-zero value thus allowing the PC to bypass the branch and escape. (At which point the very first item of business is to zero-out that RAM slot, thus setting up the loop for the next frame.)

Disch

Opcodes?  What are you, some kind of animal?  Use mnemonics!   ;D

Dr. Floppy

Quote from: Disch on March 06, 2016, 05:37:37 PM
Opcodes?  What are you, some kind of animal?  Use mnemonics!   ;D

Yes, probably, and aight. (In that precise order.)

LDA_zp $FF          A5 FF
BEQ_#FC             F0 FC
LDA_imm #00      A9 00
STA_zp $FF          85 FF
JMP_abs $C000   4C 00 C0

LDA_imm #01      A9 01
STA_zp $FF          85 FF

RetroRain

Thank you for the information guys.  Yeah, that ROM definitely uses interrupts, since you can switch a bank, but then the game automatically takes care of the bankswitching afterwards, and it has a special routine which constantly writes the ppu tiles to the titlescreen.  I have to somehow get my graphics written to the titlescreen, since disabling and re-enabling the NMI isn't working, and putting it right after the titlescreen writes slows the game down.  I am going to meditate on this and see what I can do.  At least I know that I can definitely do the hack that I want to do.

Thanks again.
My YouTube Channel: RetroRainZX85 - https://www.youtube.com/channel/UCdHK6fSwUlcM-q8_EgZQfdw

Disch

In general, if you're writing your own drawing code, you're probably doing it wrong.

Games typically have a buffer in RAM that they write drawing commands to -- and then the NMI code will read that buffer and do the appropriate drawing.

Your hack should either change what is being written to that buffer -- or should be adding new commands to that buffer.

So the first question you should ask is not "how do I draw my stuff?".... the question you REALLY should be asking is "how does the game draw its stuff?"

RetroRain

I agree with you, but that still doesn't solve my problem.

I know how the game draws its graphics, but I don't know how to go about using it.  It's hard to explain, but I will try.


07:C975:A9 E8     LDA #$E8
07:C977:85 FC     STA $00FC = #$00
07:C979:A9 01     LDA #$01
07:C97B:0A        ASL
07:C97C:8D DF 03  STA $03DF = #$00
07:C97F:4C 89 C9  JMP $C989
07:C982:A9 00     LDA #$00
07:C984:85 FC     STA $00FC = #$00
07:C986:8D DF 03  STA $03DF = #$00
07:C989:BD CE E3  LDA $E3CE,X @ $E3CE = #$FE
07:C98C:85 00     STA $0000 = #$00
07:C98E:BD CF E3  LDA $E3CF,X @ $E3CF = #$C9
07:C991:85 01     STA $0001 = #$CA
07:C993:20 B3 C4  JSR $C4B3
07:C996:A9 00     LDA #$00
07:C998:85 FD     STA $00FD = #$00
07:C99A:AD 02 20  LDA $2002 = #$00
07:C99D:A0 01     LDY #$01
07:C99F:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9A1:8D 06 20  STA $2006 = #$00
07:C9A4:88        DEY
07:C9A5:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9A7:8D 06 20  STA $2006 = #$00
07:C9AA:A2 00     LDX #$00
07:C9AC:A9 02     LDA #$02
07:C9AE:20 1B C7  JSR $C71B
07:C9B1:A0 00     LDY #$00
07:C9B3:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9B5:C9 FF     CMP #$FF
07:C9B7:F0 3E     BEQ $C9F7
07:C9B9:C9 7F     CMP #$7F
07:C9BB:F0 32     BEQ $C9EF
07:C9BD:A8        TAY
07:C9BE:10 1D     BPL $C9DD
07:C9C0:29 7F     AND #$7F
07:C9C2:85 02     STA $0002 = #$78
07:C9C4:A0 01     LDY #$01
07:C9C6:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9C8:8D 07 20  STA $2007 = #$00
07:C9CB:C4 02     CPY $0002 = #$78
07:C9CD:F0 03     BEQ $C9D2
07:C9CF:C8        INY
07:C9D0:D0 F4     BNE $C9C6
07:C9D2:A9 01     LDA #$01
07:C9D4:18        CLC
07:C9D5:65 02     ADC $0002 = #$78
07:C9D7:20 1B C7  JSR $C71B
07:C9DA:4C B1 C9  JMP $C9B1
07:C9DD:A0 01     LDY #$01
07:C9DF:85 02     STA $0002 = #$78
07:C9E1:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9E3:A4 02     LDY $0002 = #$78
>07:C9E5:8D 07 20  STA $2007 = #$00
07:C9E8:88        DEY
07:C9E9:D0 FA     BNE $C9E5
07:C9EB:A9 02     LDA #$02
07:C9ED:D0 E8     BNE $C9D7
07:C9EF:A9 01     LDA #$01
07:C9F1:20 1B C7  JSR $C71B
07:C9F4:4C 9A C9  JMP $C99A
07:C9F7:A9 03     LDA #$03
07:C9F9:85 20     STA $002
07:C9FB:4C 84 C4  JMP $C484
07:C9FE:00        BRK
07:C9FF:20 78 00  JSR $0078
07:CA02:78        SEI
07:CA03:00        BRK
07:CA04:78        SEI
07:CA05:00        BRK
07:CA06:78        SEI
07:CA07:00        BRK
07:CA08:78        SEI
07:CA09:00        BRK
07:CA0A:78        SEI
07:CA0B:00        BRK
07:CA0C:78        SEI
07:CA0D:00        BRK
07:CA0E:78        SEI
07:CA0F:00        BRK
07:CA10:40        RTI
07:CA11:00        BRK
07:CA12:7F        UNDEFINED
07:CA13:00        BRK
07:CA14:28        PLP
07:CA15:78        SEI
07:CA16:00        BRK
07:CA17:78        SEI
07:CA18:00        BRK
07:CA19:78        SEI
07:CA1A:00        BRK
07:CA1B:78        SEI
07:CA1C:00        BRK
07:CA1D:78        SEI


What the game is doing is reading and writing the graphics indirectly.  For instance, the code above shortened, so you can see the real important parts:


07:C99A:AD 02 20  LDA $2002 = #$00
07:C99D:A0 01     LDY #$01
07:C99F:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9A1:8D 06 20  STA $2006 = #$00
07:C9A4:88        DEY
07:C9A5:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9A7:8D 06 20  STA $2006 = #$00


07:C9C4:A0 01     LDY #$01
07:C9C6:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9C8:8D 07 20  STA $2007 = #$00
07:C9CB:C4 02     CPY $0002 = #$78
07:C9CD:F0 03     BEQ $C9D2
07:C9CF:C8        INY
07:C9D0:D0 F4     BNE $C9C6
07:C9D2:A9 01     LDA #$01
07:C9D4:18        CLC
07:C9D5:65 02     ADC $0002 = #$78
07:C9D7:20 1B C7  JSR $C71B
07:C9DA:4C B1 C9  JMP $C9B1
07:C9DD:A0 01     LDY #$01
07:C9DF:85 02     STA $0002 = #$78
07:C9E1:B1 00     LDA ($00),Y @ $CA78 = #$FE
07:C9E3:A4 02     LDY $0002 = #$78
>07:C9E5:8D 07 20  STA $2007 = #$00
07:C9E8:88        DEY
07:C9E9:D0 FA     BNE $C9E5


So, what you have to do is write to $00, $01, and $02.

$00 is addeded with what is in $01, which gives you the full address:

So, for instance, $00 = #$00, and $01 is #$80,

So, the full address is $8000.

Then, Y is loaded with what is in $02.  So if I load Y with #$37,

I will get $8000 + #$37, which is $8037, and then the Y gets decremented.  So it is reading the bytes in reverse, and writing them to the screen.

The titlescreen data is located in bank 01, at $BD00.  The above code shows $CA78, but that is because it is reading something else.  That code above was just to show where the PPU routine is.  If you run the code a few times after the debugger snaps, $BD00 will show up.

The problem is, how do I ADD the bytes I want written in that $BD00 range?  At first glance it doesn't seem like I can without screwing things up badly.  I'm going to have to count the total number of bytes, and the study the PPU routine a bit more to figure out how what is exactly going on.

The other thing I have had trouble with, is if I add a JSR or JMP after all of this writing routine, or at a different point in the code, it screws up the game as well.  I tried to manually write the indirect values into $00, $01, and $02, and JSRsing to the PPU writing routine, and I'm still getting problems.

It's a case of I know where the PPU tiles writing routine is, I've got a general idea of how it operates, but I don't know how to squeeze the data I want written in there.

So I just have to continue to meditate on this until the answer comes to me.

I hate games that do this, but I would like to know how to get around it.  I believe Super Mario Bros. 2 (USA) did the same thing, which is why I was having problems with it.  In fact, I couldn't figure it out with that game, so I just used sprites to display the titlescreen option box.  But I would very much like to know how this works, so I can have the titlescreen I want.

I'm going to study the PPU writing routine a lot more, and see if I can learn more from it.

Thank you for the information Disch.  I know you can only help up to a point, and I appreciate the information that you given me.  I would feel good about myself if I can figure this out myself, so I will continue to try.  I am not going to give up on this problem.  For however long it takes I will make it my business to figure this problem out.

March 09, 2016, 02:29:35 AM - (Auto Merged - Double Posts are not allowed before 7 days.)

I did it! :D

I was able to write my tiles to the titlescreen! :D

Now, I didn't exactly use the game's PPU routine.  What I did instead was, I set a breakpoint of write to $2007, to bring up the game's PPU routine as I have done many times before.  Only this time, I had the trace logger opened, and I logged the code of the titlescreen.  I searched for the proper bankswitch to bank 01 (which I knew had the titlescreen tiles), and checked the trace logger and the debugger.  I was able to find out where bank 1 was loaded right before it jumped to the ppu writing routine.  So I jumped to some free space I had in the hardwired bank, loaded my free bank, and wrote the tiles manually.  It worked like a charm! :)

Thanks for your help guys! :)
My YouTube Channel: RetroRainZX85 - https://www.youtube.com/channel/UCdHK6fSwUlcM-q8_EgZQfdw