reverse engineering Robotron 2084 for the Apple II

Topics related to older 6502-based hardware and systems including (but not limited to) the MOS Technology KIM-1, Synertek SYM-1, and Rockwell AIM-65.
Chromatix
Posts: 1462
Joined: 21 May 2018

Re: reverse engineering Robotron 2084 for the Apple II

Post by Chromatix »

Interesting to see LDA/PHA/LDA/PHA/RTS used here, at $51C4. A more compact alternative might be:

Code: Select all

JSR readNextChar   ; just to increment the pointer to an actual instruction
JMP ($0022)   ; indirect through the pointer
…which takes 6 bytes instead of 7, not a big difference, and the readNextChar routine isn't very fast. But it does save a bit of explicit stack manipulation.
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

BigEd wrote:
A milestone - well done!
Thank you, BigEd, your encouragements are very motivating!
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

Quote:
As a port, hopefully it'll not be super tied into any hardware specifics, though it doesn't look like the Apple II has too many hardware specifics to begin with. ;)
Thanks for the feedback, White Flame. Yes, the Apple II is really a clean platform, perfect for learning 6502. The one thing which I haven't cared about at all yet is the speaker. That's really so basic that it hurts :)
whartung
Posts: 1004
Joined: 13 Dec 2003

Re: reverse engineering Robotron 2084 for the Apple II

Post by whartung »

fschuhi wrote:
The one thing which I haven't cared about at all yet is the speaker. That's really so basic that it Hz :)
Fixed that for ya!
:)
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

Chromatix wrote:
Interesting to see LDA/PHA/LDA/PHA/RTS used here, at $51C4. A more compact alternative might be:

Code: Select all

JSR readNextChar   ; just to increment the pointer to an actual instruction
JMP ($0022)   ; indirect through the pointer
…which takes 6 bytes instead of 7, not a big difference, and the readNextChar routine isn't very fast. But it does save a bit of explicit stack manipulation.
There is not a single indirect jump in the whole Robotron code. I wonder why, seeing how elegant it can be used. Do you use indirect JMP regularly?
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

whartung wrote:
fschuhi wrote:
The one thing which I haven't cared about at all yet is the speaker. That's really so basic that it Hz :)
Fixed that for ya!
:)
:lol: :lol: :lol:
Chromatix
Posts: 1462
Joined: 21 May 2018

Re: reverse engineering Robotron 2084 for the Apple II

Post by Chromatix »

It is one of the more obscure addressing modes (absolute indirect), as on the NMOS 6502 it's *only* available for JMP. But it's a central part of the Acorn MOS, as each entry point (eg. JSR $FFEE) is often just a JMP ($02xx) through the patch table. It just struck me as the natural way to implement a return from an inline-data subroutine.
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

Robotron has a great gameplay. It's getting crowded pretty quickly - that's a strength, but it has a steep learning curve. There are distinct types of levels (called "waves" in Robotron), regarding the type of robot enemies you have to battle. The waves get increasingly difficult, fast. In the "Tank Wave" (first occuring as #7) there are Quarks pulsing around and spanning multiple tanks, each shooting bouncing shells https://youtu.be/t7_hvzIaqxk?t=134, that's a good player, still struggling through #7.

Problem: It takes time to get to this level - - no savepoints, no way to train a particular wave. I therefore inserted a screen to the configuration menu where you can select the wave you want to start with:
2.jpg
The game continues to the controls menu:
3.jpg
The game then starts with the Tank Wave, score is 0:
4.jpg
Robotron cycles through the types while throwing in some additional robot types and generally ramping up the total number of enemies. Here is Tank Wave #27, with two additional Spheriods lurking in the corners:
6.jpg
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

I wanted to enter two-digit wave numbers, with backspace, return and escape, handle special cases like "00". It's implemented as a statemachine using an RTS jump table as described in https://wiki.nesdev.com/w/index.php/RTS_Trick. Apart from the special characters, only numbers are allowed. The tutorial http://6502.org/tutorials/compare_instructions.html was very helpful to get the constraint right (I hope).

Code: Select all

next_char:                      ; main loop
		JSR show_digits
		JSR	waitKbdControls

		CMP ch_escape           ; <Escape> breaks
		BEQ break

		CMP ch_return           ; <Return> is a valid char, concludes input (if possible)
		BEQ .send_event

		CMP ch_left             ; <Left> removes last digit
		BEQ .send_event

		check_is_number         ; only chars <0> to <9> allowed
		BCC next_char           ; carry cleared if not in <0> to <9>

	.send_event:
		STA event               ; dispatch event
		JSR dispatch_event

		LDA state               ; check if we should exit, i.e. user has entered a number
		CMP s_exit
		BEQ exit

		JMP next_char

dispatch_event:
		LDA state               ; state index into RTS table
		ASL                     ; table has 16bit addresses
		TAX
		LDA rts_table+1,X       ; push hibyte first
		PHA
		LDA rts_table,X         ; push lobyte second, will be popped first by RTS
		PHA
		LDA event               ; state subroutines get the event in A
		RTS

state_hasNone:
		...
		RTS

state_hasOne:
		...
		RTS
		
state_hasTwo:
		...
		RTS
		
state_exit:
		...
		RTS
		
rts_table:
		.word   state_hasNone-1
		.word   state_hasOne-1
		.word   state_hasTwo-1
		.word   state_exit-1
Attachments
hook.asm
(5.37 KiB) Downloaded 84 times
User avatar
barrym95838
Posts: 2056
Joined: 30 Jun 2013
Location: Sacramento, CA, USA

Re: reverse engineering Robotron 2084 for the Apple II

Post by barrym95838 »

I can see the 1 MHz Apple ][ start to "run out of breath" on some of the busier screens, but it still impresses me that games like this can remain playable and entertaining on hardware with "unassisted" graphics, one-bit sound, '555 A to D, and no interrupts. The "centipede levels" in Bandits were especially marvelous to me. Benny Ngo was (is?) quite the 6502 virtuoso.
Got a kilobyte lying fallow in your 65xx's memory map? Sprinkle some VTL02C on it and see how it grows on you!

Mike B. (about me) (learning how to github)
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

Yes, both Robotron and Bandits have aged very well, IMHO. Apart from the fun gameplay, Bandits is also a formidable piece of software, given what Benny Ngo was able to cram into 6kb.
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

I have started to get a closer look at what's happening in terms of reads from and writes to memory. The problem is of course not collecting the info but rather condensing it sensibly. I first tried to group the access in pages, for the different subroutines as suggested by sark02 earlier in this thread. The info is of course helpful but only on a per-subroutine basis. Given the overwhelming number of instructions (and even subroutines) in Robotron, I decided to try to see the bigger picture first, on a per-byte basis.

It's work in progress so it's a bit premature to share it here, but maybe you guys have feedback to offer on how to progress from here. That's the reads from a couple of seconds of the intro (up to $CFFF, i.e. including the softswitches but excluding the higher parts of the ROM):
reads small.jpg
That's really the big picture - - a heatmap with all reads, each line of blocks a page. I can zoom in:
reads top.jpg
Address and access count of the selected block is shown in the status bar:
reads bottom.jpg
reads bottom.jpg (11.33 KiB) Viewed 1987 times
Beginning $2000 is the memory which is mapped to the screen. I had first assumed that I should not see any reads here, only writes, but you can see the effect of the code updating "sprites" in memory. The algorithm obviously needs to know what's in a particular area of the screen to remove and display pixels correctly. Related to displaying graphics are the pages $1200 and $1300. Access to the pages have the same color (i.e. exactly the same counts in both pages for each byte). These are the lobyte/hibyte lookup tables to translate logical pixel line into the where those lines physically reside in memory.

Here the writes which clearly show the hires memory:
writes small.jpg
There are also some lone hot spots below the hires memory. That's where the code resides, so those bytes are modified by and in the code. The two consecutive ones on the left are part of the printChar subroutine explained in one of my earlier posts.

At last the counts for particular program counters for read and write instructions (hotter colors = instruction was executed more often):
PCs small.jpg
PCs small.jpg (11 KiB) Viewed 1987 times
The workbench is reasonably fast enough to allow "slicing" the total dataset into smaller pictures = containing less cycles. I should thus be able to drill down to seeing a particular sprite drawn on the screen (write) and relate the bitmap to it's origin (read).
whartung
Posts: 1004
Joined: 13 Dec 2003

Re: reverse engineering Robotron 2084 for the Apple II

Post by whartung »

The heat map of memory access is a pretty neat feature.
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

After some time away I've been able to spend some more hours on the reverse engineering project.

The Apple II is severly constrained in terms of memory. Consequently, Robotron uses for the "cut scenes" the actual game code to display elements like the player character or enemies. From a reverse engineering point of view this is fortunate, because I can use the uncluttered (and even scripted) movements of elements on the start screens to understand how the game moves "sprites" around the screen. For an example see the movements of the player character on the screen explaining the controls.

I've extended the read/write tracking of the workbench from my last post to project what's happening in memory onto the simulated screen. In the following pic you can see the "echo" of the beginning of the movement of the player character on the controls screen (i.e. what you see in the Youtube video, the character going from left to right first.)
control_move.jpg
Following the strategy suggested in my last post....
Quote:
The workbench is reasonably fast enough to allow "slicing" the total dataset into smaller pictures = containing less cycles. I should thus be able to drill down to seeing a particular sprite drawn on the screen (write) and relate the bitmap to it's origin (read).
... I can visually do a trial-and-error to constrain the shown range of cycle counts (which I use as global clock) until I only see one particular "frame" of the movement:
control_single.jpg
control_single.jpg (3.78 KiB) Viewed 1860 times
The workbench can now show me the corresponding reads from memory:
control_reads.jpg
control_reads.jpg (5.17 KiB) Viewed 1860 times
The area between the horizontal lines is the video memory. The colors on the zero page (first line) show that there is some heavy indirect reading involved.
Looking at which code was executed between the shown cycle counts is of course necessary to understand what e.g. the memory related to the two green lines on the lower left contain: It's the actual sprite data, both for removing the first shown player character ("facing the camera")...
man1.jpg
man1.jpg (9.57 KiB) Viewed 1860 times
... as well as one frame of the player character in movement to the right:
man2.jpg
man2.jpg (9.39 KiB) Viewed 1860 times
From inspecting the code and what's in memory I've gleaned some first information about how the sprites are stored in memory (resulting in the crude X. displays above), in particular how multiple sprites are stored in order to make it easy to horizontally move the sprite pixel-wise through the bits in screen memory.

This part of Robotron is quite central to the overall gameplay. The code takes generalised sprites (i.e. different heights, widths and "facings") and projects them onto the screen, inside bytes and scattered across the video memory (due to Apple II ideosyncrasies). In addition, the code needs to track the state of the sprite, to cycle through e.g. the left+right movements of the legs (or pulsing shapes etc.), which also needs to take into account the direction of movement. The algorithm is much more involved than I had originally thought it would be. A nice challenge! :)
User avatar
fschuhi
Posts: 38
Joined: 23 Feb 2019

Re: reverse engineering Robotron 2084 for the Apple II

Post by fschuhi »

Having a good idea about how something works is still far from really knowing the details of how this is accomplished (oh yeah, Captain Obvious, tell us more ;) ). The "sprite projector" hinted at in the post above can serve as a good training ground to learn to tear into asm code.

Sadly (or interestingly, actually), my usual debugging skills can not so skillfully employed in the 6502 / Apple II arena. For the Apple II there are of course debuggers with single-stepping and breakpoints, but I think at this point of the learning curve I need more of an "explorer" than a "debugger".

My workbench has the ability to track all memory reads and writes, both absolute and indirect. That's e.g. what's behind the heat maps and the "screen echos" shown in my last two posts. For each read/write I track the cycle count of that operation, together with its PC. For the writes I store both the old value before the operation and the new value stored by the write. Although I do not yet log AXY (and SP only indirectly via the activity on page $01), some good understanding can be derived from relating changes in memory with the flow of instructions.
memlog.jpg
The pic above shows a part of the "sprite projector" and the memory log, positioned at a particular cycle and PC. I can move upwards and downwards in the MemLog window. This automatically synchronises with the asm code in the main window. I can also search forward and backward to the same PC, to compare changes in memory between different runs through the same stretches of code. I can also go in the main asm window to any PC and synchronise the MemLog to the first (or nth, forward and backward) occurance.

It's a bit like time travel debugging. For fully fledged time travel I would also have to store all changes to the registers, but that hasn't been necessary so far. Given the importance of the registers for, well, everything, that might come as a surprise. But because registers have to be loaded from somewhere and because this somewhere is usually not far from the point of interest, it's actually quite easy to trace the flow of data both through the memory and also through the registers. I'll see how far I get with that approach.
Post Reply