Splitting Apart the Split ScreenThe Journey to
Find, Analyze, and Fix Pac Man’s
Split Screen 4/21/2008: This article is featured on Kotaku's home page http://kotaku.com/ 12/10/2007: This article is featured on MAME World's home page http://www.mameworld.net/
In the previous article on Donkey Kong, we saw that the game designers built in a mechanism to prevent the player from ever passing level 99. After level 99 is reached, it repeats forever, never advancing to 100. However, this is not widely known because a timer bug prevents anyone from ever passing level 22. If only the designers of Pac-Man had done something similar. But they didn’t, and Pac-Man suffers from the well known split screen when level 256 is reached. We will examine how and why this occurs, and create a patch to fix the program’s bug.
The Problem
The first step is to search the
Internet to see if this answer has
already been found. We find a
lot of good work has been done
at Mark Longridge's web site [
http://cubeman.org/arcade-source/pacman.asm
] where Pac Man’s code is
disassembled and some comments
are added. There is also a more
complete disassembly of Ms. Pac
Man at Scott Lawrence’s web site
http://umlautllama.com/projects/pacdocs/mspac/mspac.asm. Ms. Pac Man shares a great
deal of code with the original
Pac Man, so some of these
comments are useful as well. 2C03 CD8F2B CALL #2B8F ; Draw fruit Using this we discover the subroutine which draws the fruit. It follows here with my comments. You will probably need some knowledge of assembly language to follow this. 2BF0 3A134E LD A,(#4E13) ; Load A with level number 2BF3 3C INC A ; Increase by one 2BF4 FE08 CP #08 ; Is this level < 8 ? 2BF6 D22E2C JP NC,#2C2E ; No, jump to compute different start for fruit table 2BF9 11083B LD DE,#3B08 ; Yes, load DE with address of cherry in fruit table 2BFC 47 LD B,A ; For B = 1 to level number 2BFD 0E07 LD C,#07 ; C is 7 = the total number of locations to draw 2BFF 210440 LD HL,#4004 ; Load HL with the start of video memory 2C02 1A LD A,(DE) ; Load A with value from fruit table 2C03 CD8F2B CALL #2B8F ; Draw fruit subroutine 2C06 3E04 LD A,#04 ; 2C08 84 ADD A,H ; Add 400 to HL 2C09 67 LD H,A ; HL now points to color memory 2C0A 13 INC DE ; DE now points to color code in fruit table 2C0B 1A LD A,(DE) ; Load A with color code from fruit table 2C0C CD802B CALL #2B80 ; Draw color subroutine 2C0F 3EFC LD A,#FC ; 2C11 84 ADD A,H ; Subtract 4 from H 2C12 67 LD H,A ; HL now points back to video memory 2C13 13 INC DE ; Increase pointer to next fruit in table 2C14 23 INC HL ; 2C15 23 INC HL ; Next starting point is 2 bytes higher 2C16 0D DEC C ; Count down how many clears to draw 2C17 10E9 DJNZ #2C02 ; Next B – loop back and draw next fruit 2C19 0D DEC C ; Count down C. Did C just turn negative? 2C1A F8 RET M ; Yes, return to game, we are done 2C1B CD7E2B CALL #2B7E ; No, call subroutine to draw a clear 2C1E 3E04 LD A,#04 ; 2C20 84 ADD A, H ; 2C21 67 LD H,A ; Increase HL by 400 for color value to be cleared 2C22 AF XOR A ; Load A with 0, the code for black color 2C23 CD802B CALL #2B80 ; Draw color subroutine – draws black color 2C26 3EFC LD A,#FC ; 2C28 84 ADD A,H ; Subtract 4 from H 2C29 67 LD H,A ; HL now points back to video memory 2C2A 23 INC HL ; 2C2B 23 INC HL ; Set next starting point to be 2 bytes more 2C2C 18EB JR #2C19 ; Jump back and draw next clear ; Arrive here when the level is 8 or higher 2C2E FE13 CP #13 ; Is the level > #13 (19 decimal, 7th key) ? 2C30 3802 JR C,#2C34 ; If not, skip next step 2C32 3E13 LD A,#13 ; If yes, treat all levels 19 and up as if they are level 19. 2C34 D607 SUB #07 ; Subtract 7 to point to first value to be drawn 2C36 4F LD C,A ; Copy result to C 2C37 0600 LD B,#00 ; Load B with Zero 2C39 21083B LD HL,#3B08 ; Load HL with pointer to start of fruit table 2C3C 09 ADD HL,BC ; 2C3D 09 ADD HL,BC ; Adjust fruit table pointer, based on current level 2C3E EB EX DE,HL ; Load DE to point to fruit in table 2C3F 0607 LD B,#07 ; Load B counter to draw 7 fruit 2C41 C3FD2B JP #2BFD ; Jump back up to fruit drawing section The Fruit Table:
Here are the main points of fruit drawing: Load level
number and increase its count by
1 We need to know that levels on Pac Man are stored with a starting value of zero. In other words, the first level that we play (cherry) is treated as level zero by the game. The second level (strawberry) is counted as level 1 by the game, and so on. The programmers know this and so when the subroutine is called to draw the fruit, it loads the level counter and then increases it by one. Then it figures out how many and which fruit to draw. When level 256 is reached, it is counted by the game as level 255 (this number is #FF in hexadecimal). The subroutine is called to draw the fruit and the value for the level wraps around to zero when it is incremented. No check is done to see if the Carry flag is set, which would have been one way for the programmers to realize the game has reached this point. So with the counter at zero, this causes the subroutine which draws fruit to think that it is a level less than 7 with only 1 to 7 fruit to draw, in addition to some possible blank spaces. The program starts drawing fruit with the counter in register B set to zero, instead of the expected number from 1 to 7. At the end of the loop, B is decreased by one and checked for zero. If it is not zero then the loop runs again, if it is zero then the subroutine ends. On level 256, B is zero to begin with. It is decremented at the end of the loop, which makes it roll back down to 255, causing the loop to run a total of 256 times, which is why the split screen gets drawn on this level. The result is that memory locations which are up to 512 bytes past the start of the fruit table, which are used for some other totally different part of the program, are drawn on the screen at increasing locations in video memory. The subroutine which draws the fruit is expecting only eight distinct values for the various fruit, and is expecting only seven memory locations where the fruit are drawn at the bottom of the screen. The memories which end up getting fed into this subroutine have different values than the ones the subroutine is expecting. There are two main parts to drawing a fruit. The first part draws the fruit, and the second part draws the color. It turns out that each fruit is actually made up of four separate pieces of graphics, all painted with a single color code. Let’s examine the subroutines which draw the fruit and the colors. The following subroutine draws the four parts of a fruit onscreen. It requires that A is loaded with the code for the first part of the fruit and HL is loaded with the memory address of the first position on screen where it is to be drawn. 2B8F E5 PUSH HL ; Save HL 2B90 D5 PUSH DE ; Save DE 2B91 111F00 LD DE,#001F ; this offset is added later for third part of fruit 2B94 77 LD (HL),A ; Draw first part of fruit code into screen memory 2B95 3C INC A ; Point to second part of fruit 2B96 23 INC HL ; Increment screen memory for second part of fruit 2B97 77 LD (HL),A ; Draw second part of fruit code into screen memory 2B98 3C INC A ; Point to third part of fruit 2B99 19 ADD HL,DE ; Add offset for third part of fruit 2B9A 77 LD (HL),A ; Draw third part of fruit code into screen memory 2B9B 3C INC A ; Point to fourth part of fruit 2B9C 23 INC HL ; Increment screen memory for fourth part of fruit 2B9D 77 LD (HL),A ; Draw fourth part of fruit code into screen memory 2B9E D1 POP DE ; Restore DE 2B9F E1 POP HL ; Restore HL 2BA0 C9 RET ; Return The following subroutine draws colors onscreen for a 2x2 grid. It requires that A is loaded with the code for the color and HL is loaded with the memory address of the position on screen where the first color is to be drawn. If a clear value is to be drawn, the first address is called (#2B7E). If A is preloaded with a color, then the second address is called (#2B80). 2B7E 3E40 LD A,#40 ; Used to draw clear value 2B80 E5 PUSH HL ; Save HL 2B81 D5 PUSH DE ; Save DE 2B82 77 LD (HL),A ; Draw color into first part 2B83 23 INC HL ; Set location to second part of fruit 2B84 77 LD (HL),A ; Draw color into second part 2B85 111F00 LD DE,#001F ; Offset is used for third part 2B88 19 ADD HL,DE ; Set location to third part of fruit 2B89 77 LD (HL),A ; Draw color into third part 2B8A 23 INC HL ; Set location to fourth part of fruit 2B8B 77 LD (HL),A ; Draw color into fourth part 2B8C D1 POP DE ; Restore DE 2B8D E1 POP HL ; Restore HL 2B8E C9 RET ; Return The next part of
understanding the split screen
is to examine the memory
locations of the screen
elements. Luckily this has
already been done by Bart
Grantham.
http://www.bartgrantham.com/projects/mspacman/sprite-RAM.html
The memory grid has been
overlayed with the screen to
show this:
After the cherry
is drawn, the program calls the
subroutine which draws in the
color, which is basically the
same except it writes to the
color memory instead of
character memory, and it writes
the same color to all four
positions instead of increasing
the value each time. The Bug in Action
The fourteenth fruit, (the 2nd key), is drawn into the very bottom-left corner of video memory which is not visible on the screen (starting with address #401E).
Then the next fruit drawn, (the third key), starts at address #4020 which is the bottom right corner, not visible on screen. But the fruit-drawing routine draws four locations: #4020, #4021, #4040 and #4041. So, the top half of this fruit is not seen onscreen.
The bottom half is drawn into the top-right location of the playing field, going top-to-bottom instead of going right-to-left.
The next fruit that is drawn, (the fourth key), is visible onscreen, split between the bottom right corner for its top half, and the top right side of the playing field for the bottom half. This is repeated for the entire bottom row, including into the very bottom left corner where the top half memories are not visible onscreen (#403E and #403F), but the bottom half is drawn to the bottom two places in the first column of the playfield (#405E and #045F). Also during this bottom row, after the 8th key is drawn, (the 5th visible being split at the bottom), the fruit table ends and the program starts drawing fruit using values from the memories that come after the fruit table. These values cause the subroutine to draw seemingly random fruit and colors. For example, the values of the two memory locations which follow final key in the fruit table are #73 and #20. So the program tries to draw fruit #73 with color #20 into the next memory locations. The subroutine draws character values #73-#77, which are not visible characters, so it just draws blanks on the screen. The next two values in memory are #00 and #0C. The subroutine draws a “fruit” with these values by drawing the numbers 0, 1, 2, and 3 into these memories onscreen. However, the color subroutine draws color #0C which is black, so these values are erased from view almost instantly after they are drawn. Looking at the last fruit to be split between the bottom of the screen and the right side, we observe the behavior of the 9th fruit after the end of the fruit table. The first value that is drawn is #06 which draws the number 6, 7, 8, and 9 into memory, as seen. Then the color subroutine blanks them all out with a black color.
Looking at the next fruit, we see it is split between invisible memories in the bottom left of the screen and the right side. The memory contains #04 which makes the subroutine draws the number 4, 5, 6, and 7 into memory, as seen. After they are drawn, the color subroutine blanks them all out with a black color.
After the end of the bottom row, the starting addresses of the fruit shift from #403E, to #4040 which is the top right corner of the playfield. The 11th location after the fruit table is drawn into the top right corner of the playfield, and we can see that since the memories here are arranged differently than at the bottom, the fruits are drawn going down 2 columns instead of across 2 rows.
Then the program draws the
corrupted fruit values down each
column of memory, overwriting
the values that it had written
on the previous pass for the
bottom half of the fruit, with
new values for the top half of
each fruit as well as its own
bottom half in the next column
over. These bottom half values
keep getting replaced by the
next column of fruits, except in
the final column when the
subroutine ends.
We can see more
clearly the data written to the
screen by taking a screen shot
of the level being cleared by
the rack skip dip switch
setting. This un-hides most of
the data written to the screen
that might otherwise be hidden
by being colored black.
THE FIXThe next step is
to fix the code so that this
behavior does not occur on level
256. HACK8: Mark Spaeth's "20 byte" level 255 Pac-Man rom fix ; level 255 pac fix ; HACK8 ;0A90 C3800F JP #0F88 ;0A93 00 NOP … ;0F88 3A134E LD A,(#4E13) ; board number ;0F8B 3C INC A ;0F8C FEFF CP #FF ;0F8E 2803 JR Z,#0F93 ; don't store level if == 255 ;0F90 32134E LD (#4E13),A ; store new board number ;0F93 C3940A JP #0A94 ; jump back What does this
fix do? It overwrites the
program that increases the
game’s level with a jump command
(called a ‘hook’) that takes it
out to a new subroutine that has
been written and placed in some
unused memory. This subroutine
really has nothing directly to
do with the fruit drawing. It
checks the value of the level
right after it has been
increased after a level is
completed. If an overflow
occurs, the result is not
written back to memory. Then it
jumps back to the original
program. The result is the game
stays forever on game level #FE
(the 255th screen) and never
goes any higher. Another hack
which bypasses the self-test
must also be employed with this
one, or else the checksum
routines that are run when the
game powers on will detect a
problem and refuse to run the
game. 2BF0 3A134E LD A,(#4E13) ; Load A with level number 2BF3 3C INC A ; Increase by one 2BF4 FE08 CP #08 ; Is this level < 8 ? 2BF6 D22E2C JP NC,#2C2E ; No, jump to compute different start for fruit table Part of the fix will change the code to read this way: 2BF0 3A134E LD A,(#4E13) ; Load A with level number 2BF3 FE07 CP #07 ; Is this level < 7 ? 2BF5 D22E2C JP NC,#2C2E ; No, jump to compute different start for fruit table 2BF8 3C INC A ; Increase by one So instead of
increasing the value by 1 and
then checking against 8, we
check against 7 and increase
only if the original value is
less than 7. Now we have to
modify the code that starts at
#2C2E if the level is greater
than 7. 2C2E FE13 CP #13 ; Is the level > #13 (19 decimal, 7th key) ? 2C30 3802 JR C,#2C34 ; No, skip next step 2C32 3E13 LD A,#13 ; Yes, load A with #13 2C34 D607 SUB #07 ; Subtract 7 to point to first value to be drawn We modify this to read: 2C2E FE12 CP #12 ; Is the level > #12 (18 decimal, 7th key) ? 2C30 3802 JR C,#2C34 ; No, skip next step 2C32 3E12 LD A,#12 ; Yes, load A with #12 2C34 D606 SUB #06 ; Subtract 6 to point to first value to be drawn We arrive at
this location with A being one
less than the program was
expecting, because we did not
increment it before coming here.
So we check against #12 instead
of #13, force to #12 instead of
#13, and subtract 6 instead of 7
to make up for it. Everything
else can remain the same and the
bug is fixed. Before: 2FFC: 00 2FFD: 00 After: 2FFC: 8B 2FFD: 79 The total patch is made by changing just nine bytes of the original code, plus two bytes for the checksum fix. This patch can be written into Puckman romset file named [namcopac.6h] into three areas. For continuity, this is padded with four bytes of the original code, making it 15 bytes instead of the expected 11 bytes. 0BF3: FE 07 D2 2E 2C 3C 0C2F: 12 38 02 3E 12 D6 06 0FFC: 8B 79 MAME will
complain that the ROM set is
bad, because it uses more
complicated hashing algorithms
to detect errors, but it will
still work. :puckman:20700001:2BF3:FE07D22E:FFFFFFFF:No Split Screen :puckman:20510001:2BF7:00002C3C:0000FFFF:No Split Screen (2/5) :puckman:20010001:2C2F:00000012:000000FF:No Split Screen (3/5) :puckman:20610001:2C33:0012D606:00FFFFFF:No Split Screen (4/5) :puckman:20510001:2FFC:00008B79:0000FFFF:No Split Screen (5/5) Learn more about
cheat codes in MAME at
http://cheat.retrogames.com
Thoughts & CommentsIt is probably a
good thing that Pac Man has this
bug in its program. If it
didn’t, expert players could
conceivably be able to play the
game indefinitely, because
whenever they get tired they can
just park Pac Man in the hiding
places and leave the game to go
eat, sleep, or whatever, and
then return to the game and
continue playing. The only
limiting factor would have been
the length of time that the game
could run without a power outage
or suffering from some other
hardware failure. Experts would
have been able to play the game
for weeks, months, even years,
or more. |
In accordance with Title 17 U.S.C. Section 107, some of the material on this site is distributed without profit to those who have expressed a prior interest in receiving the included information for research and educational purposes. For more information go to: http://www.law.cornell.edu/uscode/17/107.shtml. If you wish to use copyrighted material from this site for purposes of your own that go beyond 'fair use', you must obtain permission from the copyright owner. |
|
|
|