Foreword
In the previous post we managed to boot the C64 system with its ROMs.Our Android C64 emulator, however, didn't have a emulated screen in which we could see the boot message. We had to manually inspect the contents of screen memory to confirm that the boot message was written.
In this section we are going to develop the emulated screen. This is going to be a very simple emulated screen having just the capability to show textmode graphics in monochrome.
Our current Android app currently consists out of a single page showing debug information. Now, we could add our emulated screen to this page, but this will cause our page to look very squashed.
A better alternative would be to move the emulated screen into a page of its own.
Therefore, in this post we will be developing a two page solution and cover the implications of doing it this way.
Process Flow
Let us start off by first looking on a high level what we want to achieve in this post.When starting our Android application, we would like to show the emulated screen as the first page of the application:
At any point in time we can decide that we want to pause our emulator and inspect the state of it. This pause action can be activated by the user by means of a menu action which will pause execution and move to the Debug page.
In our application the menu will be opened by pressing the three dots, as I have highlighted in the screenshot above. The menu will typically look as follows:
Hitting Stop, will pause execution and bring up the Debug Page:
Within this screen you can step and inspect in the same way as is previous posts.
Hitting RUN will take you back to the emulated screen and resume execution.
Creating emulated screen page
Let us now proceed to create the emulated screen page.The first step would be to create a new activity. So within Android Studio, Select File/New/Activity/Empty Activity. Go through the set of wizard pages keeping the defaults for each one. When prompted for an activity name, specify FrontActivity.
One of the items created by the Empty Activity wizard is a overflow menu, similar is the own highlighted in the previous section.
Let us now add a item to the menu allowing you to pause emulator execution. Within the app folder structure, under res/menu, create a file named menu_front.xml. Contents of this file should be as follows:
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="com.johan.emulator.activities.FrontActivity"> <item android:id="@+id/action_stop" android:orderInCategory="100" android:title="Stop" app:showAsAction="never" /> </menu>
Now, within FrontActivity, add the following method:
@Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_front, menu); return true; }
In effect we are overriding the onCreateOptionsMenu method of the Activity base class.
You will also see that for the inflate method, we are passing R.menu.menu_front as one of the parameters. This is a points to the xml file we have created earlier on. By convention the Android Build system knows by convention that all xml files within res/menu should be treated as property files of menus.
We haven't implemented an on tap event yet for this menu item. We will do so in another section.
With our application containing two activities, one might be tempted to wonder how the Android knows which Activity should be shown first on startup.
The answer lies within the AndroidManifest.xml file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.johan.emulator"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".activities.FrontActivity" android:label="@string/title_activity_front" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".activities.DebugActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"></activity> </application> </manifest>
You will see that one the activities have an intent-filter element, as highlighted. So, in short, the activity with an intent-filter element, as shown above, will be the activity that Android will first show when the application has started.
Refactoring
With our application containing two activities, one should ensure that both of them should access the same emulator state.This may sound that I am stating the obvious, but this is indeed a brain teaser for our situation.
As our emulator stands in its current state, all of the emulator state is contained within our Debug Activity. The problem here is that within Android, Activity instances is not aware aware of each other's existence. So, as it stands, the two activities would not to be able to access common emulator state.
A solution to this issue would be to move all the emulator functionality out of the DebugActivity into a class of its own. This will involve a bit of refactoring which I will explain in this section.
We start of by creating a class called Emu6502. As explained we will move all the emulator functionality into this class. This includes things like getting debug strings of emulator state and native method stub. I will not go into very deep detail into this class, but I will highlight one thing though:
public class Emu6502 { ... private static Emu6502 emu6502Instance = null; ... protected Emu6502() { } ... static public Emu6502 getInstance(AssetManager mgr) { if (emu6502Instance != null) return emu6502Instance; loadROMS(mgr); resetCpu(); emu6502Instance = new Emu6502(); return emu6502Instance; } .. }
Here I have hidden the Object constructor and the only way to get an instance of the class Emu6502 is via the method getInstance. This method will only create an instance once the first time. For all subsequent calls to getInstance, the same instance will be returned.
This methodology is called a singleton and is perfect for our brain teaser. Both our FrontActivity and Debug Activity can call this method and we can be sure they oth share the same emulator instance.
As part of our refactoring, let us have a look again at FrontActivity, with a couple of changes applied:
public class FrontActivity extends AppCompatActivity { Timer timer; TimerTask timerTask; private boolean running = false; private boolean switchToDebug = false; final Handler handler = new Handler(); private Emu6502 emuInstance; //Emu6502.getInstance(getResources().getAssets()); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_front); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); emuInstance = Emu6502.getInstance(getResources().getAssets()); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } }); } @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; } return super.onOptionsItemSelected(item); } @Override protected void onPause() { super.onPause(); running = false; } @Override protected void onResume() { super.onResume(); running = true; switchToDebug = false; timer = new Timer(); //breakAddress = getBreakAddress(); timerTask = new TimerTask() { @Override public void run() { final int result = emuInstance.runBatch(0); if (result > 0) { handler.post(new Runnable() { @Override public void run() { timer.cancel(); doAlert(result); } }); } else if (!running | (result < 0) | switchToDebug) { handler.post(new Runnable() { @Override public void run() { timer.cancel(); running = false; } }); } if (switchToDebug) { Intent i = new Intent(FrontActivity.this, DebugActivity.class); FrontActivity.this.startActivity(i); } } }; timer.schedule(timerTask, 20, 20); } }
In the onCreate method we get an Emu6502 instance and store it as a private variable.
You will also realise that I have moved all the timer scheduling code within the Debug Activity to this activity.
You will also realise that I have implemented the methods onPause and onResume, which are in fact overidden methods of Activity.
Let us pause for moment and just discuss the purpose of the onPause and onResume methods.
The onPause method gets invoked when the current activity is entering the paused state. An activity enters the pause state when either switching to another activity or going to the home screen.
One thing to keep in mind with paused activities is that timer threads of paused activities will still run in the background. So, when a activity gets paused, it is important to also stop its timers within the onPause method.
As an Activity can get paused, similarly it can also be re-activated. The process of activity reactivation is called Resuming within the Android jargon. As you might have guest, the onResume method of an activity will be called when it resumes.
You might recognise the running variable from the previous post as a very important variable that is used to decide when a timer should be stopped. In this refactoring exercise we introduce yet another boolean variable called switchToDebug. This variable is set to true when you tap the STOP menu item.
The question at this point in time would be why define two booleans for more or less the same purpose. The answer is that the FrontActivity can pause for two reasons:
- The user hit stop and we need to go to the Debug Activity
- The user hit the Home button. In this scenario we would not to transition to the Debug Activity
So, the switchToDebug variable is just an indication when we should really be switching to the DebugActivity.
The actual switching to the DebugActivity happens in the following if statement:
if (switchToDebug) { Intent i = new Intent(FrontActivity.this, DebugActivity.class); FrontActivity.this.startActivity(i); }
Drawing
Let us spend some time now on the actual drawing process of the emulated screen.To facilitate the drawing we need to define a dedicated drawing element on the FrontActivity.
The type of element we will be using for this purpose will be a SurfaceView. The nice thing about SurfaceViews is that the draw commands is not processed by the GUI event queue, so it can give you better performance!
To use SurfaceView, we first need to create a class that subclass from SurfaceView:
public class C64SurfaceView extends SurfaceView { private SurfaceHolder surfaceHolder; public C64SurfaceView(Context context) { super(context); init(); } public SurfaceHolder getCreatedHolder() { return surfaceHolder; } public C64SurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public C64SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { surfaceHolder= getHolder(); surfaceHolder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }); } }
Next, we should add an element to content_front.xml, the resource file associated with the FrontActivity:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.johan.emulator.activities.FrontActivity" tools:showIn="@layout/activity_front"> <com.johan.emulator.view.C64SurfaceView android:id="@+id/Video" android:layout_width="320px" android:layout_height="200px" /> </RelativeLayout>
So, when an instance is created of FrontActivity, an instance of C64SurfaceView will automatically be added to it, having dimensions 320 by 200 pixels, the dimensions of the C64 screen.
We have now defined an area on the screen of the FrontActivity on which we can draw. So the question is: How do we do the actual drawing on this area?
Let us do this question step for step.
Firstly, we need to create the following private variables within FrontActivity:
private ByteBuffer mByteBuffer; private Bitmap mBitmap;
The purpose of these two variables will become clear in a moment.
Let us proceed to initialize these variables within onCreate:
mByteBuffer = ByteBuffer.allocateDirect(320*200*2); mBitmap = Bitmap.createBitmap(320,200, Bitmap.Config.RGB_565); emuInstance.setFrameBuffer(mByteBuffer);
mByteBuffer defines a fixed area in memory defining pixel data to be displayed. Native code can directly write to this area that can dramatically improve performance. The format for each pixel in the buffer is 5 bits for red, 6 bits for green and 5 bits for blue. This equals two bytes per pixel.
We give our native code a handle to this buffer by calling setFrameBuffer on emuInstance. This is a native method stub within EmuInstance:
public native void setFrameBuffer(ByteBuffer buf);
The implementation of this native stub lives within memory.c:
... jchar* g_buffer; ... void Java_com_johan_emulator_engine_Emu6502_setFrameBuffer(JNIEnv* pEnv, jobject pObj, jobject oBuf) { g_buffer = (jchar *) (*pEnv)->GetDirectBufferAddress(pEnv, oBuf); }
The buffer pointer being passed to this method is a java Object reference. To get the physical memory address, you need to call the GetDirectBufferAddress method on the JNI environment.
You will notice that although a ByteBuffer is an actual fact an array of byte, I am casting it as an array of char, which is an array with two bytes per element.
This just makes working with individual pixels so much easier and you don't need to worry if the underlining architecture store the two bytes per pixel in Big Endian format, or not.
While at the native code, let us look at the rest rest of the native code related to drawing. After that we will return to the Java Code.
First important thing for native code, is to load character ROM into memory:
... jchar charRom[4096]; ... void Java_com_johan_emulator_engine_Emu6502_loadROMS(JNIEnv* env, jobject pObj, jobject pAssetManager) { ... assetF = AAssetManager_open(assetManager, "characters.bin", AASSET_MODE_UNKNOWN); AAsset_read(assetF, buffer, 4096); for (i = 0x0; i < 0x1000; i++) { charRom[i] = buffer[i]; } AAsset_close(assetF); }
Next up, is to write a method for populating the ByteBuffer:
void Java_com_johan_emulator_engine_Emu6502_populateFrame(JNIEnv* pEnv, jobject pObj) { int currentLine; int currentCharInLine; int currentPixel; int currentPosInCharMem = 1024; int posInBuffer = 0; for (currentLine = 0; currentLine < 200; currentLine++) { int currentLineInChar = currentLine & 7; if ((currentLine != 0) && ((currentLineInChar) == 0)) currentPosInCharMem = currentPosInCharMem + 40; for (currentCharInLine = 0; currentCharInLine < 40; currentCharInLine++) { jchar currentChar = mainMem[currentPosInCharMem + currentCharInLine]; jchar dataLine = charRom[(currentChar << 3) | currentLineInChar]; int posToWriteBegin = posInBuffer + (currentCharInLine << 3); int posToWrite; for (posToWrite = posToWriteBegin; posToWrite < (posToWriteBegin + 8); posToWrite++) { if ((dataLine & 0x80) != 0) g_buffer[posToWrite] = 0xffff; else g_buffer[posToWrite] = 0x0; dataLine = dataLine << 1; } } posInBuffer = posInBuffer + 320; } }
So, the buffer gets drawn to with the aid of the screen memory, starting at address 1024 and the character ROM.
This method gets called once by the Java code every time when runBatch gets invoked.
This concludes the native part of drawing.
Back to the Java code. In the onResume() method, we add the following code:
@Override protected void onResume() { super.onResume(); running = true; switchToDebug = false; timer = new Timer(); timerTask = new TimerTask() { @Override public void run() { final int result = emuInstance.runBatch(0); C64SurfaceView surfaceView = (C64SurfaceView) findViewById(R.id.Video); SurfaceHolder holder = surfaceView.getCreatedHolder(); Canvas canvas = null; if (holder != null) canvas = holder.lockCanvas(); if (canvas != null) { emuInstance.populateFrame(); mByteBuffer.rewind(); mBitmap.copyPixelsFromBuffer(mByteBuffer); canvas.drawBitmap(mBitmap,0,0, null); holder.unlockCanvasAndPost(canvas); ... }
We get the SurfaceView by ID and subsequently we get the SurfaceHolder and then canvas. You will see a couple of null checks along the way. This just to ensure that everything is properly created within the surface view before we do anything.
When we are sure we have a valid canvas, we call populateFrame(), which we described previously. We then copy this buffer to Bitmap object, which we then use to draw to the canvas.
You might find the call to rewind on the ByteBuffer a bit strange. the copyPixelsFromBuffer almost treat the ByteBuffer as file. Everytime it gets information from the buffer, it keeps track where it last ended within the buffer. So, when this method reads from the buffer a second time, it will not start at the beginnining, but just after the place it ended off the first time. For that reason it is nesseary to rewind() each time.
Implementing the flashing cursor
It could be nice if we could end off this post showing the flashing cursor as well within our Surface view together with the boot message.To implement the flashing cursor, I am going to use the same hack I used JavaScript emulator here. This hack involves simulating an IRQ after each execution of runBatch.
So, first we implement the following method within cpu.c:
void Java_com_johan_emulator_engine_Emu6502_interruptCpu(JNIEnv* pEnv, jobject pObj) { if (interruptFlag == 1) return; pushWord(pc); breakFlag = 0; Push(getStatusFlagsAsByte()); breakFlag = 1; interruptFlag = 1; int tempVal = memory_read(0xffff) * 256; tempVal = tempVal + memory_read(0xfffe); pc = tempVal; }
This is a method we can call to simulate an interrupt. We need to call this method within the OnResume method of FrontActivity:
@Override protected void onResume() { super.onResume(); running = true; switchToDebug = false; timer = new Timer(); //breakAddress = getBreakAddress(); timerTask = new TimerTask() { @Override public void run() { final int result = emuInstance.runBatch(0); emuInstance.interruptCpu(); ...
A Test Run
Let us do a quick test run.Everything looks perfect, except the number of bytes available:
I actually had the same issue when I developed my JavaScript emulator. The way it is calculating this number involves testing memory from a very early location in memory, just after screen memory. For each location it writes a value to RAM and then check if it gets the same value back. This process stops when it gets to the beginning of BASIC ROM, where the check will obviously will fail, so the number of free bytes will in effect be the number of bytes tested successfully.
This is just where our emulator will fall on its face: Memory tests in the ROM areas will succeed!
So, to get the free byte count right, we need to ignore writes within the ROM area. For now, this is just going to be a quick hack in memory.c:
void memory_write(int address, jchar value) { if (((address >= 0xa000) && (address < 0xc000)) | ((address >= 0xe000) && (address < 0x10000))) return; mainMem[address] = value; }
This will do the trick!
In Summary
In this post we have implemented an emulated screen for our emulator.In the next post we will be implementing a virtual keyboard that will allow us to interact with the emulator.
Till next time!
Thanks Johan!
ReplyDelete