The Rockwell 6502 manual around page 151 shows the read-modify-write cycles:
Code:
External Internal
Cycles Address Bus Data Bus Operation Operation
1 100 OP CODE Fetch Finish Previous
OP CODE Operation, Incre-
ment PC to 101
2 101 ADL Fetch ADL Decode Current In-
struction, Increment
PC to 102
3 102 ADH Fetch ADH Add ADL + X, Incre-
ment PC to 103
4 ADH, ADL + X ? False Read Add Carry from
Previous Add to ADH
5 ADH + C, Data Fetch Value
ADL + X
6 ADH + C, ? Destroy Perform Rotate,
ADL + X Memory Turn on Write
7 ADH + C, Shifted Store Set Flags
ADL + X Data Results
8 103 OP CODE Fetch Next Increment PC to 104
OP CODE
From this I get that the first data read is ignored and from the un-page-crossed address, the second is used. In the first example I posted, the dummy read would fetch whatever was in the PPU's read buffer and fill it with $12, the second read would get $12 from the buffer and fill it with $34, then the first write would modify VRAM at address $0002, and the second at $0003, writing the value $24 ($12 << 1). Apparently the dummy write is the low byte of the PPU's VADDR:
Code:
jsr clear_vram
lda #$00 ; vaddr = 0
sta $2006
sta $2006
ldx #$00
dec $2007,x ; garbage write is to $0002
jsr print_vram ; prints $00 $00 $02 $FF $00 $00 $00 $00
jsr clear_vram
lda #$00 ; vaddr = 3
sta $2006
lda #$03
sta $2006
ldx #$00
dec $2007,x ; garbage write is to $0005
jsr print_vram ; prints $00 $00 $00 $00 $00 $05 $FF $00
jmp forever
clear_vram:
lda #$00 ; clear vram
sta $2006
sta $2006
ldy #0
lda #$00
: sta $2007
iny
bne -
rts
print_vram:
lda #$00
sta $2006 ; vaddr = 0
sta $2006
lda $2007 ; fill buffer
ldy #8
: lda $2007
jsr print_a
dey
bne -
jsr print_newline
rts
Maybe the CPU leaves the bus open during the write, causing the PPU to see a phantom of the low byte of its own address bus.
Now for even more weirdness that makes me wonder whether my code is causing it. Running the above test, but using inc $2007,x instead of dec, and running it multiple times with the starting VADDR at $0000, then again at $0001, etc., VRAM contents afterwards is really screwed up:
Code:
ldy #0
: jsr clear_vram2
lda #$00 ; vaddr = y
sta $2006
sty $2006
ldx #$00
inc $2007,x
jsr print_vram
iny
cpy #12
bne -
$01 $00 $02 $01 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00
$00 $00 $02 $03 $02 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00
$00 $01 $00 $00 $04 $01 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00
$00 $01 $00 $00 $00 $05 $01 $00 $00 $00 $00 $00 $00 $00 $00 $00
$01 $00 $00 $00 $00 $00 $06 $01 $00 $00 $00 $00 $00 $00 $00 $00
$00 $00 $02 $00 $00 $00 $00 $07 $02 $00 $00 $00 $00 $00 $00 $00
$00 $01 $00 $00 $00 $00 $00 $00 $08 $01 $00 $00 $00 $00 $00 $00
$00 $01 $00 $00 $00 $00 $00 $00 $00 $09 $01 $00 $00 $00 $00 $00
$01 $00 $00 $00 $00 $00 $00 $00 $00 $00 $0A $01 $00 $00 $00 $00
$00 $00 $02 $00 $00 $00 $00 $00 $00 $00 $00 $0B $02 $00 $00 $00
$00 $01 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $0C $01 $00 $00
$00 $01 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $00 $0D $01 $00
If I clear VRAM to $11 instead, I get this:
Code:
$11 $11 $02 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $03 $12 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $11 $04 $13 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $11 $11 $05 $12 $11 $11 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $11 $11 $11 $06 $11 $11 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $11 $11 $11 $11 $07 $12 $11 $11 $11 $11 $11 $11 $11
$11 $11 $11 $11 $11 $11 $11 $11 $08 $11 $11 $11 $11 $11 $11 $11
$11 $02 $11 $11 $11 $11 $11 $11 $11 $09 $02 $11 $11 $11 $11 $11
$01 $11 $11 $11 $11 $11 $11 $11 $11 $11 $0A $01 $11 $11 $11 $11
$11 $11 $02 $11 $11 $11 $11 $11 $11 $11 $11 $0B $02 $11 $11 $11
$11 $03 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $0C $03 $11 $11
$11 $02 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $11 $0D $02 $11
I have no idea how deep this rabbit hole goes. How the hell can it be modifying VRAM outside the current address?!? I think I'll take the blue pill and get some sleep, at least for now.