Foreword
In the previous post we implemented the complete emulation of timers of CIA#1. This is mandatory in order to implement Tape emulation.In this post we will be finishing off the implementation of tape emulation. Apart from the technical emulation aspects of tape loading, we will also be implementing a file browser you will use to browse to the .tap file on your mobile device that you want to load.
As an added bonus, I will also be showing how you would go about enlarge the video output displayed. As you know the C64 screen has a resolution of 320x200 pixels. This can result in quite a small block on a high resolution screen. To counteract this inconvenience, I will also be showing in this post how to scale this image so it is displays larger.
Implementing a File Browser
My first step was to implement a file browser allowing you to browse to a .tap file on your device that you want to attach.Luckily I didn't need to re-invent the wheel here. I found a very nice tutorial on the net showing you how to create an Android File Browser together with some source code:
http://custom-android-dn.blogspot.co.za/2013/01/create-simple-file-explore-in-android.html
I ended up modifying the code a bit so that it fits within our application.
I am not going to go into a lot of detail on how to create the create the file browser. I will, however, highlight a couple points of importance.
The file browser consists out of two screens:
Needless to say, each of above the two screens is an activity in its won right. The class name of the first screen is FileDialogueActivity.class and that of the second screen is FileChooser.class.
Something interesting I want to highlight is that there is parameter passing between FileDialogueActivity.class and FileChooser.class. This is functionality you would quite often use when writing an Android application. Like in our case, you would launch FileChooser from FileDialogueActivity so that the user can choose a file and when that activity returns, you will want to know which file was chosen.
Let us look at some code for this functionality. When you tap on Browser button in FileDialogueActivity, the following method will execute:
public void getfile(View view){ Intent intent1 = new Intent(this, FileChooser.class); startActivityForResult(intent1,REQUEST_PATH); }
This code looks close to the conventional way we have used up to know for invoking a new activity. But, with a subtle difference. The activity FileChooser is initiated with the method call startActivityForResult. This will the launch FileChooser, but indicates that we expects a result back. This will become clear in a moment.
Now, at the point when FileChooser is about to return, that is, a file was chosen, the following method will execute:
private void onFileClick(Item o) { //Toast.makeText(this, "Folder Clicked: "+ currentDir, Toast.LENGTH_SHORT).show(); Intent intent = new Intent(); intent.putExtra("GetPath",currentDir.toString()); intent.putExtra("GetFileName",o.getName()); setResult(RESULT_OK, intent); finish(); }
As you can see, we are creating an Intent instance for the purpose of passing back values to FileDialogueActivity. The values of interest are GetPath, GetFileName and finally the result code, which is RESULT_OK when all went well.
Finally, we are calling finish() that will cause us to move back to FileDialogueActivity. With FileDialogueActivity coming back to live, we need to know which file was choosen within FileChooser. To obtain this knowledge we need to add a method to FileDialogueActivity called onActivityResult:
protected void onActivityResult(int requestCode, int resultCode, Intent data){ // See which child activity is calling us back. if (requestCode == REQUEST_PATH){ if (resultCode == RESULT_OK) { curFileName = data.getStringExtra("GetPath") + "/" +data.getStringExtra("GetFileName"); edittext.setText(curFileName); } } }
This is a method that we override from the base class Activity. As one of the parameters we receive the Intent instance that we previously created and populated with parameters.
Subsequently we can retrieve the parameters from the Intent instance and do something useful with it. In out case we create an absolute path and show it in the edit box of the File dialogue box.
Next, we should implement similar parameter passing between FrontActivity (e.g. the main activity of our emulator) and FileDialogueActivity. We will cover this in the next section.
Loading a tape image into Memory
In the previous section we have seen how to implement a file browser within an Android application.This whole exercise was just to the absolute path to tape image.
In this section we will be interfacing our FrontActivity to the file browser in order to hold of the absolute path to a tape image file and then load this file into memory.
We start off by adding an extra menu item for browsing for a tape image and implement an event handler for it within the FrontActivity:
@Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_stop) { switchToDebug = true; return true; } else if (id == R.id.action_attach) { Intent i = new Intent(this, FileDialogueActivity.class); startActivityForResult(i, 1); return true; } return super.onOptionsItemSelected(item); }
I will cover startActivityFoResult in a moment. Let us first have a look at what happens when you click the Attach button at which you will be be effectively leaving the File Dialogue:
public void onAttachClick(View v) { Intent intent = new Intent(); intent.putExtra("GetFullPath",curFileName); setResult(RESULT_OK, intent); finish(); }
As you see, we are passing the full path back to our FrontActivity.
Now, let us have a look at the implementation of startActivityFoResult within FrontActivity:
protected void onActivityResult(int requestCode, int resultCode, Intent data){ // See which child activity is calling us back. if (requestCode == 1){ if (resultCode == RESULT_OK) { String curFileName = data.getStringExtra("GetFullPath"); ... } } }
At this point in our code, we have a handle on the absolute path of the requested tape image file.
The question now is, what do we do with this absolute path?
The quick and easy answer is just open this file and load it into memory. One might be thinking that this is quite a waste. Shouldn't we rather just open the Tape image and read it bit by bit as required when the C64 system is loading the game from tape?
I have actually being pondering about this question for some time. However, in general these tape images is less than a megabyte and I don't think the price is too big to pay if you load the whole tape image into memory one shot.
To store the tape image in memory, we will again use a ByteBuffer because that will allow our native code to directly access the data from the buffer.
Here is the full code to load the tape image into memory:
... private ByteBuffer mTape; ... protected void onActivityResult(int requestCode, int resultCode, Intent data){ // See which child activity is calling us back. if (requestCode == 1){ if (resultCode == RESULT_OK) { String curFileName = data.getStringExtra("GetFullPath"); try { RandomAccessFile file = new RandomAccessFile(curFileName, "r"); FileChannel inChannel = file.getChannel(); long fileSize = inChannel.size(); mTape = ByteBuffer.allocateDirect((int) fileSize); inChannel.read(mTape); mTape.rewind(); inChannel.close(); file.close(); emuInstance.attachNewTape(mTape); } catch (Exception e) { e.printStackTrace(); } } } }
For some people the way I did file IO might look a bit weird. The reason why I decided to do it this way is because FileInputStream doesn't support reading file contents to a ByteBuffer object. So, instead I used the File I/O wrapped within the same package in which find ByteBuffer: java.nio. NIO, by the way, stands for New IO.
In the line emuInstance.attachNewTape() we actually pass our ByteBuffer to our native code. We will cover this in the next section.
Going Native
Now it is time to implement the native part of the code for the Tape emulation.The Tape Emulation functionality can be thought of as a glorified timer, triggering interrupts at rates specified within the Tape image file.
The bulk of the tape emulation functionality will be specified within a new file, tape.c. We start off by defining a function within this file for returning a timer_struct:
struct timer_struct getTapeInstance() { struct timer_struct mytape; mytape.expiredevent = &tape_pulse_expired; mytape.remainingCycles = 0x0; mytape.started = 0; mytape.interrupt = &interrupt_flag; return mytape; }
We will discuss the implementation of the methods tape_pulse_expired and interrupt_flag in a moment.
Like with our CIA timers, we will be instantiating a tape instance within memory.c:
... struct timer_struct tape_timer; ... void Java_com_johan_emulator_engine_Emu6502_memoryInit(JNIEnv* pEnv, jobject pObj) { timerA = getTimerInstanceA(); add_timer_to_list(&timerA); timerB = getTimerInstanceB(); add_timer_to_list(&timerB); tape_timer = getTapeInstance(); add_timer_to_list(&tape_timer); } ...
We create a tape instance, store it as a global instance in memory.c and we add it to a list of timers to be processed at regular intervals.
Let us next implement the functionality for storing the tape image pushed through by our Java Code. A bit of a snag here. One of the key things that should happen when we store the tape image, is to initialise tape_timer->remainingCycles with the first value of the tape image.
We can therefore not blindly send the buffer to tape.c since tape.c doesn't have a handle on tape_timer. We need to send the buffer via memory.c to tape.c.
So, within memory.c, add the following method:
void Java_com_johan_emulator_engine_Emu6502_attachNewTape(JNIEnv* pEnv, jobject pObj, jobject oBuf) { jbyte * tape_image = (jbyte *) (*pEnv)->GetDirectBufferAddress(pEnv, oBuf); attachNewTape(tape_image, &tape_timer); }
We implement attachNewTape within tape.c:
... jbyte* tape_image; int posInTape; ... void attachNewTape(jbyte* buffer, struct timer_struct *tdev) { tape_image = buffer; posInTape = 0x14; ... }
It should be noted that real data only starts at offset 0x14 within a tape image. Hence, when we store a new tape image, we immediately set the position within the array to 0x14.
At this point it maybe a good idea just to recap on the format of a .tap file. A .tap file is basically a list of pulse widths with units in number of cpu cycles. Let us say, for instance, we derive the following numbers from a tape image file:
400
300
500
This would induce the following sequence:
- After 400 cpu clock cycles interrupt the cpu
- After 300 cpu clock cycles interrupt the cpu
- After 500 cpu clock cycles interrupt the cpu
Within a .tap file there is also happening some amount of compression to reduce the size of the file. Each delay value is stored as a unit of 8 clock cycles. Without this scheme most delay values will take up 2 bytes. With the compression scheme it storage requirements shrink to one byte for the majority of delay samples.
The .tap format, however, do make provision for longer delays. Such a sample should start with the value zero. If a sample with value zero is encountered, the actual value will be contained in the next three bytes in a the low/high format. Therefore, with such a sample you can represent a value of up to 24 bits, equalling 16777216 clock cycles.
That is right, more than 16Million clock cycles! On a normal 1MHz 6502 CPU this would equal a pulse having a width round about 16 seconds. I doubt that you would ever encounter a C64 tape containing such a long pulse. Granted, if you have data tapes that pauses between data blocks, you might pulses around 1/4 of second when the tape motor speeds. 1/4 of a second would result in 250k clock cycles, for which you would need three bytes to represent.
Let us now get back to some coding. With our knowledge of the .tap format, we can now make the following adjustments to tape.c:
void update_remaining(struct timer_struct *tdev) { int temp = tape_image[posInTape]; if (temp != 0) { tdev->remainingCycles = temp << 3; posInTape++; } else { tdev->remainingCycles = tape_image[posInTape + 1] | (tape_image[posInTape + 2] << 8) | (tape_image[posInTape + 3] << 16); posInTape = posInTape + 4; } } void attachNewTape(jbyte* buffer, struct timer_struct *tdev) { tape_image = buffer; posInTape = 0x14; update_remaining(tdev); } void tape_pulse_expired(struct timer_struct *tdev) { update_remaining(tdev); interrupt_flag(); }
As you can see, when we attach a new tape image, we also initialise the remainingCycles member of the tape instance.
You will also see that I have implemented the method tape_pulse_expired method mentioned earlier. Within this method we invoke an interrupt and advance to the next sample.
interrupt_flag is a method that we still need to implement within interrupt.c, so let us do that quickly:
void interrupt_flag() { interrupts_occured = interrupts_occured | 16; }
The method name interrupt_flag might be a bit confusing. So, let me try and clear some possible confusion.
The cassette read line is connected to the FLAG interrupt pin of the CIA#1. From there the name interrupt_flag(). The interrupt pin is represented by bit#4 within the interrupts register. For that reason we are OR'ing interrupts_occured with 16 when this interrupt has occurred.
We have basically implemented the important bits of tape emulation. Once our tape_timer struct gets in the started state, our tape emulation code should work! The catch 22 here is though, in the current state there is no way to get the tape_timer struct into the started state.
There is a number of things we still need to implement in order to get the tape_timer struct into the started state.
The first thing is emulating Tape Sense. What is Tape sense? Well, basically when you get the prompt PRESS PLAY ON TAPE, the Kernal keeps looking at Bit 4 of memory location 1 to see if you did indeed pressed the Play button.
There is a couple of things we need to do in order for the Tape sense emulation to work. First thing is to add an extra menu item to the FrontActivity menu. When the user sees the message PRES PLAY ON TAPE, he/she can just select this menu option to simulate pressing play on tape.
We also need to implement some of the Tape sense functionality within tape.c:
... int playDown = 0; ... void Java_com_johan_emulator_engine_Emu6502_togglePlay() { playDown = !playDown; } ... int isPlayDownBit() { return (playDown != 0) ? 0 : 1; } ...
Each the user tap on the play menu item, togglePlay should be invoked.
Memory.c will call isPlayDownBit when it needs to know the state of bit 4 of memory location 1 (e.g. datasettte button status).
Next thing we should consider for tape emulation is motor control. The kernal is in charge of switching the cassette motor on or off. It will generally switch the motor on when it detects that the play button is down.
When the Kernal wishes to switch on the motor, it does so by setting bit 5 of memory location to zero. This bit should also be the queue for tape.c to start the tap timer.
For motor control, we implement the following methods within tape.c:
void setMotorOn(struct timer_struct *tdev, int motorBit) { tdev -> started = (motorBit == 0) ? 1 : 0; } int getMotorOnBit(struct timer_struct *tdev) { return (tdev -> started == 1) ? 0 : 1; }
Finally, let us do memory integration for tape sense and motor control. This will involve the following changes to memory.c:
jchar read_port_1() { jchar result = mainMem[1] & 0xcf; result = result | (getMotorOnBit(&tape_timer) << 5); result = result | (isPlayDownBit() << 4); return result; } void write_port_1(jchar value) { mainMem[1] = value; int motorStatus = (value & (1 << 5)) >> 5; setMotorOn(&tape_timer, motorStatus); } jchar memory_read(int address) { if (address == 1) return read_port_1(); else if ((address >=0xdc00) & (address < 0xdc10)) return cia1_read(address); else return mainMem[address]; } void memory_write(int address, jchar value) { if (((address >= 0xa000) && (address < 0xc000)) | ((address >= 0xe000) && (address < 0x10000))) return; if (address == 1) write_port_1(value); else if ((address >=0xdc00) & (address < 0xdc10)) cia1_write(address, value); else mainMem[address] = value; }
This is it for tape emulation!
Enlarging the screen
Before we test all the code we wrote in this post, I just want to discuss something else.As mentioned earlier on, the C64 320x200 pixel screen is quite small when displayed on a high resolution screen.
In this section I just want to show how easy it is to enlarge our screen in Android just with a few lines of code.
Firstly we need to adjust the dimensions of our surfaceview in the layout file of our frontactivity:
<com.johan.emulator.view.C64SurfaceView android:id="@+id/Video" android:layout_width="640px" android:layout_height="400px" />
And next, a small adjust in the code we use render the frames:
emuInstance.populateFrame(); mByteBuffer.rewind(); mBitmap.copyPixelsFromBuffer(mByteBuffer); canvas.save(); canvas.scale(1.5f, 1.5f); canvas.setDrawFilter(filter); canvas.drawBitmap(mBitmap,0,0, paint); canvas.restore(); holder.unlockCanvasAndPost(canvas);
The canvas performs the scaling by means of a translation matrix, very much the same way OpenGL works with 3D graphics.
canvas.save saves any previously active matrix.
canvas.scale applies a transformation matrix that in effect map the pixels of our 320x200 pixel bitmap to a larger area. When he canvas eventually does the drawing, it apples our transformation matrix.
Finally, canvas.restore reverts to the previous matrix.
Testing
Time that we test all our code changes.I used again a tape image to test with.
We see that our emulator did in fact find Dan Dare. So our Tape emulation works!
We are, however, not in a postion at he moment to see the flashing borders and splash screen that gets shown while the game loads. We will tackle this in the next post.
In Summary
In this chapter we have implemented tape emulation.We have also enlarged the output of our screen.
In the next post we will be jacking up our video output functionality. This is so that we can view the flashing borders and splash screen while the game loads.
Till next time!
No comments:
Post a Comment