Seamless bankswitch on MMC3?

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Seamless bankswitch on MMC3?

Post by rox_midge » Thu Jun 18, 2020 8:03 pm

Is there any way to get MMC3 to seamlessly bankswitch mid-frame? In theory I should just need to wait for HBLANK to start and write to $8000/$8001, yes? I'm getting a half-scanline full of snow.

I'm at that part of the project where I'm pretty sure that everything is turning out to be way harder than I thought it would be and I just want to play Metroid.

lidnariq
Posts: 9491
Joined: Sun Apr 13, 2008 11:12 am
Location: Seattle

Re: Seamless bankswitch on MMC3?

Post by lidnariq » Thu Jun 18, 2020 8:05 pm

Where does Mesen's Event Viewer say your IRQ and MMC3 mapper writes are happening?

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Thu Jun 18, 2020 8:59 pm

It looks like everything is happening in HBLANK. Here's just the mapper writes and IRQ:

Image

And here's everything that might matter:

Image

You can see two other places where I'm using the MMC3 to split, one in the middle of the screen, and one in the middle of the status bar. Those are working fine, and before I tried to switch the CHR bank mid-frame, the top of the status bar was working OK too.

I've futzed around with delays and can get it localized to one side of the screen or the other, but I can't entirely hide it. I really need to do three things: update the palette, swap tile banks, and set the scroll position. I'm able to set the scroll position pretty decently, but swapping the tile banks is giving me this snow, and although I can update the palette just fine during HBLANK, writing to it during VBLANK just completely destabilizes everything.

Nothing makes sense and basically I hate everything :D

User avatar
aa-dav
Posts: 89
Joined: Tue Apr 14, 2020 9:45 pm
Location: Russia

Re: Seamless bankswitch on MMC3?

Post by aa-dav » Thu Jun 18, 2020 9:09 pm

Yes of course.

Code: Select all

; "Охранная" проверка на недопущение
; повторного ключения файла
.ifndef MMC3_INC_GUARD
MMC3_INC_GUARD		= 1		; Охранный символ

; *** MMC_BANK - выбор банка страницу которого мы можем переключить (запись)
MMC3_BANK		= $8000
; Три нижних бита порта определяют какому банку мы будем менять маппинг:
MMC3_CHR_H0	= %00000000	; Банк $0000-07FF, 2Кб в CHR0
MMC3_CHR_H1	= %00000001	; Банк $0800-0FFF, 2Кб в CHR0
MMC3_CHR_Q0	= %00000010	; Банк $1000-13FF, 1Кб в CHR1
MMC3_CHR_Q1	= %00000011	; Банк $1400-17FF, 1Кб в CHR1
MMC3_CHR_Q2	= %00000100	; Банк $1800-1BFF, 1Кб в CHR1
MMC3_CHR_Q3	= %00000101	; Банк $1C00-1FFF, 1Кб в CHR1
MMC3_PRG_H0	= %00000110	; Банк $8000-9FFF в RAM (8Кб) от 0 до 63
MMC3_PRG_H1	= %00000111	; Банк $A000-BFFF в RAM (8Кб) от 0 до 63
; Флаг альтернативной раскладки банка PRG_H1. Если взведён, то PRG_H1 находится
; по адресам $C000-DFFF, а на $A000-BFFF маппится предпоследняя страница PRG ROM.
MMC3_PRG_ALT_MODE	= %01000000
; Флаг альтернативной раскладки банков CHR. Если взведён, то CHR_Hx находятся
; в CHR1, а CHR_Qx - в CHR0, т.е. CHR0 состоит из 4-х страниц, а CHR1 - из двух.
MMC3_CHR_ALT_MODE	= %10000000

; *** MMC3_PAGE - выбор страницы отображаемой в выбранном банке (запись)
; Если записать в MMC3_PAGE байт, то банк выбранный в MMC3_BANK начнёт отображаться
; на соответствующую по номеру страницу ROM PRG или ROM CHR картриджа.
; Для банков MMC3_CHR1_Qx диапазон значений страниц 0-255, т.е. полный объём CHR ROM - 256Кб
; Для банков MMC3_CHR0_Hx диапазон значений страниц 0-254, только чётные значения, т.е. берутся
; те же блоки что и в CHR1_Qx просто выбираются два подряд идущих блока
; Для банков MMC3_PRG_* диапазон значений страниц 0-63, т.е. полный объём PRG ROM - 512Кб
MMC3_PAGE		= $8001

; mmc3_set_bank_page - выставить для банка pbank маппинг на страницу ppage
.macro mmc3_set_bank_page pbank, ppage
	store MMC3_BANK, pbank	; Выставим в MMC3_BANK номер банка
	store MMC3_PAGE, ppage	; Выставим в MMC3_PAGE номер страницы для этого банка
.endmacro

; *** MMC3_MIRROR - режим зеркалирования видеостраниц (запись)
MMC3_MIRROR		= $A000
; нижний бит выбирает вариант зеркалирования:
MMC3_MIRROR_V		= 0	; зеркалирование по вертикали
MMC3_MIRROR_H		= 1	; зеркалирование по горизонтали

; *** MMC3_RAM_PROTECT - режим защиты (S)RAM на картридже отображаемой 
; на адреса процессора $6000-7FFF (8Кб) (запись).
MMC3_RAM_PROTECT	= $A001
MMC3_RAM_ENABLED	= %10000000	; (S)RAM включена
MMC3_RAM_PROTECTED	= %01000000	; (S)RAM защищена от записи

; *** MMC3_IRQ_COUNTER - начальное значение для счётчика сканлайнов (запись).
; При записи в этот порт байта значение будет запомнено во внутреннем регистре маппера, но
; в сам счётчик оно попадёт либо когда он достигнет нуля либо в процессе RELOAD (см. ниже)
MMC3_IRQ_COUNTER	= $C000

; *** MMC3_IRQ_RELOAD - при записи любого байта в этот порт будет взведён бит перезагрузки
; и на следующем сканлайне счётчик будет перезаписан начальным значением.
MMC3_IRQ_RELOAD		= $C001

; *** MMC3_IRQ_OFF - запись любого значения в этот порт отключит генерацию прерываний (IRQ), но
; счётчик сканлайнов будет в остальном работать как ни в чём не бывало.
MMC3_IRQ_OFF		= $E000

; *** MMC3_IRQ_ON - запись любого значния в этот порт разрешит генерацию прерываний от маппера.
MMC3_IRQ_ON		= $E001

.endif		; MMC3_INC_GUARD
and somethere code for it:

Code: Select all

; irq - процедура обработки прерывания IRQ.
; Она вызывается при наступлении прерывания от MMC3, т.е. по счётчику строк.
; Сперва в теле основного цикла в VBlank мы настроим отображение страниц на начало
; данных битмапа в тайлсете.
; Далее при выводе кадра начнут отсчитываться сканлайны и данная процедура вызовется
; 3 раза - и в ней мы будем сменять отображение страниц в данные тайлов (CHR) и
; тем самым будем менять тайлсет на лету чтобы на экране сформировалась большая
; картинка из уникальных пикселей.
; На входе в arg0b хранится номер банка видеоданных на который надо переключиться.
.proc irq
	pha		; сохраняем аккумулятор в стек
	txa		; помещаем X в A
	pha		; сохраняем снова A (т.е. X) в стек
	; выключаем прерывание MMC3 и одновременно этим сбрасываем флаг
	; наступившего прерывания, иначе прерывание будет генерироваться
	; каждый сканлайн!
	sta MMC3_IRQ_OFF
	; при данной конфигурации видео (из какой половины CHR берутся данные
	; для фона, а из какой - для спрайтов) прерывание срабатывает в самом конце
	; строки в её периоде HBlank и достаточно поздно чтобы мы в этот HBlank уже
	; могли обновлять параметры видео без видимых глюков. 
	; поэтому нам придётся подождать следующего HBlank искуственной паузой
	ldx # 15	; меняя количество холостых циклов на 10 или 20 вы
loop:	dex		; можете увидеть как с разных концов экрана будут
	bne loop	; появляться глюки
	; сменим банк тайловых данных в первой половине CHR на новый:
	mmc3_set_bank_page # MMC3_CHR_H0, arg0b
	; с этой точки точные тайминги уже не критичны, т.к. видеочип
	; будет занят отрисовкой уже настроенной первой половины CHR
	inc arg0b	; увеличим текущий банк на два, т.к. мы 
	inc arg0b	; работаем в режиме половинок, а не четвертей.
	; выставим банк в CHR_H1:
	mmc3_set_bank_page # MMC3_CHR_H1, arg0b
	inc arg0b	; и опять увеличим текущий банк графики
	inc arg0b	; на две четверти вперёд
	; Следущее прерывание должно сработать через 64 строки далее, но т.к.
	; мы ждали пропуска строки, то надо загрузить в счётчик на 1 меньше - 63
	store MMC3_IRQ_COUNTER, # 63
	sta MMC3_IRQ_ON	; включим прерывания MMC3
	
	pla		; восстановим A из стека
	tax		; и скопируем в X, т.к. это был он
	pla		; а теперь восстановим A
	rti		; Инструкция возврата из прерывания
.endproc
And we see pixelmap with every pixel unique:
Image

Timing is VERY important.

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Thu Jun 18, 2020 9:16 pm

I guess it's the combination of setting the scroll position and the bankswitch that's causing it. I do see that my last $2005/$2006 write is happening outside HBLANK. I guess I'll have to burn a scanline.

Maybe I can turn off rendering and update the palette during the first HBLANK, then swap the bank while rendering is off on the following scanline, then set the scroll position and turn rendering back on?

lidnariq
Posts: 9491
Joined: Sun Apr 13, 2008 11:12 am
Location: Seattle

Re: Seamless bankswitch on MMC3?

Post by lidnariq » Thu Jun 18, 2020 9:45 pm

rox_midge wrote:
Thu Jun 18, 2020 9:16 pm
I guess it's the combination of setting the scroll position and the bankswitch that's causing it. I do see that my last $2005/$2006 write is happening outside HBLANK. I guess I'll have to burn a scanline.
Yeah, that's your problem. As we've recently covered, there's only just barely enough time to write a very narrow specific subset of a 6/5/5/6 scroll set, so adding a MMC3 bankswitch on top is probably just a little too much.

PPU starts fetching background tiles at pixel 320, well before hblanking appears to stop. During the active portion of the scanline, you have 9 to 16 pixels of latency from when you do change the data that the PPU fetches and when you can see it.
Maybe I can turn off rendering and update the palette during the first HBLANK, then swap the bank while rendering is off on the following scanline, then set the scroll position and turn rendering back on?
Make sure that you're very careful about either keeping sprites from intersecting the split, or else you're very careful about when you turn off rendering, lest you tickle this hardware bug

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Fri Jun 19, 2020 4:13 pm

lidnariq wrote:
Thu Jun 18, 2020 9:45 pm
Yeah, that's your problem. As we've recently covered, there's only just barely enough time to write a very narrow specific subset of a 6/5/5/6 scroll set, so adding a MMC3 bankswitch on top is probably just a little too much.
When scrolling, only the last writes to 5/6 need to happen in HBLANK, right? That seems to work correctly for all of my other splits, although since we're manipulating a hidden state I can't do anything else with $2006 between the 6/5 writes and the 5/6 writes (such as updating the palette).
lidnariq wrote:
Thu Jun 18, 2020 9:45 pm
PPU starts fetching background tiles at pixel 320, well before hblanking appears to stop. During the active portion of the scanline, you have 9 to 16 pixels of latency from when you do change the data that the PPU fetches and when you can see it.
Ok. So if I disable rendering around dot 246 (9 pixels from the end of the visible area), then I should have until dot 321 to update the palettes, which gives me 75 dots, or 25 cycles. That means I'd need to set the PPU address, which takes 8 cycles assuming I already have #$3F in A and #$00 in X, and then I need to make four writes so that I can get PPUADDR back to the background color so that the following line will render as black. Those four writes will take 16 cycles, bringing me to 24 cycles total. So I can only have three colors, and two of them have to be black and grey. I might be able to live with that.

I should be able to do:

Code: Select all

lda #$00
ldx #$3f
ldy #$13 ; or whatever other color I want

; Spin to dot 246 or so

; Disable rendering
sta $2001

; Set PPUADDR
stx $2006
sta $2006

; Update the palettes - I'm assuming PPUCTRL is in horizontal increment mode
stx $2007 ; universal background color: black
stx $2007 ; index 1: also black
sta $2007 ; index 2: grey
sty $2007 ; index 3: purple (or some other splash color)

; Now we should be on dot 321 or so, and we can't touch the PPU again
; calculate scroll addresses and spin until the next scanline is about to end

st_ $2006 ;
st_ $2005 ;
st_ $2005 ; Update scroll position
st_ $2006 ; 

st_ $8000 ;
st_ $8001 ; Select a new CHR bank

st_ $2001 ; Enable rendering
lidnariq wrote:
Thu Jun 18, 2020 9:45 pm
Make sure that you're very careful about either keeping sprites from intersecting the split, or else you're very careful about when you turn off rendering, lest you tickle this hardware bug
That... is a considerably dense technical analysis that I don't currently have the headspace to fully understand. I will come back to it when I can set aside enough time to understand it, though.

lidnariq
Posts: 9491
Joined: Sun Apr 13, 2008 11:12 am
Location: Seattle

Re: Seamless bankswitch on MMC3?

Post by lidnariq » Fri Jun 19, 2020 4:41 pm

rox_midge wrote:
Fri Jun 19, 2020 4:13 pm
When scrolling, only the last writes to 5/6 need to happen in HBLANK, right? That seems to work correctly for all of my other splits, although since we're manipulating a hidden state I can't do anything else with $2006 between the 6/5 writes and the 5/6 writes (such as updating the palette).
Right. The problem tokumaru had was specifically wanting to not schedule the IRQ before the split, but instead to achieve the split immediately after the IRQ. Since there's only 64 pixels during hblanking for raster effects, and the IRQ is signaled 4 pixels late, and the IRQ entry takes another 7cy=21px, it leaves very little time to do the write during that same IRQ.

Once you're willing to accept a whole scanline of preparation before the write, timing becomes easier; you can write to $8000, the first two writes to 6/5, and preload all three registers for the remaining writes to 5/6 and $8001 and comfortably fit all the three relevant writes in 1+4+4=9cy=27px with tons of room for jitter.
Ok. So if I disable rendering around dot 246 (9 pixels from the end of the visible area), then I should have until dot 321 to update the palettes, which gives me 75 dots, or 25 cycles. That means I'd need to set the PPU address, which takes 8 cycles assuming I already have #$3F in A and #$00 in X, and then I need to make four writes so that I can get PPUADDR back to the background color so that the following line will render as black. Those four writes will take 16 cycles, bringing me to 24 cycles total. So I can only have three colors, and two of them have to be black and grey. I might be able to live with that.
Yes, setting three palette entries during hblanking should be achievable if the following scanline can be off.

Do note that getting cycle-level synchronization is doable but requires disproportionate effort.

A few specific details that both help and hurt:
ldx #$3f
[...]
; Spin to dot 246 or so
[...]
; Set PPUADDR
stx $2006
Just like with scrolling, you can schedule the first write to $2006 before hblanking starts or you've disabled rendering.
stx $2007 ; universal background color: black
Any color entry you want to skip over instead of changing can be instead read, by using BIT $2007 or the NOP abs instruction ($0C). It'll take the same amount of time, but frees up a register.
sty $2007 ; index 3: purple (or some other splash color)
Two gotchas here:
1- The color after is the one that will be displayed on the following scanline
2- While rendering is off, if PPUADDR holds $3Fx4 / $3Fx8 / $3FxC, that's not the same value as what's held in $3F00.
That... is a considerably dense technical analysis that I don't currently have the headspace to fully understand.
The earlier version of the warning, from before we figured out exactly what was going wrong, is summarized on our wiki:
Turning rendering off in PPUMASK ($2001) before the PPU has finished evaluating sprites for that line (x=192 for lines with no sprites, x=240 for lines with at least one sprite) can corrupt OAM, leading to sprite flicker.

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Fri Jun 19, 2020 5:27 pm

lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Right. The problem tokumaru had was specifically wanting to not schedule the IRQ before the split, but instead to achieve the split immediately after the IRQ. Since there's only 64 pixels during hblanking for raster effects, and the IRQ is signaled 4 pixels late, and the IRQ entry takes another 7cy=21px, it leaves very little time to do the write during that same IRQ.
Yeah, I've already accepted that I'm going to have to burn a scanline each time I scroll, because I'm not always doing the same thing each time, and there's not enough time to trampoline from the NMI vector (which is unfortunately in ROM) to the right routine and still have enough time to scroll. The way I'm structuring the engine, I'm limited to 16 splits in total, only three of which will be used for the majority of the game. I'll have to burn two when I get to the status bar, so that means I'll typically waste 4 scanlines and a maximum of 17. Not great, not terrible.
lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Do note that getting cycle-level synchronization is doable but requires disproportionate effort.
I don't think I need cycle-level synchronization here - I think I've got enough time to have at least 1-2 cycles (3-6 dots) of slop. It just needs to be "close enough".
lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Just like with scrolling, you can schedule the first write to $2006 before hblanking starts or you've disabled rendering.
Good to know - that might actually allow me enough time to guarantee PPUCTRL is set to horizontal increment.
lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Any color entry you want to skip over instead of changing can be instead read, by using BIT $2007 or the NOP abs instruction ($0C). It'll take the same amount of time, but frees up a register.
Also good to know. I might in that case share the status bar palette with the player's avatar and use their colors as part of the status bar. That'll allow me to get a bit more mileage out of it (although it's a bit serendipitous that I need to use a black color and I need to have #$3f loaded, and #$3f happens to be a black).
lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Two gotchas here:
1- The color after is the one that will be displayed on the following scanline
2- While rendering is off, if PPUADDR holds $3Fx4 / $3Fx8 / $3FxC, that's not the same value as what's held in $3F00.
Oh, right, it's not mirrored. But that might actually be OK, then; I just need to leave the third palette entry alone and make sure I write the black color into $3f04, yeah?

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Fri Jun 19, 2020 7:34 pm

lidnariq wrote:
Fri Jun 19, 2020 4:41 pm
Two gotchas here:
1- The color after is the one that will be displayed on the following scanline
2- While rendering is off, if PPUADDR holds $3Fx4 / $3Fx8 / $3FxC, that's not the same value as what's held in $3F00.
I confused myself, but it makes a little more sense now. You were saying that it's not the color I just wrote, but the color that comes after that (PPU $3f04). I was already comfortable with the idea that writing to $2007 increments PPUDATA, so in my head I was already thinking of the "current color" as $3f04. When you said it's the color after that, I figured there was some sort of weird thing that the PPU does that increments PPUDATA and uses that as the background color.

And I was half-wrong about the background color not being mirrored. According to Mesen, anyway, $3f00 is mirrored to $3f_0, and $3f04 is mirrored to $3f_4, and the same is true for $3f08 and $3f0c. Basically, you can store colors in $3f04, $3f08, and $3f0c, but by default they won't actually be rendered.

In my case, this is actually a glorious thing - I can change the "global background color" to whatever I want, and still have a solid black bar (or whatever other color) separating the status bar from the playfield. I've tested this in Mesen and it actually looks really good:

Image

I'm still getting a bit of snow on the edges because my timing isn't quite right (especially when the mid-playfield split occurs on the line before the status bar), and I've just noticed that my mid-playfield split is off by a line, but it's way better than it was yesterday.

rox_midge
Posts: 84
Joined: Mon Sep 19, 2005 11:51 am

Re: Seamless bankswitch on MMC3?

Post by rox_midge » Fri Jun 19, 2020 8:13 pm

Ok! It seems to be working now without any snow. I had to move my CHR bank select to before the scroll and unroll my delay into a series of nops so that I could jump into the middle of it when splitting the playfield on the scanline before the status bar, but it works.

Now I'm going to commit it and hopefully never have to touch it ever again :D

Post Reply