PPU Garbage Graphics Problem

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
SleepySheep
Posts: 9
Joined: Thu Jan 26, 2023 5:28 pm

PPU Garbage Graphics Problem

Post by SleepySheep »

Greetings, forum.

I am a humble sheep working on my first NES project. I am starting with the Nerdy Nights pong tutorial, which I am sure some of you are familiar with. In taking my pong game a few steps beyond the tutorial, I have run into a PPU-garbage-graphics issue where some of my background tiles are getting overwritten with garbage. I cannot seem to properly solve after a few days effort, so I'm politely requesting some help.

When I want to draw some background tiles in my game logic, I set a flag for the set of tiles I want to draw (ex. line 420). Then, at the beginning of every NMI interrupt (line 161), I call a subroutine called DrawTiles (line 1240). In the subroutine, I check the drawing flags, and depending on which flags are set, I pull specific arrays of BG tile data from ROM and write them to $2007. I do this for menu text, instructive text, and floor tiles used to clear said text. At the start of this routine I disable rendering, and at the end of it, I re-enable rendering, and write $00 twice to $2005 so I don't get unwanted scrolling.

*Most* of the time, this works great. But every so often, when a player scores a point in 2-player mode, and the round resets (which sets a flag to begin blinking instructive text on the screen), my BG has some garbage written to it. It also happens sometimes when I start up the game (using Mesen) but very rarely.

I have tried debugging through Mesen, setting breakpoints, and using the Event viewer, but I cannot quite seem to capture when and why this is happening. I measures the CPU cycles from the start of the NMI to the end of DrawTiles during the heaviest tile drawing it will do, and came out to 1579 cyles, so I feel like I should still be within VBlank, yet I'm almost certain my issues is that I'm sending data to the PPU after rendering has started. I just don't know when or why!

All in all, a minor bug that only occasionally inconveniences the game, but before I can make something bigger and better, I really want to completely understand graphics timing. Thanks for taking the time to read. Any help in solving this issue is greatly appreciated!

PS Please don't bully me for my bad code, I'm just a dumb sheep and can't be held accountable. That said, I'm open to any advice on good coding practices for 6502 ASM, and games in general. Thanks again!

Code: Select all

;MyPong
  .inesprg 1   ; 1x 16KB PRG code. Bank 0 and 1: CPU $8000-$BFFF
  .ineschr 1   ; 1x  8KB CHR data. Bank 2: PPU $0000
  .inesmap 0   ; mapper 0 = NROM, no bank swapping
  .inesmir 1   ; background mirroring

;;;;Game Constants;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
SCOREDIGIT1WIN2P		= $05			;In 2P or VSAI, First player to 15 wins
SCOREDIGIT2WIN2P 		= $01
SCOREDIGIT1WIN1P		= $09			;In 1P, let player play to 99 Points
SCOREDIGIT2WIN1P 		= $09
LEFTGOAL 				= $08			;Screen boundary for ball
RIGHTGOAL 				= $F6
TOPWALL 				= $0C
BOTTOMWALL				= $D8
BOTTOMWALL_PLAYER 		= $C8			;Screen boundary for Player
TOPWALL_PLAYER 			= $08
BALL_X_DIR_LEFT 		= $01
BALL_X_DIR_RIGHT 		= $00
BALL_Y_DIR_DOWN 		= $00
BALL_Y_DIR_UP 			= $01
BALL_INITSPEED_X		= $02
BALL_INITSPEED_Y		= $00
P1_X 					= $10
P2_X					= $E8
LASTSCORED_P1 			= $00
LASTSCORED_P2 			= $01
GAMEMODE_2Player 		= $01
GAMEMODE_1Player 		= $00
GAMEMODE_VSAI			= $02
PLAYERSPEED_SLOW 		= $03
PLAYERSPEED_FAST 		= $05
GAMESTATE_TITLE 		= $00
GAMESTATE_PLAYING 		= $01
GAMESTATE_GAMEOVER 		= $03
GAMESTATE_PAUSED		= $04
PLAYSTATE_SERVING 			= $00
PLAYSTATE_ROUNDINPROGRESS 	= $01
TEXTTIMER 			= $44 
TEXTTIMERHALF 		= $22				
INPUTDELAYTIMER		= $1E
BEEPLENGTH			= $14				
MAXSPEEDLEVEL		= $03

;;;;Variables set in ram;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  .rsset $0000 ;Start variables here
pointerLo 			.rs 1
pointerHi 			.rs 1
counterLo 			.rs 1
counterHi 			.rs 1
debug 				.rs 1 		;0 = off, 1 = on
gamestate 			.rs 1		;Major states get their own INIT sub
gamesubstate 		.rs 1		;minor states do not
changeStateRequest 	.rs 1
gamemode			.rs 1		;1 player, 2 player, or VS
buttons1 			.rs 1 		;Reserve 1 byte for controller 1 buttons
buttons2 			.rs 1
buttons1OldState 	.rs 1
buttons2OldState 	.rs 1
delayInputTimer  	.rs 1
player1_y 			.rs 1
player2_y 			.rs 1
player1_speed 		.rs 1
player2_speed 		.rs 1
ball_x_subPx		.rs 1
ball_x 				.rs 1
ball_y_subPx		.rs 1
ball_y 				.rs 1
ball_x_speedSubPx	.rs 1
ball_x_speed 		.rs 1
ball_y_speedSubPx	.rs 1
ball_y_speed 		.rs 1
ball_x_dir 			.rs 1
ball_y_dir 			.rs 1
scoreP1Digit1 		.rs	1		;Player1 score
scoreP1Digit2 		.rs 1
scoreP2Digit1 		.rs	1		;Player2 score
scoreP2Digit2 		.rs 1
lastScored 			.rs 1 		;Set depending on who scores last, which determines who serves.
textBlinkTimer 		.rs 1
arg0 				.rs 1		;Generic variables to use as pointers, parameters, and scratch
arg1 				.rs 1		;TODO: Replace pointerLo/Hi and counters
arg2 				.rs 1
arg3 				.rs 1
arg4 				.rs 1
arg5 				.rs 1
arg6 				.rs 1
arg7 				.rs 1
arg8 				.rs 1
arg9 				.rs 1
scrollLow 			.rs 1
scrollHigh 			.rs 1
winScoreD1			.rs 1
winScoreD2			.rs 1
inputDelayTimer		.rs 1
beepTimer			.rs 1
beepTone			.rs 1
beepToneIndex		.rs 1
coverMenuTilesFlag  .rs 1
blinkTextFlag		.rs 1
drawMenuTileFlag	.rs 1
hitCount			.rs 1		;Count of how many times ball was hit for X speed index
speedLevel			.rs 1
serveSpeed			.rs 1
  ;.rsset $0400		;Array Buffers
  
;;;;Initialization;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  .bank 0 
  .org $9000 ;$8000-$FFFF Program ROM
RESET:
  SEI          				; disable IRQs. all interrupts except the NMI are inhibited.
  CLD          				; disable decimal mode
  LDX #$40
  STX $4017    				; disable APU frame IRQ
  LDX #$FF
  TXS          				; Set up stack
  INX          				; now X = 0
  STX $2000    				; disable NMI
  STX $2001    				; disable rendering
  STX $4010    				; disable DMC IRQs
  JSR vblankwait 			; First wait for vblank to make sure PPU is ready

clrmem: 		;Zero out variable and sprite RAM space
  LDA #$00
  STA $0000, x
  STA $0100, x
  STA $0300, x
  STA $0400, x
  STA $0500, x
  STA $0600, x
  STA $0700, x
  LDA #$FE
  STA $0200, x
  INX
  BNE clrmem
   
  JSR vblankwait    		; Second wait for vblank, PPU is ready after this
  JSR LoadAssets 			; Load palettes, sprites, tile data, etc from ROM into RAM
  LDA #$00					;Set debug here to 1 to turn on some features
  STA debug
  LDA #$01
  STA changeStateRequest	;We're going to call a state change immediately to initialize the first state, the title menu. Set this flag to 1
  JSR InitSound
  LDA #%10000000   			; enable NMI, sprites from Pattern Table 0
  STA $2000
  LDA #%10011000   			; enable sprites + Background
  STA $2001
  
Forever:
	JMP Forever	;Loop forever. Game logic happens on NMI interrupt
	
vblankwait: ;port $2002 tells us if PPU is in vblankstate, aka not drawing anything, and ready for data transfer       
  BIT $2002
  BPL vblankwait
  RTS
;;;;Handle Interrupts;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
NMI: 			;NMI is triggered by the PPU when it enters VBlankstate, ie a period of time between drawing each frame. All Graphical Updates, then Game Logic, happen in here. 
  LDA #$00		;Send the address of our sprite data, $0200, to PPU via 2 ports: 2003 and 4014. This begins transfer from RAM to PPU
  STA $2003  	;set the low byte (00) of the RAM address. See OAM register
  LDA #$02
  STA $4014  	;set the high byte (02) of the RAM address to start transfer See OAMDMA register
  JSR DrawTiles
  JSR ReadController
  JSR GameEngine
  RTI

;;;;Main Routine;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;**GameEngine**
;Main driver of game
;Called from NMI Interrupt
;**
GameEngine:
  JSR HandleBeep				;Handle sound here so we always end a note in time, regardless of our state
  JSR ChangeState_Sub
  LDA gamestate
  CMP #GAMESTATE_TITLE
  BNE .CheckPlayState
  JSR State_Title_Sub
  JMP .Exit_GameEngine
.CheckPlayState:
  CMP #GAMESTATE_PLAYING
  BNE .CheckGameOverState
  JSR State_MatchInProgress_Sub
  JMP .Exit_GameEngine
.CheckGameOverState:
  CMP #GAMESTATE_GAMEOVER
  BNE .CheckPausedState
  JSR StateGameOver_Sub
  JMP .Exit_GameEngine
.CheckPausedState
  CMP #GAMESTATE_PAUSED
  BNE .Exit_GameEngine
  JSR CheckStart
.Exit_GameEngine:
  RTS
  
ChangeState_Sub:
  LDA changeStateRequest
  CMP #$00
  BEQ .Exit_Sub
  LDA gamestate
  CMP #GAMESTATE_TITLE
  BNE .CheckPlayState
  JSR ResetTitle_Sub
  JMP .Exit_Sub
.CheckPlayState
  CMP #GAMESTATE_PLAYING
  BNE .CheckGameOverState
  JSR InitMatch_Sub
  JSR ResetRound_Sub
  JMP .Exit_Sub
.CheckGameOverState
  CMP #GAMESTATE_GAMEOVER
  BNE .CheckPausedState
  JSR InitGameOver_Sub
  JMP .Exit_Sub
.CheckPausedState
  CMP #GAMESTATE_PAUSED
  BNE .Exit_Sub
  JSR InitPause_Sub
.Exit_Sub:
  LDA #$00
  STA changeStateRequest
  RTS

;;;;Game Logic;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ResetTitle_Sub:
  LDA #$50
  STA ball_x
  LDA #$50
  STA ball_y
  LDA #INPUTDELAYTIMER
  STA inputDelayTimer
  LDA #TEXTTIMER
  STA textBlinkTimer
  LDA #$01
  STA drawMenuTileFlag  ;Draw tiles
  RTS
  
State_Title_Sub:
  LDA inputDelayTimer	;Small delay to reading input on title screen
  CMP #$00
  BEQ .CheckInput
  SEC
  SBC #$01
  STA inputDelayTimer
  JMP .Exit_Sub
.CheckInput:
  LDA buttons1			;Check button-down on up and down keys to switch game mode 
  AND #%00000100		;Check 'Up'
  BEQ .CheckDown
  LDA buttons1OldState	;Check that old state was a non-press. Only want to cycle one option per press
  AND #%00000100
  BNE .CheckDown
  LDA gamemode			;Up pressed, mode++
  CLC
  ADC #$01
  CMP #$03				;There are three game modes: 0, 1, and 2. Reach 3 -> roll over
  BEQ .RollModeOver
  STA gamemode
  JMP .CheckStartGame
.RollModeOver:
  LDA #$00
  STA gamemode
  JMP .CheckStartGame
.CheckDown:
  LDA buttons1		
  AND #%00001000		;Check 'Down'
  BEQ .CheckStartGame
  LDA buttons1OldState
  AND #%00001000
  BNE .CheckStartGame
  LDA gamemode			;Down pressed, mode--
  SEC
  SBC #$01
  CMP #$FF
  BEQ .RollModeUnder
  STA gamemode
  JMP .CheckStartGame
.RollModeUnder:
  LDA #$02
  STA gamemode
.CheckStartGame:
  LDA buttons1		;If start pressed, go to play state
  AND #%00010000
  BEQ .Exit_Sub
  LDA #GAMESTATE_PLAYING	;Set our new gamestate, and set the flag changeStateRequest
  STA gamestate			
  LDA #$01
  STA changeStateRequest
.Exit_Sub:
  LDX gamemode
  LDA #$50
  CLC
  ADC menuSelect_position, x
  STA ball_y
  JSR SetBallSprite_Sub
  LDA #$01
  STA blinkTextFlag
  RTS
  
InitMatch_Sub:
  LDA #$80						;Set Ball x and y to 80 (center of screen)
  STA player1_y
  STA player2_y
  LDA #LASTSCORED_P1
  STA lastScored
  LDA #$30			;Reset Score sprites, hide 'W' sprite
  STA $0221
  LDA #$30
  STA $021D
  LDA #$59
  STA $022D
  LDA #$30
  STA $0229
  LDA #$30
  STA $0225
  LDA #$00
  STA scoreP1Digit1 ;Reset player score
  STA scoreP1Digit2
  STA scoreP2Digit1
  STA scoreP2Digit2
  LDA #$01
  STA coverMenuTilesFlag		;Draw over menu tiles at beginning of next NMI
  LDA gamemode
  CMP #GAMEMODE_1Player
  BNE .Set2P
  LDA #SCOREDIGIT1WIN1P		;For 1 player mode, set winning score to 99
  STA winScoreD1
  LDA #SCOREDIGIT2WIN1P
  STA winScoreD2
  LDA #$59					;Hide P2 with a blank tile
  STA $0211
  STA $0215
  STA $0219
  JMP .Exit_Sub
.Set2P
  LDA #SCOREDIGIT1WIN2P		;For 2 player mode, set winning score to 15
  STA winScoreD1
  LDA #SCOREDIGIT2WIN2P
  STA winScoreD2
  LDA #$00					;Make sure player 2 has visible tiles
  STA $0211
  LDA #$10
  STA $0215
  LDA #$20
  STA $0219
  
.Exit_Sub
  RTS


ResetRound_Sub:
  LDA #$00 						;Set initial ball speed
  STA ball_x_speed	
  STA ball_y_speed
  STA ball_x_subPx
  STA ball_y_subPx
  STA ball_x_speedSubPx
  STA ball_y_speedSubPx
  STA speedLevel
  STA hitCount
  LDA #PLAYSTATE_SERVING
  STA gamesubstate
  JSR StickBallToPlayer_Sub
  LDA lastScored
  CMP #LASTSCORED_P1			;This player-serving/ball direction set might be un-necessary
  BEQ .SetLeft
  LDA #BALL_X_DIR_RIGHT
  STA ball_x_dir 
  RTS
.SetLeft:
  LDA #BALL_X_DIR_LEFT
  STA ball_x_dir
  RTS
  
State_MatchInProgress_Sub:
  JSR CheckStart
  JSR HandlePlayer1_Sub	;Player Movement Logic for both substates
  LDA gamemode
  CMP #GAMEMODE_1Player
  BEQ .CheckGameSubstate		;If we are in 1-Player Mode, skip logic for second player
  JSR HandlePlayer2_Sub
.CheckGameSubstate:  
  LDA gamesubstate
  CMP #PLAYSTATE_SERVING		
  BNE .RoundInProgress			
  JSR Serving_Sub			;Play substate serving
  JMP .Exit_Sub
.RoundInProgress:				;Play substate RoundInProgress
  JSR LevelCheck
  JSR HandleBall_Sub			;Ball movement logic
  JSR HandleCollisions_Sub		;Handle Collision
  JSR CheckWinCondition_Sub			;Transition to GameOver state if a player wins
.Exit_Sub:
  JSR SetBallSprite_Sub		;Place sprites
  JSR SetPlayerSprite_Sub
  RTS
    
Serving_Sub:
  JSR StickBallToPlayer_Sub		;If serving player presses A, serve ball
  LDA lastScored
  CMP #LASTSCORED_P1
  BNE .LoadP2
  LDA buttons1
  JMP .CheckA
.LoadP2
  LDA buttons2
.CheckA
  STA arg0					;Store button state in scratch
  AND #%10000000
  BNE .CheckUp				;If A pressed, check for direction, the serve ball
  LDA #$01
  STA blinkTextFlag  		;Blink serve text
  RTS
.CheckUp
  LDA arg0
  AND #%00001000			;Check up
  BEQ .CheckDown
  LDA #BALL_Y_DIR_UP
  STA ball_y_dir
  LDA #$01
  STA serveSpeed
  JMP .CheckB
.CheckDown
  LDA arg0
  AND #%00000100			;Check down
  BEQ .Zero
  LDA #BALL_Y_DIR_DOWN
  STA ball_y_dir
  LDA #$02
  STA serveSpeed
.CheckB
 LDA arg0
 AND #%01000000				;If B is pressed while serving, ball will be extra fast!
 LDA #$03
 JMP .ServeBall
.Zero
  LDA #$00
.ServeBall					;Serve the ball! Start by setting initial values, and let next game substate take care of the rest
  STA serveSpeed
  LDA #BALL_INITSPEED_X 		
  STA ball_x_speed
  LDA #BALL_INITSPEED_Y
  CLC
  ADC serveSpeed
  STA ball_y_speed
  LDA #PLAYSTATE_ROUNDINPROGRESS
  STA gamesubstate			;Set game substate to game in progress
  JSR CoverServeText		;Hide serve text
  LDA #$00					;reset PPU Scroll
  STA $2005
  STA $2005
  RTS

;**Check how many hits we have, and INC speed level at thresholds
LevelCheck:
  LDA speedLevel
  CMP #MAXSPEEDLEVEL
  BEQ .Exit_Sub			;We've reached max level, so skip
  TAX
  LDA hitCountToLevel,x
  CMP hitCount
  BCS .Exit_Sub
.IncLevel
  LDA levelToXSpeed,x
  STA ball_x_speed
  INC speedLevel
.Exit_Sub
  RTS
  
;**HandleBall_Sub
; Ball Movement behavior.
; 1 byte for sub pixels, 1 byte for actual pixel.
HandleBall_Sub:
  LDA ball_x_dir			;Handle X movement
  CMP #BALL_X_DIR_RIGHT
  BNE .MoveBallLeft
.MoveBallRight
  LDA ball_x_subPx			
  CLC
  ADC ball_x_speedSubPx
  STA ball_x_subPx
  LDA ball_x
  ADC ball_x_speed
  JMP .SetBallX
.MoveBallLeft:		
  LDA ball_x_subPx
  SEC
  SBC ball_x_speedSubPx
  STA ball_x_subPx
  LDA ball_x
  SBC ball_x_speed
.SetBallX:
  STA ball_x
.HandleY
  LDA ball_y_dir		;Handle Y Movement
  CMP #BALL_Y_DIR_DOWN
  BNE .MoveBallUp
.MoveBallDown
  LDA ball_y_subPx
  CLC
  ADC ball_y_speedSubPx
  STA ball_y_subPx
  LDA ball_y		
  ADC ball_y_speed
  STA ball_y
  RTS
.MoveBallUp:
  LDA ball_y_subPx
  SEC
  SBC ball_y_speedSubPx
  STA ball_y_subPx  
  LDA ball_y
  SBC ball_y_speed
  STA ball_y
.Exit_Sub:
  RTS
  
;**StickBallToPlayer_Sub
;Set ball to center-front of paddle. Offset as necessary
;**
StickBallToPlayer_Sub:
  LDA lastScored
  CMP #LASTSCORED_P1
  BEQ .SetP1
  LDA player2_y
  CLC
  ADC #$08
  STA ball_y
  LDA #P2_X
  SEC
  SBC #$08
  JMP .Exit_Sub
.SetP1:
  LDA player1_y
  CLC
  ADC #$08
  STA ball_y
  LDA #P1_X
  CLC
  ADC #$04
.Exit_Sub
  STA ball_x
  RTS
  
HandlePlayer1_Sub:
.CheckB:			;Hold B to go faster
  LDA buttons1
  AND #%01000000
  BNE .SetBSpeed
  LDA #PLAYERSPEED_SLOW
  STA player1_speed
  JMP .ReadPlayer1Y
.SetBSpeed:
  LDA #PLAYERSPEED_FAST
  STA player1_speed
.ReadPlayer1Y: 		;Use up and down to move
  LDA buttons1
  AND #%00001000
  BNE .MoveP1Up
  LDA buttons1
  AND #%00000100
  BNE .MoveP1Down
  JMP .Exit_Sub
.MoveP1Down:			;Move player 1 down, check against bottom bounds and kick out if necessary
  LDA player1_y
  CLC
  ADC player1_speed
  STA player1_y
  CMP #BOTTOMWALL_PLAYER
  BCC .Exit_Sub
  LDA #BOTTOMWALL_PLAYER
  STA player1_y
  JMP .Exit_Sub
.MoveP1Up:			;Move player 1 up, check against top bounds and kick out if necessary
  LDA player1_y
  SEC
  SBC player1_speed
  STA player1_y
  SEC
  CMP #TOPWALL_PLAYER
  BCS .Exit_Sub
  LDA #TOPWALL_PLAYER
  STA player1_y
.Exit_Sub:
  RTS
  
HandlePlayer2_Sub:
.P2CheckB:			;Hold B to go faster
  LDA buttons2
  AND #%01000000
  BNE .P2SetBSpeed
  LDA #PLAYERSPEED_SLOW
  STA player2_speed
  JMP .ReadPlayer2Y
.P2SetBSpeed:
  LDA #PLAYERSPEED_FAST
  STA player2_speed
.ReadPlayer2Y: 		;Use up and down to move
  LDA buttons2
  AND #%00001000
  BNE .MoveP2Up
  LDA buttons2
  AND #%00000100
  BNE .MoveP2Down
  JMP .Exit_Sub
.MoveP2Down:			;Move player 2 down, check against bottom bounds and kick out if necessary
  LDA player2_y
  CLC
  ADC player2_speed
  STA player2_y
  CMP #BOTTOMWALL_PLAYER
  BCC .Exit_Sub
  LDA #BOTTOMWALL_PLAYER
  STA player2_y
  JMP .Exit_Sub
.MoveP2Up:					;Move player 2 up, check against top bounds and kick out if necessary
  LDA player2_y
  SEC
  SBC player2_speed
  STA player2_y
  SEC
  CMP #TOPWALL_PLAYER
  BCS .Exit_Sub
  LDA #TOPWALL_PLAYER
  STA player2_y
.Exit_Sub:
  RTS
  
HandleCollisions_Sub:
  LDA ball_y
  CLC
  ADC #$04				;Test against center of ball by adding 4 to ball x and y coords
  STA ball_y
  LDA ball_x
  CLC
  ADC #$04
  STA ball_x
  JSR Collisions_BallV_Sub
  JSR Collisions_BallH_Sub
  LDA ball_x		   ;remove 4 from ball x and y so that drawing coordinates are correct
  SEC
  SBC #$04
  STA ball_x
  LDA ball_y
  SEC
  SBC #$04
  STA ball_y
  RTS
  
Collisions_BallV_Sub:		;Check against top and bottom walls
  LDA ball_y
  CMP #TOPWALL
  BCS .CheckBottomWall
  JSR BounceY_Sub			;Collide with Top wall
  RTS
.CheckBottomWall:
  LDA ball_y
  CMP #BOTTOMWALL
  BCC .Exit_Sub
  JSR BounceY_Sub			;Collide with bottom wall
.Exit_Sub:
  RTS
  
Collisions_BallH_Sub:			;Check against left and right boundaries and player paddles
  LDA ball_x_dir
  CMP #BALL_X_DIR_RIGHT
  BNE .CheckLeftSide			
  JSR CheckCollisions_Right
  RTS
.CheckLeftSide
  JSR CheckCollisions_Left
.Exit_Sub:
  RTS
 
;**CheckCollisions_Left
;Test ball against left wall and P1
;**
CheckCollisions_Left:
  LDA ball_x
  CMP #LEFTGOAL
  BCS .CheckPlayer1				;BCS: true if A > M
.CheckLeftGoal
  LDA #LASTSCORED_P2			;If Ball x < left wall, ball has collided with goal
  STA lastScored
  JSR ToggleBallXDirection
  JSR HandleGoalCollision
  RTS
.CheckPlayer1:
  LDA player1_y					;Test againt four sides of P1 paddle.
  SEC							;y-4 to account for diameter of ball
  SBC #$04
  CMP ball_y
  BCS .Exit_Sub
  LDA player1_y
  CLC
  ADC #$1B						;$18+$04-1=$1B to compensate for diameter of ball, Would need extra powerIndex
  CMP ball_y
  BCC .Exit_Sub
  LDA #P1_X
  CMP ball_x
  BCS .Exit_Sub
  CLC
  ADC #$07
  CMP ball_x
  BCC .Exit_Sub		
  JSR Calculate_BallYSpeed  
  JSR ToggleBallXDirection			;Ball has hit paddle
  LDA #$40							;Load player 1 palette
  STA $020E							;Set ball palette
  INC hitCount
.Exit_Sub
  RTS
  
;**CheckCollisions_Right
;Test ball against Right wall and P2
;**
CheckCollisions_Right:
  LDA ball_x					;Ball moving Right, check against right goal/P2
  CMP #RIGHTGOAL
  BCC .CheckPlayer2
.CheckRightGoal  
  LDA #LASTSCORED_P1			;Ball x > right goal = collision. Award point to P1. 				
  STA lastScored
  JSR ToggleBallXDirection
  JSR HandleGoalCollision
  RTS
.CheckPlayer2:
  LDA gamemode
  CMP #GAMEMODE_1Player			;If we are in 1 player mode, skip player 2 check
  BEQ .Exit_Sub
  LDA player2_y					;Test againt four sides of P2 paddle. BCS = A > X. BCC = A < X
  SEC							
  SBC #$04
  CMP ball_y
  BCS .Exit_Sub
  LDA player2_y
  CLC
  ADC #$1B						;$18+$04-1=$1B		
  CMP ball_y
  BCC .Exit_Sub
  LDA #P2_X
  CLC
  ADC #$01
  CMP ball_x 
  BCS .Exit_Sub
  CLC
  ADC #$07
  CMP ball_x
  BCC .Exit_Sub
  JSR Calculate_BallYSpeed
  JSR ToggleBallXDirection
  LDA #$42							;Load player 2 palette
  STA $020E							;Set ball palette
.Exit_Sub
  RTS
  
ToggleBallXDirection:
  JSR StartBeep
  LDA ball_x_dir
  EOR #$01
  STA ball_x_dir
  RTS

BounceY_Sub:
  LDA ball_y_dir
  CMP #BALL_Y_DIR_DOWN
  BNE .Top
.Bottom
  JSR KickoutBall_Bottom
  JMP .bounce
.Top
  JSR KickoutBall_Top
.bounce
  JSR StartBeep
  JSR ToggleBallYDirection
  RTS
  
;**KickoutBall_Bottom
;TODO: make this more advanced.
;
KickoutBall_Bottom:
  LDA #BOTTOMWALL
  STA ball_y
  RTS
  
KickoutBall_Top:
  LDA #TOPWALL
  STA ball_y
  RTS
  
ToggleBallYDirection: 
  LDA ball_y_dir
  EOR #$01
  STA ball_y_dir
  RTS
  
;**Calculate_BallYSpeed
;Calculate new ball speed and direction of ball, dependant on the position it hits a paddle
;BUG WITH PADDLE COLLISION: When d = 1A, X = 6 (oob)
;Use arg0,1,2 for scratch
Calculate_BallYSpeed:
  LDA ball_x_dir				;We have just hit the ball with the paddle, but not yet changed ball x direction. Use direction to determine which paddle it hit
  CMP #BALL_X_DIR_RIGHT
  BNE .LeftPaddle
.RightPaddle
  LDA ball_y					;We also know ball_y > player_y
  SEC
  SBC player2_y
  JMP .GetSpeedFromTable
.LeftPaddle
  LDA ball_y
  SEC
  SBC player1_y
.GetSpeedFromTable						
  CLC
  ADC #$04								;Add 4 to account for collision
  STA arg0								;Store distance in arg0
  LSR A									;A=distance. A/4 = index. Used two LSRs and ignore remainder
  LSR A
  TAX
  LDA distanceToPower_3_Lo,x		;Store px and sub pix speed changes based on index
  STA arg1
  LDA distanceToPower_3_Hi,x
  STA arg2
.AddOrSubtract
  LDA ball_y_dir
  CMP #BALL_Y_DIR_UP
  BNE .Down
.Up										;When ball is moving up, top half of paddle increases y speed
  LDA arg0								;Check distance. if d < 12, we have hit top half (24+4+4)
  CMP #$0C
  BCS .Sub
  JMP .Add
.Down									;When ball is moving down, bottom half of paddile increases y speed
  LDA arg0
  CMP #$0C
  BCS .Add
  JMP .Sub
.Add
  LDA ball_y_speedSubPx
  CLC
  ADC arg1
  STA ball_y_speedSubPx
  LDA ball_y_speed
  ADC arg2
  STA ball_y_speed
  JMP .Max
.Sub
  LDA ball_y_speedSubPx
  SEC
  SBC arg1
  STA ball_y_speedSubPx
  LDA ball_y_speed
  SBC arg2
  STA ball_y_speed
  BCS .Max
  JSR TwosCompliment					  ;If BCC, top bit had to borrow, so we are negative, and have to make our speed positive
  JSR ToggleBallYDirection				;Since we have flipped signs, flip ball direction
.Max
  RTS
  
;**Performs 2's compliment on a Negative 16 bit number, giving us its absolute value
TwosCompliment:
  ;If top bit is subtracted and overflows, get absolute value of speed
  LDA ball_y_speed
  EOR #$FF					;N xor FF is same as performing 1's Compliment operation
  STA ball_y_speed
  LDA ball_y_speedSubPx
  EOR #$FF
  CLC
  ADC #$01
  STA ball_y_speedSubPx
  RTS
  
StartBeep:
  LDA #BEEPLENGTH
  STA beepTimer				;Start beep timer
.SetNextNote				
  LDA beepToneIndex			;Load current index
  asl a						;multiply index by two for word array
  tay
  LDA beepToneSeq, y		;Store low byte of period
  STA $4002
  LDA beepToneSeq+1, y		;Store high byte of period
  STA $4003
  LDA beepToneIndex
  CLC
  ADC #$01
  CMP #$10						;16 note sequence. Roll over index
  BNE .Exit_Sub
  LDA #$00
.Exit_Sub
  STA beepToneIndex
  RTS
  
HandleBeep:
  LDA beepTimer
  CMP #$00
  BEQ .Exit_Sub
  SEC
  SBC #$01
  STA beepTimer		;Subtract 1 from beepTimer every frame
  LDA #$00			;Turn off beep 000 turns off square?
  STA $4002
  LDA #$00
  STA $4003
.Exit_Sub
  RTS

HandleGoalCollision:				;Award point, and either reset board to serving mode, or reflect ball, depending on GAMEMODE
  LDA lastScored
  CMP #$00
  BNE .CheckP2
  JSR IncP1ScoreOnes
  LDA gamemode 					;Game mode = 1 player, reflect ball off right wall rather than reseting board
  CMP #GAMEMODE_1Player
  BNE .Exit_Sub
  RTS
.CheckP2:
  LDA gamemode 					;Game mode = 1 player, reflect ball off right wall rather than reseting board
  CMP #GAMEMODE_1Player
  BNE .AwardPointP2
  LDA #$00
  STA gamesubstate

  LDA #GAMESTATE_GAMEOVER
  STA gamestate
  LDA #$01
  STA changeStateRequest
.AwardPointP2
  JSR IncP2ScoreOnes
.Exit_Sub:
  JSR ResetRound_Sub
SKIP_STATECHANGE
  RTS
  
IncP1ScoreOnes:
  LDA scoreP1Digit1     	; load the lowest digit of the number
  CLC 
  ADC #$01          		; add one
  STA scoreP1Digit1
  CMP #$0A          		; check if it overflowed, now equals 10
  BNE IncP1ScoreDone       	; if there was no overflow, all done
IncP1ScoreTens:
  LDA #$00
  STA scoreP1Digit1     	; wrap digit to 0
  LDA scoreP1Digit2   		; load the next digit
  CLC 
  ADC #$01          		; add one, the carry from previous digit
  STA scoreP1Digit2
  CMP #$0A          		; check if it overflowed, now equals 10
  BNE IncP1ScoreDone       	; if there was no overflow, all done
  LDA #$00
  STA scoreP1Digit2     	; wrap digit to 0
IncP1ScoreDone:
  LDA scoreP1Digit1			; Set score sprite tile numbers
  CLC
  ADC #$30					;Offset tile number
  STA $021D					;Store sprite number
  LDA scoreP1Digit2
  CLC
  ADC #$30					;Offset tile number
  STA $0221					;Store sprite number
  RTS
  
IncP2ScoreOnes:
  LDA scoreP2Digit1     
  CLC 
  ADC #$01          
  STA scoreP2Digit1
  CMP #$0A          
  BNE IncP2ScoreDone       
IncP2ScoreTens:
  LDA #$00
  STA scoreP2Digit1    
  LDA scoreP2Digit2   
  CLC 
  ADC #$01          
  STA scoreP2Digit2
  CMP #$0A          
  BNE IncP2ScoreDone       
  LDA #$00
  STA scoreP2Digit2     
IncP2ScoreDone:
  LDA scoreP2Digit1
  CLC
  ADC #$30	;Offset tile number
  STA $0225	;Store sprite number
  LDA scoreP2Digit2
  CLC
  ADC #$30	;Offset tile number
  STA $0229	;Store sprite number
  RTS
  
CheckWinCondition_Sub:		;****TODO**** For one player, we'll want to allow player as many points as they can get until they are out
  LDA scoreP1Digit1
  CMP winScoreD1
  BNE CheckPlayer2Score
  LDA scoreP1Digit2
  CMP winScoreD2
  BNE CheckPlayer2Score
  JMP SetGameOverState
CheckPlayer2Score:
  LDA scoreP2Digit1
  CMP winScoreD1
  BNE End_CheckWinCondition_Sub
  LDA scoreP2Digit2
  CMP winScoreD2
  BNE End_CheckWinCondition_Sub
SetGameOverState:		;If either player reaches 15 (0E), move to game over state. 'lastScored' will determine what displays.
  LDA #$00
  STA gamesubstate
  LDA #GAMESTATE_GAMEOVER
  STA gamestate
  LDA #$01
  STA changeStateRequest
End_CheckWinCondition_Sub:
  RTS
  
StateGameOver_Sub:
  LDA buttons1		;If start pressed, go to play state
  AND #%00010000
  BEQ .Exit_Sub
  STA gamesubstate			;Set Substate PLAYSTATE_SERVING
  LDA #GAMESTATE_TITLE	;Set game state to Playing
  STA gamestate
  LDA #$01
  STA changeStateRequest
.Exit_Sub:
  RTS

InitGameOver_Sub:
  LDA #$80		;Set Ball x and y to 80 (center of screen)
  STA ball_x	;Variable values
  STA ball_y
  STA player1_y
  STA player2_y
  LDA #$00 		;Set initial ball speed
  STA ball_x_speed
  STA ball_y_speed
  STA player1_speed
  STA player2_speed
  LDA gamemode
  CMP #GAMEMODE_1Player
  BNE .Set2P
  LDA $0221				;For 1P mode, transfer player score to right side, and set left side to 'P1'
  STA $0229
  LDA $021D
  STA $0225
  LDA #$31
  STA $021D
  JMP .Exit_Sub
.Set2P:
  LDA #$31				;Display '1' or '2' depending on who won. Use score sprites as part of message. For 1Player mode, always display P1
  CLC
  ADC lastScored
  STA $021D
  LDA #$50		;W
  STA $022D
  LDA #$42		;I
  STA $0229
  LDA #$47		;N
  STA $0225
.Exit_Sub:
  LDA #$49		;Set P Sprite
  STA $0221
  RTS
  
;**TODO... merge this with PauseState behavior
CheckStart:
  LDA buttons1
  AND #%00010000
  BEQ .CheckP2
  LDA buttons1OldState
  AND #%00010000
  BNE .CheckP2
  JMP .Pause
.CheckP2
  LDA buttons2
  AND #%00010000
  BEQ .Exit_Sub
  LDA buttons2OldState
  AND #%00010000
  BNE .Exit_Sub
.Pause:
  LDA gamestate
  CMP #GAMESTATE_PAUSED
  BEQ .Unpause
  LDA #GAMESTATE_PAUSED
  STA gamestate
  LDA #$01
  STA changeStateRequest
  JMP .Exit_Sub
.Unpause
  LDA #GAMESTATE_PLAYING
  STA gamestate				;Change game state back to playing without setting stateChangeRequest, so we don't reinitialize values that are mid match
.Exit_Sub
  RTS
  
InitPause_Sub:
  ;Start a jingle here
  ;Start tile text drawing here
  RTS
  
;**BlinkPressStart_Sub
;;Subtract 1 from timer each from. At half time, we hide text. At 0, we show text again
;**
BlinkPressStart_Sub:
  LDA textBlinkTimer
  SEC
  SBC #$01
  STA textBlinkTimer
  CMP #TEXTTIMERHALF		
  BNE .CheckBlink
  LDA #$0F
  STA arg2
  LDA #$22
  STA arg0
  LDA #$48
  STA arg1
  JSR WriteFloorTiles_Upper	
  JMP .Exit_Sub
.CheckBlink
  LDA textBlinkTimer
  CMP #$00
  BNE .Exit_Sub
  LDA #LOW(textHeader_pressStart)
  STA arg0
  LDA #HIGH(textHeader_pressStart)
  STA arg1
  JSR WriteBGTiles
  LDA #TEXTTIMER
  STA textBlinkTimer
.Exit_Sub
  LDA #$00
  STA $2005
  STA $2005
  RTS
  
BlinkPressA_Sub:
  LDA textBlinkTimer
  SEC
  SBC #$01
  STA textBlinkTimer
  CMP #TEXTTIMERHALF					;Timer has reached half time. Overwrite text with floor tiles
  BNE .CheckBlink
  JSR CoverServeText
  JMP .Exit_Sub
.CheckBlink
  LDA textBlinkTimer
  CMP #$00
  BNE .Exit_Sub							;Timer has reached 0.
  LDA #TEXTTIMER						;Reset Timer
  STA textBlinkTimer
  LDA #LOW(textHeader_playerServe)		;Draw text
  STA arg0
  LDA #HIGH(textHeader_playerServe)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_pressA)
  STA arg0
  LDA #HIGH(textHeader_pressA)
  STA arg1
  JSR WriteBGTiles
  LDA lastScored
  CMP #LASTSCORED_P1
  BEQ .SetP1
  LDA #LOW(textHeader_player2)
  STA arg0
  LDA #HIGH(textHeader_player2)
  STA arg1
  JSR WriteBGTiles
  JMP .Exit_Sub
.SetP1
  LDA #LOW(textHeader_player1)
  STA arg0
  LDA #HIGH(textHeader_player1)
  STA arg1
  JSR WriteBGTiles
.Exit_Sub
  LDA #$00
  STA $2005
  STA $2005
  RTS
  
CoverServeText:
  LDA #$0E
  STA arg2
  LDA #$22
  STA arg0
  LDA #$48
  STA arg1
  JSR WriteFloorTiles_Upper
  LDA #$68
  STA arg1
  JSR WriteFloorTiles_Lower
  RTS

SetBallSprite_Sub:
  LDA ball_x
  STA $020F
  LDA ball_y
  STA $020C
.Exit_Sub:
  RTS
  
SetPlayerSprite_Sub:
  LDA player1_y
  STA $0200
  CLC
  ADC #$08
  STA $0204
  ADC #$08
  STA $0208
  LDA gamemode		;If game mode is 1 player, skip player 2 sprites POS logic
  CMP #GAMEMODE_1Player
  BEQ .Exit_Sub
  LDA player2_y
  STA $0210
  CLC
  ADC #$08
  STA $0214
  ADC #$08
  STA $0218
.Exit_Sub:
  RTS
 
ToggleGoalLineTiles_Sub:
  RTS

;;;;;Tiles Setting Stuff;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrawTiles:
  LDA #$00
  STA $2001					;Start by disabling rendering to avoid VRAM corruption. This is a lot of tiles to draw
  LDA drawMenuTileFlag
  CMP #$00
  BEQ .CoverMenu
  JSR DrawMenuTiles
  LDA #$00
  STA drawMenuTileFlag
  JMP .Exit_Sub				;Don't do 
.CoverMenu
  LDA coverMenuTilesFlag
  CMP #$00
  BEQ .BlinkTextFlag
  JSR CoverTitleMenuText_Sub
  LDA #$00
  STA coverMenuTilesFlag
  JMP .Exit_Sub				;Might remove this if I think I can do them on the same frame
.BlinkTextFlag
  LDA blinkTextFlag
  CMP #$00
  BEQ .Exit_Sub
  LDA #$00
  STA blinkTextFlag
  LDA gamestate
  CMP #GAMESTATE_PLAYING
  BEQ .BlinkA
.BlinkStart
  JSR BlinkPressStart_Sub
  JMP .Exit_Sub
.BlinkA
  JSR BlinkPressA_Sub
.Exit_Sub
  LDA #%10011000   			; enable sprites + Background
  STA $2001
  LDA #$00					;Reset PPU  Scroll
  STA $2005
  STA $2005
  RTS
  
DrawMenuTiles:
  LDA #LOW(textHeader_solo)
  STA arg0
  LDA #HIGH(textHeader_solo)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_2Player)
  STA arg0
  LDA #HIGH(textHeader_2Player)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_VSAI)
  STA arg0
  LDA #HIGH(textHeader_VSAI)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_pressStart)
  STA arg0
  LDA #HIGH(textHeader_pressStart)
  STA arg1
  JSR WriteBGTiles 
  LDA #LOW(textHeader_title1)
  STA arg0
  LDA #HIGH(textHeader_title1)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_title2)
  STA arg0
  LDA #HIGH(textHeader_title2)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_title3)
  STA arg0
  LDA #HIGH(textHeader_title3)
  STA arg1
  JSR WriteBGTiles
  LDA #LOW(textHeader_title4)
  STA arg0
  LDA #HIGH(textHeader_title4)
  STA arg1
  JSR WriteBGTiles
  RTS
  
;**CoverTitleMenuText_Sub:
; Draw floor tiles to over write menu text
;**
CoverTitleMenuText_Sub:
  LDA #$08					;Each draw will be 8 tiles long
  STA arg2
  LDA #$21
  STA arg0
  LDA #$4C
  STA arg1
  JSR WriteFloorTiles_Upper 
  LDA #$21
  STA arg0
  LDA #$8C
  STA arg1
  JSR WriteFloorTiles_Upper 
  LDA #$21
  STA arg0
  LDA #$CC
  STA arg1
  JSR WriteFloorTiles_Upper 
  LDA #$0F				;Last draw is 14 tiles long
  STA arg2
  LDA #$22
  STA arg0
  LDA #$48
  STA arg1
  JSR WriteFloorTiles_Upper
  RTS

;**WriteBGTiles
; arg0 Text pos object low byte
; arg1 text pos object high byte  
; arg2 used for scratch
;**
WriteBGTiles:
  LDA $2002
  LDY #$00
  LDA [arg0],y
  STA $2006
  INY
  LDA [arg0],y
  STA $2006
  INY
  LDA [arg0],y
  CLC
  ADC #$03			;Offset by size of header
  STA arg2
  INY
.LoadTilesLoop
  LDA [arg0], y
  STA $2007
  INY
  CPY arg2
  BNE .LoadTilesLoop
  RTS
  
;**WriteFloorTiles_Upper
; arg0 start address HIGH
; arg1 start address LOW
; arg2 length of bytes/number of tiles to draw
;**
WriteFloorTiles_Upper:
  LDA $2002
  LDA arg0
  STA $2006
  LDA arg1
  STA $2006
  LDX #$00
.LoadTilesLoop
  LDA text_coverUpper, x
  STA $2007
  INX
  CPX arg2
  BNE .LoadTilesLoop
  RTS
  
WriteFloorTiles_Lower:
  LDA $2002
  LDA arg0
  STA $2006
  LDA arg1
  STA $2006
  LDX #$00
.LoadTilesLoop
  LDA text_coverLower, x
  STA $2007
  INX
  CPX arg2
  BNE .LoadTilesLoop
  RTS
  

;**FillTileColumn
;arg0: Low byte of top tile in column. High byte always starts at $20
;arg1: tile ID
;arg0,2 used as scratch
;**FillTileColumn
;arg0: Low byte of top tile in column. High byte always starts at $20
;arg1: tile ID
;arg0,2 used as scratch
;Currently not functional. It seems this method, called from NMI, can fill about 24 tiles before corruption
FillTileColumn:
  LDA #$01
  STA arg0
  LDA #$20
  STA arg1
  LDX #$00
.loop
  LDA $2002
  LDA arg1
  STA $2006
  LDA arg0
  STA $2006
  LDA #$6A
  STA $2007
  LDA arg0
  CLC
  ADC #$20
  STA arg0
  LDA arg1
  ADC #$00
  STA arg1
  INX
  CPX #$02		;Do this for 30 tiles in column
  BNE .loop
.Exit_Sub
  RTS
;;;;Audio;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
InitSound:
  LDA #%00000001			;Enable square 1 of audio
  STA $4015  				;Write to Audio control
  LDA #%10111111; Duty 10 (50%), volume F (max!)
  STA $4000
  
  RTS
;;;;Asset Loading For Initialization Subroutines;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LoadAssets:
LoadPalettes: ; Send  palette data from PRG ROM to store @ PPU RAM $3F00
  LDA $2002    ; read PPU status to reset the high/low latch
  LDA #$3F
  STA $2006    ;write high, then low byte of PPU address to PPU port $2006
  LDA #$00
  STA $2006  
  LDX #$00
LoadPalettesLoop:
  LDA palette, x        ;load palette byte
  STA $2007             ;write to PPU port $2007
  INX                   ;set index to next byte
  CPX #$20            
  BNE LoadPalettesLoop  ;if x = $20, 32 bytes copied, all done
  
LoadSprites:
  LDX #$00 ; start at 0
LoadSpritesLoop:
  LDA sprites, x ;Load data from PRG-ROM (address sprites + x)
  STA $0200, x ;store sprite data in CPU RAM ( address $0200 + x)
  INX
  CPX #$30		;Compare X to # of bytes of sprite data
  BNE LoadSpritesLoop ;Loop if we haven't loaded all bytes yet
  
LoadBackground:
  LDA $2002             ; read PPU status to reset the high/low latch
  LDA #$20
  STA $2006             ; write the high byte of $2000 address
  LDA #$00
  STA $2006             ; write the low byte of $2000 address
  LDX #$00              ; start out at 0
  LDA #LOW(background)
  STA pointerLo       ; put the low byte of the address of background into pointer
  LDA #HIGH(background)
  STA pointerHi       ; put the high byte of the address into pointer
  LDA #$00		
  sta counterLo       ; put the loop counter into 16 bit variable
  LDA #$04		
  sta counterHi       ; count = $0400 =1KB, the whole screen at once including attributes
  LDY #$00            ; put y to 0 and don't change it
.LoadBackgroundLoop:
  LDA [pointerLo], y
  STA $2007           ; copy one background byte
  LDA pointerLo
  CLC
  ADC #$01
  STA pointerLo
  LDA pointerHi
  ADC #$00
  STA pointerHi       ; increment the pointer to the next byte
  LDA counterLo
  SEC
  SBC #$01
  STA counterLo
  LDA counterHi
  SBC #$00
  STA counterHi       ; decrement the loop counter
  LDA counterLo
  CMP #$00
  BNE .LoadBackgroundLoop
  LDA counterHi
  CMP #$00
  BNE .LoadBackgroundLoop  ; if the loop counter isn't 0000, keep copying
  LDA #$00
  ;STA $2005
  ;STA $2005
  RTS
;;;;Handle Input;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;**ReadController: Sets buttons1 and buttons2
;Byte order: A, B, Select, Start, Up, Down, Left, Right
;**
ReadController: 
  LDA buttons1			;Save our input from to detect button presses and releases
  STA buttons1OldState
  LDA buttons2
  STA buttons2OldState
  LDA #$01				;Initialize controller reading at port $4016
  STA $4016
  LDA #$00
  STA $4016       		; tell both the controllers to latch buttons
  LDX #$08 				;counter to read 8 buttons from shift register @ $4016
.ReadControllerLoop:
  LDA $4016
  LSR A					;Left Shift, bit0 -> Carry
  ROL buttons1 			; Right Shift, bit0 <- Carry
  DEX
  BNE .ReadControllerLoop
.ReadController2: 		;Initialize controller reading at port $4016
  LDA #$01
  STA $4017
  LDA #$00
  STA $4017       		; tell both the controllers to latch buttons
  LDX #$08 				;counter to read 8 buttons from shift register @ $4016
.ReadController2Loop:
  LDA $4017
  LSR A					;Left Shift, bit0 -> Carry
  ROL buttons2 			; Right Shift, bit0 <- Carry
  DEX
  BNE .ReadController2Loop
  RTS
  
;;;;Resource bank;;;;
  .bank 1
  .org $E000
palette: 
  .db $0F,$1C,$2D,$0C, $0F,$20,$2D,$0C, $0F,$14,$03,$24, $00,$00,$00,$00 ;First 16 bytes are for BG. 4 sets of 4 colors $E000-$E010
  .db $0F,$21,$39,$01, $0F,$13,$20,$1C, $0F,$2B,$39,$13, $0F,$10,$01,$00 ;Second 16 bytes are for sprites. $E011-$E020
  
sprites:				;Y, tile#, Attr, X
  .db $80,$00,$00,$10	;player 1 paddle top -> 0200-0203
  .db $88,$10,$00,$10	;player 1 paddle middle 0204-0207
  .db $90,$20,$00,$10	;player 1 padle bottom -> 0208-020B
  .db $10,$01,$00,$80	;ball	-> 020C-020F
  .db $80,$00,$42,$E8	;player 2 paddle top -> 0210-0213
  .db $88,$10,$42,$E8	;player 2 paddle middle -> 0214-0217
  .db $90,$20,$42,$E8	;player 2 paddle bottom -> 0218-021B
  .db $40,$30,$00,$38	;Player 1 score low digit -> 021C-021F
  .db $40,$30,$00,$30	;Player 1 score high digit -> 0220-0223
  .db $40,$30,$00,$C8	;Player 2 score low digit -> 0224-0227
  .db $40,$30,$00,$C0	;Player 2 score high digit -> 0228-022B
  .db $40,$59,$00,$B8	;"W" sprite on Win Screen -> 022C-022F
  
background:
  .db $59,$5A,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5c,$5B,$5C,$5B,$5C,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  .db $59,$5A,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5D,$5A,$59
  .db $59,$5A,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6c,$6B,$6C,$6B,$6C,$5A,$59
  
attribute:
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  .db %00000000, %00000000, %01010000, %01010000, %01010000, %01010000, %00000000, %00000000
  .db %00000000, %00000000, %10101010, %10101010, %10101010, %10101010, %00000000, %00000000
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  .db %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
  
textHeader_solo:
  .db $21, $4C, $04		;address to start writng tiles (HIGH, LOW), character count. The text data should always be the next in memory, so that it is three bytes from this position 
text_solo:
  .db $4C,$48,$45,$48

textHeader_2Player:
  .db $21,$8C,$08
text_2Player:
  .db $32,$59,$49,$45,$3A,$52,$3E,$4B
  
textHeader_VSAI:
 .db $21,$CC,$05
text_VSAI:
  .db $4F,$4C,$59,$3A,$42
  
textHeader_pressStart:
  .db $22,$49,$0E
text_pressStart:
  .db $94,$89,$8B,$7E,$8C,$8C,$59,$8C,$8D,$7A,$8B,$8D,$95,$94
  
textHeader_playerServe:
  .db $22,$49,$08
text_playerServe:
  .db $94,$89,$85,$7A,$92,$7E,$8B,$59
  
textHeader_pressA:
  .db $22,$6B,$08
text_pressA:
  .db $89,$8B,$7E,$8C,$8C,$59,$7A,$94
  
textHeader_player1:
  .db $22,$51,$01
text_Player1:
  .db $71
  
textHeader_player2:
  .db $22,$51,$01
text_Player2:
  .db $72
 
text_coverUpper:
  .db $5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C,$5B,$5C
  
text_coverLower:  
  .db $6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C,$6B,$6C
  
textHeader_title1:
  .db $22,$88,$10
text_title1;
  .db $B0,$B1,$B2,$B3,$B4,$B5,$B6,$B7,$B8,$B9,$59,$59,$59,$59,$59,$59
textHeader_title2:
  .db $22,$A8,$10
text_title2;
  .db $C0,$C1,$C2,$C3,$C4,$C5,$C6,$C7,$C8,$C9,$59,$59,$59,$59,$59,$59
textHeader_title3:
  .db $22,$C8,$10
text_title3;
  .db $59,$59,$59,$59,$59,$59,$D0,$D1,$D2,$D3,$D4,$D5,$D6,$D7,$D8,$D9
textHeader_title4:
  .db $22,$E8,$10
text_title4;
  .db $59,$59,$59,$59,$59,$59,$E0,$E1,$E2,$E3,$E4,$E5,$E6,$E7,$E8,$E9
  
beepToneSeq:		;C4, G4, A#4(Bb?), C5, C4, G4, A#4, C5, A#3, F4, G#4(Ab?), A#4, A#3, F4, G#4, A#4
	.word $00d2, $008E, $00EF, $006A, $00d2, $008E, $00EF, $006A, $01DF, $009F, $0096, $00EF, $01DF, $009F, $0096, $00EF ;16
  
menuSelect_position:
	.db $00,$10,$20
	
distanceToPower_3_Hi:					;Player is 3 tiles long, 24 px, + 4px pffset for top and bottom due to ball radius
	.db $01,$01,$00,$00,$00,$00,$01,$01		;Pixel/Frame
distanceToPower_3_Lo:					
	.db $7F,$00,$7F,$30,$30,$7F,$00,$7F		;SubPixel/Frame
	
hitCountToLevel:
  .db $05,$19,$28		;5 hits, 25 hits, 50 hits
levelToXSpeed:
  .db $03,$04,$05		;Probably want one level that can't be achieved except temporarily after a power shot, which then drops back down into normal max level
	  
;;;;Interrupt vars;;;;
  .org $FFFA     ;first of the three vectors starts here
  .dw NMI        ;when an NMI happens (once per frame if enabled) the 
                   ;processor will jump to the label NMI:
  .dw RESET      ;when the processor first turns on or is reset, it will jump
                   ;to the label RESET:
  .dw 0          ;external interrupt IRQ is not used in this tutorial
  
;;;;CHR Bank;;;;
  .bank 2
  .org $0000 ;CHR ROM, will show up in PPU memory
  .incbin "D:\NESDev\Projects\CyberPong2023\CyberPong.chr"   ;includes 8KB graphics file
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: PPU Garbage Graphics Problem

Post by Fiskbit »

Since you're willing to share the code, I suggest also sharing the ROM; debugging tools for the NES are very powerful and it's often much easier to find the cause using them than by looking at the code. I'll wait on trying to debug the background issue until I can look at a ROM.

Regarding improvements you could make to the code:
- In NMI, I generally prefer to do OAM DMA after writing to VRAM (e.g. nametable tiles), not before. If your VRAM writes extend past the end of vblank, you get corruption of VRAM like you're describing that persists until the corrupted data is written again, and it's hard to know what will get corrupted. If your OAM DMA extends past the end of vblank, you'll have (probably minor) sprite corruption for a single frame. The latter is much less disruptive.

- One concept I suggest learning is jump tables. Instead of having a chain of comparisons trying to figure out which code to run based on a variable, you can simply use that variable to index into a table of function pointers. In addition to the approaches listed on the wiki, another jump table method that can be useful is to have the table immediately follow the 'JSR do_action' instruction and then have the do_action function PLA the return address off the stack to use as the base address of the table; this can be useful because it doesn't hardcode the table address into the function. There are even more methods, too. All the various approaches have pluses and minuses and which one you choose comes down to context and preference.

- When using a value that is composed of many flags, such as writing a constant to a PPU register like $2000 or $2001, it is generally much better to build the value by OR'ing together named flags. This way, you don't have to worry about whether you got the right bits in the right locations, and it's easy to understand from the names what the value does. For example: LDA #kPpuControlEnableNmi | kPpuControlSpriteAddress1000

- Instead of keeping the previous button state and comparing it against the current state each time you are checking input, you can instead create variables that indicate whether a button has been pressed or released on this frame. The wiki has example code for both. Personally, I haven't found it useful to track the release state and so I never do, but button presses are extremely useful. And as mentioned above, use named constants that you can OR together, not raw values; there are example constants on that same wiki page.

- Your controller reading code has an issue. While you're correct that joypad1 is read from $4016 and joypad2 is read from $4017, it is not the case that joypad2 is written to through $4017. Both joypads are written through $4016; writes go to both, so they're both strobed at the same time (unconditionally). $4017 writes serve a different purpose entirely.

- If your IRQ handler is unused, I suggest having it reset the game rather than point at 0, where it could do anything (most likely hang). IRQs could happen if you misconfigure the hardware or your game crashes and executes a BRK instruction, and resetting is a marginally better experience for the player. A more advanced trick would be to have the IRQ point at some kind of crash handling function that prints information you could use to debug, but given how good Mesen is as a debugging tool, this is mostly helpful just for taking crash reports from players.

- In the future, you may want to structure your game loop so that the NMI only does PPU and audio tasks while the rest of the game is handled outside the NMI by code that waits for the NMI to run. This allows you to more gracefully handle lag frames, where processing the next frame of gameplay takes longer than 1 PPU frame. Your current game is simple and thus likely won't lag, but if it did, it could crash because your NMI would interrupt your code and clobber registers (which should normally be saved at the start of an interrupt handler and restored at the end), do PPU transfers with partial data, and call GameEngine again while it's already running. When all of the logic is run in the NMI like you're doing, normally you have to prevent additional NMIs from happening by disabling NMI at the start and enabling at the end (which can trigger graphical glitches because of bugs in the PPU) or implementing a flag that tells the NMI to exit early. Having everything in the NMI also makes it harder to allow the audio engine to run on lag frames, and lag frames are much more disruptive to the player's experience when audio also slows down. In this post, I wrote some skeleton code showing how to structure a game where the NMI just does PPU and audio tasks.

- This is just a personal preference thing and code style is very personal in general, but I find it incredibly helpful to make use of vertical whitespace in assembly code. I like to use line breaks to separate chunks of assembly that are each focused on a particular task, allowing me to think more about logical units rather than having to piece apart individual lines of code in a giant contiguous wall. This also aids in commenting, because if the code needs a comment, it's usually just one comment at the top of the chunk. These units are usually 2-6 instructions long, with outliers.
SleepySheep
Posts: 9
Joined: Thu Jan 26, 2023 5:28 pm

Re: PPU Garbage Graphics Problem

Post by SleepySheep »

I'm sharing the ROM in the link below, as well a .fns file that contains a list of address values for variables/subroutines.
https://drive.google.com/drive/folders/ ... sp=sharing

This is incredible advice, thank you! In particular I was wondering how to better structure so not everything is in the NMI, and the jump tables are something I'm excited to learn about, since my chaining of cmp/branch instructures feels messy. Once I get this game 'finished' in a playable sense, I am going to rewrite it with your points in mind (plus a bunch of stuff I already know I can improve on)

The first things I'm going to do are swap tile writing and sprite transfer as per your pointer, set my IRQ pointer to Reset, and fix my controller 2 issue. Coincidentally I had wondered if I was using $4017 right once I learned it has shared duties, but my code was working like I wanted, so I shrugged my shoulders on it.
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: PPU Garbage Graphics Problem

Post by Fiskbit »

The issue occurs when the A button event and the text event from textBlinkTimer happen at the same time. This causes your VRAM accesses to extend beyond the end of vblank. I've attached an image of the Mesen event viewer showing the accesses going off the bottom (vblank) to the top (rendering). I don't think all of this is happening in the DrawTiles call, though, because between the two sets of red dots indicating $2007 writes, we see blue dots indicating controller reads. That makes me think the second set may be happening in GameEngine, which is probably not what you intend.
Attachments
Cybe Pong glitch.png
SleepySheep
Posts: 9
Joined: Thu Jan 26, 2023 5:28 pm

Re: PPU Garbage Graphics Problem

Post by SleepySheep »

Geeze, I think I see the problem now. There is an errant call to cover the "Press A to Serve" text with floor tiles that I'd meant to replace with a flag set. How embarassing! That's alright, the tile drawing code needs some refactoring anyway. I really can't thank you enough for taking the time to help me.

If I might ask, what version of Mesen are you using? I am using 0.9.9. My event viewer doesn't have the option to view controller reads, nor the set of debug controls at the top, and I couldn't find any options to show them. In fact, I had some trouble trying to step through debugging while having the Event Viewer open that I haven't had time to distinctly qualify.
User avatar
Individualised
Posts: 310
Joined: Mon Sep 05, 2022 6:46 am

Re: PPU Garbage Graphics Problem

Post by Individualised »

SleepySheep wrote: Fri Jan 27, 2023 1:22 pm Geeze, I think I see the problem now. There is an errant call to cover the "Press A to Serve" text with floor tiles that I'd meant to replace with a flag set. How embarassing! That's alright, the tile drawing code needs some refactoring anyway. I really can't thank you enough for taking the time to help me.

If I might ask, what version of Mesen are you using? I am using 0.9.9. My event viewer doesn't have the option to view controller reads, nor the set of debug controls at the top, and I couldn't find any options to show them. In fact, I had some trouble trying to step through debugging while having the Event Viewer open that I haven't had time to distinctly qualify.
Check out Mesen 2: viewtopic.php?t=24391
SleepySheep
Posts: 9
Joined: Thu Jan 26, 2023 5:28 pm

Re: PPU Garbage Graphics Problem

Post by SleepySheep »

Individualised wrote: Fri Jan 27, 2023 2:36 pm
SleepySheep wrote: Fri Jan 27, 2023 1:22 pm Geeze, I think I see the problem now. There is an errant call to cover the "Press A to Serve" text with floor tiles that I'd meant to replace with a flag set. How embarassing! That's alright, the tile drawing code needs some refactoring anyway. I really can't thank you enough for taking the time to help me.

If I might ask, what version of Mesen are you using? I am using 0.9.9. My event viewer doesn't have the option to view controller reads, nor the set of debug controls at the top, and I couldn't find any options to show them. In fact, I had some trouble trying to step through debugging while having the Event Viewer open that I haven't had time to distinctly qualify.
Check out Mesen 2: viewtopic.php?t=24391
Right on, I never would have known there is a newer version, just going by the Mesen website. Thanks a bunch!
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: PPU Garbage Graphics Problem

Post by Pokun »

Because it's not really released yet. It's just a preview and is still very buggy crashing a lot. But it merges Mesen and Mesen-S plus Game Boy and PC-Engine emulation.
Fiskbit
Posts: 891
Joined: Sat Nov 18, 2017 9:15 pm

Re: PPU Garbage Graphics Problem

Post by Fiskbit »

For what it's worth, I've been using Mesen v2 for at least half a year as a tester, and in my experience, it's currently stable, works at least as well as the old one, and has worthwhile improvements to accuracy and functionality. For the NES, I don't think there's any reason to use the old version (though if you must, hunt down a copy of 0.9.9.62, which was the last public development version, or use one of the forks such as Mesen-X). I can't really vouch for how well v2 works for the other consoles, but NES is solid.
SleepySheep
Posts: 9
Joined: Thu Jan 26, 2023 5:28 pm

Re: PPU Garbage Graphics Problem

Post by SleepySheep »

I'll definitely check out Mesen 2 then. Sounds like its in pretty good shape. Thanks for the recommendation.
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: PPU Garbage Graphics Problem

Post by Pokun »

Odd, I've only used it for a few minutes and managed to get it to crash or freeze several times. It have only happened in the debugger or when opening certain tools like the tile viewer in certain games. I thought it was too early to bother Sour with it since he is still sorting many bugs out before the release version.
Sour
Posts: 891
Joined: Sun Feb 07, 2016 6:16 pm

Re: PPU Garbage Graphics Problem

Post by Sour »

Pokun wrote: Sun Jan 29, 2023 5:32 pm Odd, I've only used it for a few minutes and managed to get it to crash or freeze several times. It have only happened in the debugger or when opening certain tools like the tile viewer in certain games. I thought it was too early to bother Sour with it since he is still sorting many bugs out before the release version.
Not at all, if you've had crashes - let me know, fixing crashes/freezes is pretty much the main priority at the moment.
I did fix a number of crashes/freezes since the first preview, but finally got CI dev builds configured today, so if you could try grabbing the latest dev build (https://github.com/SourMesen/Mesen2#development-builds), checking if you can still get the crashes you noticed, and if you can, let me know what game/console/tools are causing the crashes.
Pokun
Posts: 2681
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: PPU Garbage Graphics Problem

Post by Pokun »

I see, I'll try and see if I can reproduce the crashes with the new build and report in the thread.
Post Reply