Question about MMC1 bank swap only working sometimes
Moderator: Moderators
- NeverCameBack
- Posts: 61
- Joined: Mon Feb 24, 2020 12:22 am
Question about MMC1 bank swap only working sometimes
I've been stuck for a few sessions on this...
*Edit - Maybe I have fixed it by entering the blue section below - It seems to work now 95% of the time - Artifacts are gone - Apologies if this post is a waste of space
I have a title screen where, when you hit A, a CHR bank is swapped, a new screen appears - drawn from the newly swapped CHR files, and you can press A to progress through 3 of these screens. This works well enough.
On the 3rd of these single slide intro screens, you press A, causing a CHR and PRG bank swap to occur, which should bring you into the game itself.
About 50% of the time, it glitches up and freezes. The other 50% - The bank swap occurs normally and the game starts. I've been trying to figure out the cause of this inconsistency.
If you make it to the "game", you can press B for another CHR+PRG swap, and a start menu is drawn. New music track plays - Again this is somewhat of a lottery if it'll not glitch up, although when it doesn't glitch up, it seems to work every time (you can pause and unpause x10+ in a row without a glitch). However there are noticeable "artifacts" momentarily appearing on screen when entering and returning to the game screen from this start menu. Artifacts are definitely gone now with below edit
Would anyone see an obvious problem with the order I'm doing things?
- A is pressed, I set a trigger bit for PRG and/or CHR swaps to happen
- Code finishes running and we go to "forever loop" until NMI occurs
- NMI occurs and program jumps to "NMI" label
- I turn off NMI interrupt immediately
- JSR to my bank swap routine (which works without freezing the game up, sometimes), then RTS
- In the above line about the bank swap routine, I turn off rendering of BG and Sprites - This may have fixed the issue...
- Check if new screen needs to be drawn (yes in this case)
- Wait for v-blank
- Turn off rendering of BG and Sprites
- Load Pallettes to PPU
- Load BG tile data to PPU
- Wait for vblank again
- Re-enable BG and sprite rendering
- Turn NMI back on
*For some reason, before returning to the "game" screen from the start menu, I found that I needed to "reset the mapper" by commanding:
resetMapper:
lda #$80
sta $8000
Otherwise the PRG and CHR bank #s I command to swap to seem to be mis-interpreted, and presumably a non-existing bank is swapped in and the result is a crashed game. But commanding the above reset on the mapper before the title screen -> game, and game -> start menu bank swaps seems to cause the same problem as above. So it is as if: "don't reset the mapper the first two bank swaps, but I have to do it on the 3rd or else it'll glitch out".
Thanks if anyone takes the time to read this, and if I find an answer myself I'll post my solution.
*Edit - Maybe I have fixed it by entering the blue section below - It seems to work now 95% of the time - Artifacts are gone - Apologies if this post is a waste of space
I have a title screen where, when you hit A, a CHR bank is swapped, a new screen appears - drawn from the newly swapped CHR files, and you can press A to progress through 3 of these screens. This works well enough.
On the 3rd of these single slide intro screens, you press A, causing a CHR and PRG bank swap to occur, which should bring you into the game itself.
About 50% of the time, it glitches up and freezes. The other 50% - The bank swap occurs normally and the game starts. I've been trying to figure out the cause of this inconsistency.
If you make it to the "game", you can press B for another CHR+PRG swap, and a start menu is drawn. New music track plays - Again this is somewhat of a lottery if it'll not glitch up, although when it doesn't glitch up, it seems to work every time (you can pause and unpause x10+ in a row without a glitch). However there are noticeable "artifacts" momentarily appearing on screen when entering and returning to the game screen from this start menu. Artifacts are definitely gone now with below edit
Would anyone see an obvious problem with the order I'm doing things?
- A is pressed, I set a trigger bit for PRG and/or CHR swaps to happen
- Code finishes running and we go to "forever loop" until NMI occurs
- NMI occurs and program jumps to "NMI" label
- I turn off NMI interrupt immediately
- JSR to my bank swap routine (which works without freezing the game up, sometimes), then RTS
- In the above line about the bank swap routine, I turn off rendering of BG and Sprites - This may have fixed the issue...
- Check if new screen needs to be drawn (yes in this case)
- Wait for v-blank
- Turn off rendering of BG and Sprites
- Load Pallettes to PPU
- Load BG tile data to PPU
- Wait for vblank again
- Re-enable BG and sprite rendering
- Turn NMI back on
*For some reason, before returning to the "game" screen from the start menu, I found that I needed to "reset the mapper" by commanding:
resetMapper:
lda #$80
sta $8000
Otherwise the PRG and CHR bank #s I command to swap to seem to be mis-interpreted, and presumably a non-existing bank is swapped in and the result is a crashed game. But commanding the above reset on the mapper before the title screen -> game, and game -> start menu bank swaps seems to cause the same problem as above. So it is as if: "don't reset the mapper the first two bank swaps, but I have to do it on the 3rd or else it'll glitch out".
Thanks if anyone takes the time to read this, and if I find an answer myself I'll post my solution.
- Attachments
-
- Incandesant_NEW_r5.nes
- (136.02 KiB) Downloaded 27 times
-
- Incandesant_NEW_r4.nes
- (136.02 KiB) Downloaded 23 times
Re: Question about MMC1 bank swap only working sometimes
I've looked a bit at your r4 ROM and it's not clear to me what the root cause of the crashes is. I've seen it crash for two reasons: one is an RTI that returns to an incorrect address, and the other is an indirect JMP to an incorrect address. One suspicious thing I've seen is the code handling a second NMI during a first NMI. This is normally a bad thing; if a second NMI occurs during a first one, it should return without doing any work so the first NMI can complete its work unimpeded. Otherwise, the second NMI ends up working on partially-updated state, and then the first NMI has state suddenly change partway through execution. Whether it's causing a crash or not, it's likely a problem.
A couple other things I've seen that aren't so great:
1. Your game is waiting for vblank by spinning on the vblank flag in PPU_STATUS ($2002), which is bad for two reasons. One is that this is an unreliable way to detect vblank: if you read the vblank flag on the same PPU cycle where the flag is set, you read it as 0 and the flag is cleared. This means you miss vblank for that entire frame. The other is that reading this flag within the first few PPU cycles of it being set suppresses NMI, so you read it as 1 but NMI never occurs because the NMI signal was asserted so briefly that the CPU didn't see it. We discuss this on the wiki here at the bold-and-underline section. I don't know if NMI suppression is a problem in your game because the loop I looked at had NMI disabled, but missing the vblank flag likely does happen, which can cause random stuttering as the game doesn't advance to the next frame when it should.
Instead, the code that is waiting for vblank should be polling a variable that is modified by the NMI handler. When the variable changes, the code knows the NMI handler has run, signaling that the new frame has started. I don't know if this fixes the bug; I'm not sure why you're hitting RTI without being in an interrupt context. But it would explain why the bug only sometimes happens.
2. You only have interrupt vectors in the last bank. With MMC1, you should assume that every MMC1 register is in a random state at power-on, which means any bank could be loaded where the CPU vectors are. You should have a little reset stub at the same location in each bank that resets the mapper (an easy way to do this is an INC instruction incrementing the INC opcode itself), which will immediately load in the final bank. Then that bank can jump to the startup code that initializes the rest of the registers. This was actually discussed on the forums a couple times recently.
A couple other things I've seen that aren't so great:
1. Your game is waiting for vblank by spinning on the vblank flag in PPU_STATUS ($2002), which is bad for two reasons. One is that this is an unreliable way to detect vblank: if you read the vblank flag on the same PPU cycle where the flag is set, you read it as 0 and the flag is cleared. This means you miss vblank for that entire frame. The other is that reading this flag within the first few PPU cycles of it being set suppresses NMI, so you read it as 1 but NMI never occurs because the NMI signal was asserted so briefly that the CPU didn't see it. We discuss this on the wiki here at the bold-and-underline section. I don't know if NMI suppression is a problem in your game because the loop I looked at had NMI disabled, but missing the vblank flag likely does happen, which can cause random stuttering as the game doesn't advance to the next frame when it should.
Instead, the code that is waiting for vblank should be polling a variable that is modified by the NMI handler. When the variable changes, the code knows the NMI handler has run, signaling that the new frame has started. I don't know if this fixes the bug; I'm not sure why you're hitting RTI without being in an interrupt context. But it would explain why the bug only sometimes happens.
2. You only have interrupt vectors in the last bank. With MMC1, you should assume that every MMC1 register is in a random state at power-on, which means any bank could be loaded where the CPU vectors are. You should have a little reset stub at the same location in each bank that resets the mapper (an easy way to do this is an INC instruction incrementing the INC opcode itself), which will immediately load in the final bank. Then that bank can jump to the startup code that initializes the rest of the registers. This was actually discussed on the forums a couple times recently.
- NeverCameBack
- Posts: 61
- Joined: Mon Feb 24, 2020 12:22 am
Re: Question about MMC1 bank swap only working sometimes
Thanks very much for the help. In my first post, I'd only corrected the artifacts on screen change - So it's true the crashes during/right after bank swapping PRG+CHR together are still occuring... I've never had a crash only swapping CHR (which I do x3 during the title sequence).
The RTI and JMP crashes:
I've replaced the one RTI instance with "JMP Foreverloop" - however I still get crashes.
To my current knowledge, the RTI would return the CPU to the address it was at before the NMI interrupt. In my program this should be the "forever loop" - Where the CPU waits for NMI after it's run through the code.
I'll try using the FCEUX emulator debugger and "break" at the Op codes for RTI and JMP during the crash, to see if I can repeat what you tested...
Since you saw crashes here, I'm sure this has something to do with it.. so will keep investigating.
I'm not sure what you mean by a second NMI after a first - There is only one NMI label in the code... so as I understood, the CPU will jump to the address of that label...
About waiting for a V-blank detection via $2002 in a loop:
Aside from the initial start-up, I have two times where my code will wait for v-blank:
#1: Right after NMI interrupt - right before a new BG palette and nametable are written to $2007
#2: Right after the above, before "PPU Cleanup" - enable BG and sprite rendering, re-enable NMI.
I removed these two v-blank "waits", but it didn't correct the occasional crashing.
Removing the 2nd instance, there was always artifacts when changing screens - I guess something needs to be there to wait before the PPU cleanup happens as per above?
The link you sent, I believe it says to use NMI instead of waiting for v-blank... But after NMI interrupt, I immediately turn NMI interrupts off (so that I have time to write update the nametables in the PPU), so am not sure what I should use instead of v-blank (BIT $2002).
About the vectors:
I have the MMC1 mapping set to swap only the first two PRG banks ($8000 & $A000).
Since my "fixed" PRG banks are the last two, does the CPU really just decide randomly which banks will be the "Active 4"?
I would think I'd of gotten a crash at start-up before, but the title screen boots everytime.
I have a .org instruction to tell the assembler to always write the reset vectors (.dw followed by the string - and I guess my NESASM translates these strings into machine code that the NES would know what they are...)
.org $FFFA
.dw NMI
.dw RESET
.dw 0
I imagine these have worked on every boot up - or else I'd get a frozen title screen right?
I'm sure I didn't fully understand the posts that you had linked.
I'll keep plugging away at this and update if I find a reason...
The RTI and JMP crashes:
I've replaced the one RTI instance with "JMP Foreverloop" - however I still get crashes.
To my current knowledge, the RTI would return the CPU to the address it was at before the NMI interrupt. In my program this should be the "forever loop" - Where the CPU waits for NMI after it's run through the code.
I'll try using the FCEUX emulator debugger and "break" at the Op codes for RTI and JMP during the crash, to see if I can repeat what you tested...
Since you saw crashes here, I'm sure this has something to do with it.. so will keep investigating.
I'm not sure what you mean by a second NMI after a first - There is only one NMI label in the code... so as I understood, the CPU will jump to the address of that label...
About waiting for a V-blank detection via $2002 in a loop:
Aside from the initial start-up, I have two times where my code will wait for v-blank:
#1: Right after NMI interrupt - right before a new BG palette and nametable are written to $2007
#2: Right after the above, before "PPU Cleanup" - enable BG and sprite rendering, re-enable NMI.
I removed these two v-blank "waits", but it didn't correct the occasional crashing.
Removing the 2nd instance, there was always artifacts when changing screens - I guess something needs to be there to wait before the PPU cleanup happens as per above?
The link you sent, I believe it says to use NMI instead of waiting for v-blank... But after NMI interrupt, I immediately turn NMI interrupts off (so that I have time to write update the nametables in the PPU), so am not sure what I should use instead of v-blank (BIT $2002).
About the vectors:
I have the MMC1 mapping set to swap only the first two PRG banks ($8000 & $A000).
Since my "fixed" PRG banks are the last two, does the CPU really just decide randomly which banks will be the "Active 4"?
I would think I'd of gotten a crash at start-up before, but the title screen boots everytime.
I have a .org instruction to tell the assembler to always write the reset vectors (.dw followed by the string - and I guess my NESASM translates these strings into machine code that the NES would know what they are...)
.org $FFFA
.dw NMI
.dw RESET
.dw 0
I imagine these have worked on every boot up - or else I'd get a frozen title screen right?
I'm sure I didn't fully understand the posts that you had linked.
I'll keep plugging away at this and update if I find a reason...
Re: Question about MMC1 bank swap only working sometimes
The CPU will jump to that label every time an NMI happens, even if your code hasn't returned yet from the previous time it jumped there. If your NMI code is not "re-entrant" (e.g. it uses global variables in RAM), then this can cause strange things to happen when the second instance returns and the first one tries to continue where it left off.NeverCameBack wrote: ↑Sun Feb 02, 2025 7:47 pm I'm not sure what you mean by a second NMI after a first - There is only one NMI label in the code... so as I understood, the CPU will jump to the address of that label...
Quietust, QMT Productions
P.S. If you don't get this note, let me know and I'll write you another.
P.S. If you don't get this note, let me know and I'll write you another.
- NeverCameBack
- Posts: 61
- Joined: Mon Feb 24, 2020 12:22 am
Re: Question about MMC1 bank swap only working sometimes
The CPU will jump to that label every time an NMI happens, even if your code hasn't returned yet from the previous time it jumped there. If your NMI code is not "re-entrant" (e.g. it uses global variables in RAM), then this can cause strange things to happen when the second instance returns and the first one tries to continue where it left off.
[/quote]
I may understand now.
However I thought I was safe from this - Since as per nerdy nights, I turned off the NMI directive immediately after entering the NMI label. Once the PPU is loaded with the nametable data, it gets turned back on. Then I believe my code is short enough that the CPU can make it to the end (forever loop) before an NMI interrupts it again (and it has to be right, otherwise you'd have to write you code so that it takes two screen frames to completely execute?).
For my original problem, using the debugger, I'm finding that my crash happens when I hit a BRK instruction inside the famistudio_nesasm code (provided by famistudio of course, and which I patched into my game). I have to look into this further. I may end up trying to delay the sound engine from starting for a second or two after my prg bank swap, since this happens about 25% of the time - moreso with one song than another (prg swapping to my start menu music barely makes it crash).
[/quote]
I may understand now.
However I thought I was safe from this - Since as per nerdy nights, I turned off the NMI directive immediately after entering the NMI label. Once the PPU is loaded with the nametable data, it gets turned back on. Then I believe my code is short enough that the CPU can make it to the end (forever loop) before an NMI interrupts it again (and it has to be right, otherwise you'd have to write you code so that it takes two screen frames to completely execute?).
For my original problem, using the debugger, I'm finding that my crash happens when I hit a BRK instruction inside the famistudio_nesasm code (provided by famistudio of course, and which I patched into my game). I have to look into this further. I may end up trying to delay the sound engine from starting for a second or two after my prg bank swap, since this happens about 25% of the time - moreso with one song than another (prg swapping to my start menu music barely makes it crash).
Last edited by NeverCameBack on Sun Feb 02, 2025 10:15 pm, edited 1 time in total.
Re: Question about MMC1 bank swap only working sometimes
While I normally think it's easier to debug a .nes ROM file than source code (because of how good Mesen's tools are), I think there might be some structural problems here that could be easier to track down with source code. Are you able to post your source code?
Re: NMIs, as Quietust said, if NMI is enabled, you'll get an NMI every single vblank, regardless of whether the previous one is finished. When you RTI, you'll return back to the previous NMI at the place where it was interrupted (or if you never RTI, that previous NMI handler will just never complete, having done only part of the work it was supposed to do). This can cause all sorts of bugs and you need to protect against it. If you do all your work in the NMI, a naive way to address this problem is to turn off NMI at the start of the NMI handler and enable it again at the end. This has drawbacks, though. There's some discussion of how to handle threading on the wiki.
The reason you're not getting random crashes in FCEUX is that it is an emulator and it doesn't support undefined MMC1 power-on state. It just picks a default and your code works with that default. Mesen (a more accurate emulator with many more options and better development tools) has options to enable randomization of at least some hardware state, including MMC1 registers, and I can see that the game crashes on some power cycles when these are enabled.
Re: NMIs, as Quietust said, if NMI is enabled, you'll get an NMI every single vblank, regardless of whether the previous one is finished. When you RTI, you'll return back to the previous NMI at the place where it was interrupted (or if you never RTI, that previous NMI handler will just never complete, having done only part of the work it was supposed to do). This can cause all sorts of bugs and you need to protect against it. If you do all your work in the NMI, a naive way to address this problem is to turn off NMI at the start of the NMI handler and enable it again at the end. This has drawbacks, though. There's some discussion of how to handle threading on the wiki.
This vector issue isn't related to the CPU at all. When the CPU powers on or resets, it simply fetches a reset vector from $FFFC-$FFFD. The problem here is the MMC1 mapper itself, in the cartridge. When the cartridge receives power, the MMC1 registers have an undefined state; they can, in theory, contain any value, which means it could have any PRG-ROM bank mode (in $8000) and any PRG bank (in $E000). This means that any single 16 KiB bank could be loaded into the region from where the CPU fetches its vectors. In reality, it seems that there may be differences between certain specific revisions of MMC1 chip with regard to what power-on values are actually possible, but in general, you should assume that hardware registers can have any value at power-on unless it is clearly documented that they have some guaranteed value.NeverCameBack wrote: ↑Sun Feb 02, 2025 7:47 pm Since my "fixed" PRG banks are the last two, does the CPU really just decide randomly which banks will be the "Active 4"?
I would think I'd of gotten a crash at start-up before, but the title screen boots everytime.
The reason you're not getting random crashes in FCEUX is that it is an emulator and it doesn't support undefined MMC1 power-on state. It just picks a default and your code works with that default. Mesen (a more accurate emulator with many more options and better development tools) has options to enable randomization of at least some hardware state, including MMC1 registers, and I can see that the game crashes on some power cycles when these are enabled.
This issue wasn't related to crashing in any way; it's just something your code is doing that is wrong because it will introduce random stutters where the game loses a vblank flag or NMI and has to wait an entire extra frame while doing nothing. The only time you should ever poll this register for the vblank flag is in your reset handler, because NMI can't be used at first and you need to wait a minimum amount of time before it starts to work. After that, the NMI must be your time source, not the vblank flag.NeverCameBack wrote: ↑Sun Feb 02, 2025 7:47 pm About waiting for a V-blank detection via $2002 in a loop:
- NeverCameBack
- Posts: 61
- Joined: Mon Feb 24, 2020 12:22 am
Re: Question about MMC1 bank swap only working sometimes
Sure, here is a .zip file of my code, and the latest .nes file (only real difference from before is I made a start menu (press b during game) with a cursor that moves now. Pressing B again returns you to the game screen, and the position of the character is remembered.
I really wanted to fix this crashing issue before the game gets more complex.
- "Incandesant_NEW_r6.asm" is the "main" file.
All of the other routine files are called inside of that.
I attached a picture of the address where the BRK occurs (and I've found this in the famistudio_nesasm.asm file), but have yet to look into what's causing it. They give some good comments in the famistudio_nesasm file. I removed the BRK to see what would happen and I seem to get stuck inside a shallow loop inside the file.
*I'll look into restructuring what I have to in order to not rely on the v-blank flag. Right now, it seems to cause some nuisance issues if I remove my instances of it.
*Understood about the variability of real hardware. Will look into switching to Mesen if it has more features. Was thinking it'd be nice to "step back" instead of only forward through the code.
I really wanted to fix this crashing issue before the game gets more complex.
- "Incandesant_NEW_r6.asm" is the "main" file.
All of the other routine files are called inside of that.
I attached a picture of the address where the BRK occurs (and I've found this in the famistudio_nesasm.asm file), but have yet to look into what's causing it. They give some good comments in the famistudio_nesasm file. I removed the BRK to see what would happen and I seem to get stuck inside a shallow loop inside the file.
*I'll look into restructuring what I have to in order to not rely on the v-blank flag. Right now, it seems to cause some nuisance issues if I remove my instances of it.
*Understood about the variability of real hardware. Will look into switching to Mesen if it has more features. Was thinking it'd be nice to "step back" instead of only forward through the code.
- Attachments
-
- Incandesant_NEW_r6.nes
- (176.02 KiB) Downloaded 17 times
-
- Incandescent_02Feb24.7z
- (1.21 MiB) Downloaded 18 times
- NeverCameBack
- Posts: 61
- Joined: Mon Feb 24, 2020 12:22 am
Re: Question about MMC1 bank swap only working sometimes
Without looking further into the BRK cause, I added:
lda #$01
jsr famistudio_music_pause
to my "intro.asm" routine, after A is pressed, along with the bank swap preparation, etc. and this may have solved it... maybe I just wasn't being nice to the sound engine by swapping it's song out mid play. I've gotten to my game screen about 15 times in a row now in FCEUX and haven't had a crash yet.
There is a quick flash of sprites/artifacts upon entering the game screen, since I'm sure I have other problems / optimizations to address. Thanks for bringing these to my attention.
lda #$01
jsr famistudio_music_pause
to my "intro.asm" routine, after A is pressed, along with the bank swap preparation, etc. and this may have solved it... maybe I just wasn't being nice to the sound engine by swapping it's song out mid play. I've gotten to my game screen about 15 times in a row now in FCEUX and haven't had a crash yet.
There is a quick flash of sprites/artifacts upon entering the game screen, since I'm sure I have other problems / optimizations to address. Thanks for bringing these to my attention.
- Attachments
-
- Incandesant_NEW_r7.nes
- (176.02 KiB) Downloaded 28 times
Re: Question about MMC1 bank swap only working sometimes
I've looked through the code a bit, and looked at the crash in r4 again. I think the RTI crash was happening because you had code spinning on the vblank flag in the swappable bank, and it gets interrupted by NMI, which swaps the bank but does not swap it back. So, when the NMI returns with RTI, the code it's returning to is swapped out, causing a crash. In this case, you're not necessarily doing a ton of work, but because you're polling the vblank flag, your code will necessarily take more than a frame to complete and will (usually) be interrupted by the NMI if enabled. I also wouldn't assume you never do more than a frame's worth of work at a time; most games have at least the occasional lag frame and you need to handle it gracefully or your game can crash. Whether it ever happens right now or not, assume that it will eventually happen.NeverCameBack wrote: ↑Sun Feb 02, 2025 10:13 pm However I thought I was safe from this - Since as per nerdy nights, I turned off the NMI directive immediately after entering the NMI label. Once the PPU is loaded with the nametable data, it gets turned back on. Then I believe my code is short enough that the CPU can make it to the end (forever loop) before an NMI interrupts it again (and it has to be right, otherwise you'd have to write you code so that it takes two screen frames to completely execute?).
As-is, your code is currently very NMI-unsafe and I think it will continue to cause random bugs for you until you fix it. Since you currently have an everything-in-NMI approach, you need to either:
- make it impossible for an NMI to occur outside of your forever loop. To do this, disable NMI at the start of the NMI handler. At the very end of the NMI handler (after all the work is done for this frame), LDA $2002 to clear the vblank flag, enable NMI again via $2000, and then RTI. Reading $2002 is critical because if you don't do this, the next NMI could occur anywhere in vblank, potentially toward the end without enough time left to interact with the PPU.
- or, make it so the NMI checks whether the previous NMI handler completed its work and, if not, exits without doing anything. This is a better approach, but also more complicated. Your NMI handler needs to save registers at the start and restore them at the end so that the registers it uses don't interfere with the code that was interrupted. You then have a variable which is set at the start of the NMI handler and cleared at the end right before RTI, and if this variable is already set when the NMI starts, you simply return from that NMI. Ideally, you'd still run the sound engine on these lag frames to prevent sound from lagging, too, but this might require swapping banks with MMC1, and you need to be very careful about this because you may have interrupted MMC1 register writes. To start with, I wouldn't run the sound engine on lag frames.
In case it helps, I wrote up some example code showing how I generally structure my NMI and game loop. I don't know how helpful it will be for you at this point, since you've gone for a different model (everything-in-NMI) and you have a lot of code already, but maybe it will give you some ideas. Some useful concepts are:
- Register saving in NMI.
- The frame_ready variable that is used to see if this is a lag frame, in which case it skips all the PPU work.
- Handling audio on lag frames (but you have to be careful to make sure that any mapper interaction in this function is safe).
- PPU registers being modified in a single place, so they're always updated safely and at the right time (during vblank).
Re: Question about MMC1 bank swap only working sometimes
One conceptually simple way to keep time is to have the NMI handler increment the number of times that the sound driver needs to be updated. Then write a routine that checks this variable and calls the sound driver that many times. This way, you won't have to worry quite as much about sharing the MMC1 shift register between the main program and the NMI handler. If you do get a lag frame this way, you end up with a barely noticeable blip in your instruments' envelope, and tempo remains solid.
If you end up using this approach, you can put a breakpoint on this variable reaching 2 or more so that you can find where to insert calls to your sound catch-up routine even on blank-screen loading.
If you end up using this approach, you can put a breakpoint on this variable reaching 2 or more so that you can find where to insert calls to your sound catch-up routine even on blank-screen loading.
Re: Question about MMC1 bank swap only working sometimes
To be honest, if the OP went all the way through a "everything in NMI" paradigm, I don't see any need to change that. Just either disable NMIs completely while the first one is not completed (*), or, much better solution, have a flag that states the NMI is not completed, and if this is the case, run the sound logic only and return. In the case where sound logic requires MMC1 bankswitching, I'd have another flag that says whether MMC1 switching is ongoing, and if so, don't bother running the sound logic. Having an extremely occasional and unlikely slow down in music when the game is lagging already sounds like a perfectly acceptable solution.
In code, this would be something like this:
(*) Caveat, if you go through the path of simply disabling further NMIs in your interrupt code, the problem is that another NMI could trigger after you enable them back, but before you return to the main (jmp infinite) code. Even if you read $2002 to prevent an NMI triggererring in the middle of VBlank, nothing prevent the start of the VBlank to happen just here. Repeated again and again with the same timing, this could have the negative effect of filling up the stack eventually causing a crash, or simply cause glitches if you don't back up the registers. For that reason I'd aim for the path of always backing up the registers, and use a "already in NMI" flag instead, as seen above.
In code, this would be something like this:
Code: Select all
Reset:
sei
cld
ldx #$ff
txs
<blah blah, initialisation there>
lda #$80
sta $ffff ; Reset the MMC1 before using it for the first time
<blah blah, initialization there>
infinite:
jmp infinite ; Once initialisation is done, everything is done in NMI code
NMI:
pha
txa
pha
tya
pha
lda already_in_nmi_flag
bne _skip_VRAM_updates ; Do not do VRAM updates if game logic is unfinished
inc already_in_nmi_flag
jsr update_NT_AT ; NT/AT updates...
jsr update_palette ; Palette updates...
lda HScroll
sta $2005 ;Set scrolling
lda VScroll
sta $2005
lda M_2000
sta $2000 ; 9th scrolling bits, PT select
_skip_VRAM_updates:
lda MMC1_write_ongoing_flag
bne _skip_sound ; Do not do sound if MMC1 write is onoging
lda #SOND_BANK
jsr MMC1_PRG_SWITCH ; Switch sound bank
jsr handle_sound ; Music engine, update sound registers...
_skip_sound:
lda already_in_nmi_flag
bne _skip_game_logic ; Do not run game logic again if was unfinished
jsr ReadJoypad
jsr GameLogic ; Run whatever logic necessary here
dec already_in_nmi_flag ;We're completed now, next NMI can do the VRAM updates and logic processing
_skip_game_logic:
pla
tay
pla
tax
pla
IRQ:
rti ; Assuming IRQs aren't used anyways
MMC1_WRITE_REG0
inc MMC1_write_ongoing_flag ; Make sure to not run sound if MMC1 write is ongoing
sta $9fff
lsr A
sta $9fff
lsr A
sta $9fff
lsr A
sta $9fff
lsr A
sta $9fff
dec MMC1_write_ongoing_flag
rts
MMC1_WRITE_REG1
<same as above but for Reg1>
MMC1_WRITE_REG2
<same as above for Reg2>
MMC1_WRITE_REG3
<same as above for Reg3>
Useless, lumbering half-wits don't scare us.