package mars.tools import mars.Globals import mars.detectRadix import mars.mips.hardware.* import mars.toHex import mars.util.Binary import java.awt.* import java.awt.BorderLayout.* import java.awt.event.KeyEvent import java.awt.event.KeyListener import java.util.* import javax.swing.* import javax.swing.border.EmptyBorder import kotlin.collections.ArrayList import kotlin.collections.HashSet /* Copyright (c) 2010-2011, Pete Sanderson and Kenneth Vollmar Developed by Pete Sanderson (psanderson@otterbein.edu) and Kenneth Vollmar (kenvollmar@missouristate.edu) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. (MIT license, http://www.opensource.org/licenses/mit-license.html) */ /** * Bitmapp display simulator. It can be run either as a stand-alone Java application having access to the mars package, * or through MARS as an item in its Tools menu. It makes maximum use of methods inherited from its abstract superclass * AbstractMarsToolAndApplication. Pete Sanderson, verison 1.0, 23 December 2010. */ class BitmapDisplay : AbstractMarsToolAndApplication { // Major GUI components private lateinit var uiBaseAddressSelector: JComboBox private lateinit var uiKeyboardCheckbox: JCheckBox private lateinit var canvas: JPanel private lateinit var results: JPanel // Some GUI settings private val emptyBorder = EmptyBorder(4, 4, 4, 4) private val backgroundColor = Color.WHITE // Values for display canvas. Note their initialization uses the identifiers just above. private var unitWidth: Int = 2 private var unitHeight: Int = 2 private var displayWidth: Int = 512 private var displayHeight: Int = 256 private val displayDimension get() = Dimension(displayWidth, displayHeight) private val baseAddresses = intArrayOf(Memory.dataSegmentBaseAddress, Memory.globalPointer, Memory.dataBaseAddress, Memory.heapBaseAddress, Memory.memoryMapBaseAddress) private val baseAddress get() = baseAddresses[uiBaseAddressSelector.selectedIndex] // Keyboard++ private var keyboardAddr: UInt = 0xFFFF0010u private var oldKeyboardAddr: UInt = 0xFFFF0000u private var oldKeyboardLastPressed: Char = ' ' private var keyboardAttached = false private lateinit var uiKeyboard: JTextField private lateinit var grid: Grid val pooledKeyEvents = hashMapOf>( 0x00u to ArrayList(), 0x10u to ArrayList(), 0x20u to ArrayList() ) val downKeys = HashSet() /** * Simple constructor, likely used to run a stand-alone bitmap display tool. * * @param title String containing title for title bar * @param heading String containing text for heading shown in upper part of window. */ constructor(title: String, heading: String) : super(title, heading) constructor() : super("Azalea's Bitmap Display++", HEADING) override fun getName() = HEADING /** * Override the inherited method, which registers us as an Observer over the static data segment (starting address * 0x10010000) only. This version will register us as observer over the the memory range as selected by the base * address combo box and capacity of the visualization display (number of visualization elements times the number of * memory words each one represents). It does so by calling the inherited 2-parameter overload of this method. If * you use the inherited GUI buttons, this method is invoked when you click "Connect" button on MarsTool or the * "Assemble and Run" button on a Mars-based app. */ override fun addAsObserver() { var highAddress: Int = baseAddress + grid.rows * grid.cols * Memory.WORD_LENGTH_BYTES // Special case: baseAddress<0 means we're in kernel memory (0x80000000 and up) and most likely // in memory map address space (0xffff0000 and up). In this case, we need to make sure the high address // does not drop off the high end of 32 bit address space. Highest allowable word address is 0xfffffffc, // which is interpreted in Java int as -4. if (baseAddress < 0 && highAddress > -4) { highAddress = -4 } addAsObserver(baseAddress, highAddress) addAsObserver(keyboardAddr.toInt(), (keyboardAddr + 0x20u).toInt()) } /** * Method that constructs the main display area. It is organized vertically into two major components: the display * configuration which an be modified using combo boxes, and the visualization display which is updated as the * attached MIPS program executes. * * @return the GUI component containing these two areas */ override fun buildMainDisplayArea(): JComponent { results = JPanel() results.add(buildOrganizationArea()) results.add(buildVisualizationArea()) return results } ////////////////////////////////////////////////////////////////////////////////////// // Rest of the protected methods. These override do-nothing methods inherited from // the abstract superclass. ////////////////////////////////////////////////////////////////////////////////////// /** * Update display when connected MIPS program accesses (data) memory. * * @param memory the attached memory * @param ac information provided by memory in MemoryAccessNotice object */ override fun processMIPSUpdate(memory: Observable, ac: AccessNotice) { if (ac !is MemoryAccessNotice || ac.accessType != AccessNotice.WRITE) return val addr = ac.address.toUInt() // For the keyboard++ if (addr in keyboardAddr..keyboardAddr + 0x30u) { // Offset 0x01 or 0x11 or 0x21 bytes are for telling the keyboard that the events are received var offset = addr - keyboardAddr // Clear the segment if (offset == 0x01u || offset == 0x11u) { if (Globals.memory.getByte(addr.toInt()) != 1) return println("Offset written: ${offset.toHex(2)}") // Clear bytes if 1 is written to the 0x01 offset offset -= 1u val range = offset..offset + 0x0Fu synchronized(Globals.memoryAndRegistersLock) { range.map { keyboardAddr + it }.forEach { Globals.memory.setByte(it.toInt(), 0) } } pooledKeyEvents[offset]!!.clear() println("[Keyboard++] Keyboard segment range $range cleared") } return } // For the display val value = ac.value val offset = ((addr - baseAddress.toUInt()) / 4u).toInt() try { grid.setElement(offset / grid.cols, offset % grid.cols, value) } catch (e: IndexOutOfBoundsException) { // If address is out of range for display, do nothing. } if (offset == 0) canvas.repaint() } /** * Event on key press * * @param offset Event type / offset * @param e Event */ fun keyEvent(offset: UInt, e: KeyEvent) { if (!keyboardAttached) return println("[Keyboard++] ${e.id} '${e.keyChar}' ${e.keyCode}") val queue = pooledKeyEvents[offset]!! // Old keyboard compatibility synchronized(Globals.memoryAndRegistersLock) { if (e.id == KeyEvent.KEY_PRESSED) { oldKeyboardLastPressed = e.keyChar Globals.memory.setWord(oldKeyboardAddr.toInt(), 1) Globals.memory.setWord((oldKeyboardAddr + 4u).toInt(), e.keyChar.code) } if (e.id == KeyEvent.KEY_RELEASED && e.keyChar == oldKeyboardLastPressed) { oldKeyboardLastPressed = 0.toChar() Globals.memory.setWord(oldKeyboardAddr.toInt(), 0) Globals.memory.setWord((oldKeyboardAddr + 4u).toInt(), 0) } } // Check for more than 7 key events queued if (queue.size == 7) return // Add to queue queue.add(e) // Add to memory var addr = keyboardAddr + offset println("[Keyboard++] Address ${addr.toHex(8)} set to ${queue.size} | ${addr.toHex(8)} set to ${e.keyCode} (${e.keyChar.toHex()})") synchronized(Globals.memoryAndRegistersLock) { // Change 0x0: Number of events Globals.memory.setByte(addr.toInt(), queue.size) // Set the keycode addr += queue.size.toUInt() * 2u Globals.memory.setHalf(addr.toInt(), e.keyCode) } } /** * Initialize all JComboBox choice structures not already initialized at declaration. Overrides inherited method * that does nothing. */ override fun initializePreGUI() { grid = Grid(0, 0) } /** * The only post-GUI initialization is to create the initial Grid object based on the default settings of the * various combo boxes. Overrides inherited method that does nothing. */ override fun initializePostGUI() { grid = createNewGrid() } /** * Method to reset counters and display when the Reset button selected. Overrides inherited method that does * nothing. */ override fun reset() { grid.reset() canvas.repaint() } /** * Overrides default method, to provide a Help button for this tool/app. */ override fun getHelpComponent(): JComponent { val helpContent = """ Use this program to simulate a basic bitmap display where each memory word in a specified address space corresponds to one display pixel in row-major order starting at the upper left corner of the display. This tool may be run either from the MARS Tools menu or as a stand-alone application. You can easily learn to use this small program by playing with it! Each rectangular unit on the display represents one memory word in a contiguous address space starting with the specified base address. The value stored in that word will be interpreted as a 24-bit RGB color value with the red component in bits 16-23, the green component in bits 8-15, and the blue component in bits 0-7. Each time a memory word within the display address space is written by the MIPS program, its position in the display will be rendered in the color that its value represents. Version 1.0 is very basic and was constructed from the Memory Reference Visualization tool's code. Feel free to improve it and send me your code for consideration in the next MARS release. Contact Pete Sanderson at psanderson@otterbein.edu with questions or comments. """.trimIndent() val help = JButton("Help") help.addActionListener { JOptionPane.showMessageDialog(theWindow, helpContent) } return help } ////////////////////////////////////////////////////////////////////////////////////// // Private methods defined to support the above. ////////////////////////////////////////////////////////////////////////////////////// // UI components and layout for left half of GUI, where settings are specified. private fun buildOrganizationArea(): JComponent { val uiUnitWidthSelector = JComboBox(arrayOf(1, 2, 4, 8, 16, 32)).apply { isEditable = false background = backgroundColor selectedIndex = 1 toolTipText = "Width in pixels of rectangle representing memory word" addActionListener { unitWidth = getInt() grid = createNewGrid() reset() } } val uiUnitHeightSelector = JComboBox(arrayOf(1, 2, 4, 8, 16, 32)).apply { isEditable = false background = backgroundColor selectedIndex = 1 toolTipText = "Height in pixels of rectangle representing memory word" addActionListener { unitHeight = getInt() grid = createNewGrid() reset() } } val uiWidthSelector = JComboBox(arrayOf(64, 128, 256, 512, 1024)).apply { isEditable = false background = backgroundColor selectedIndex = 3 toolTipText = "Total width in pixels of display area" addActionListener { displayWidth = getInt() canvas.preferredSize = displayDimension canvas.size = displayDimension grid = createNewGrid() reset() } } val uiHeightSelector = JComboBox(arrayOf(64, 128, 256, 512, 1024)).apply { isEditable = false background = backgroundColor selectedIndex = 2 toolTipText = "Total height in pixels of display area" addActionListener { displayHeight = getInt() canvas.preferredSize = displayDimension canvas.size = displayDimension grid = createNewGrid() reset() } } val descriptions = arrayOf("global data", "\$gp", "static data", "heap", "memory map") val displayBaseAddressChoices = baseAddresses .mapIndexed { i, it -> "${Binary.intToHexString(it)} (${descriptions[i]})" }.toTypedArray() uiBaseAddressSelector = JComboBox(displayBaseAddressChoices).apply { isEditable = false background = backgroundColor selectedIndex = 1 toolTipText = "Base address for display area (upper left corner)" addActionListener { // If display base address is changed while connected to MIPS (this can only occur // when being used as a MarsTool), we have to delete ourselves as an observer and re-register. if (connectButton != null && connectButton.isConnected) { deleteAsObserver() addAsObserver() } grid = createNewGrid() reset() } } val uiKeyboardAddress = JTextField(keyboardAddr.toHex(8)).apply { addActionListener { text.toUIntOrNull(text.detectRadix())?.let { keyboardAddr = it uiKeyboardCheckbox.isSelected = false } } } val uiKeyboardCheckbox = JCheckBox("Attach Keyboard++", keyboardAttached).apply { addActionListener { keyboardAttached = isSelected if (isSelected) { deleteAsObserver() addAsObserver() } pooledKeyEvents.values.forEach { it.clear() } } } println("Bitmap Display++ Initialized") // Register key listener KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher { if (!(keyboardAttached && uiKeyboard.hasFocus())) return@addKeyEventDispatcher false when (it.id) { KeyEvent.KEY_PRESSED -> { // Keep track of down keys to avoid the system's key repeat if (!downKeys.contains(it.keyCode)) { keyEvent(0x00u, it) downKeys.add(it.keyCode) } } KeyEvent.KEY_RELEASED -> { keyEvent(0x10u, it) downKeys.remove(it.keyCode) } } true } uiKeyboard = JTextField(2).apply { class KL : KeyListener { override fun keyTyped(e: KeyEvent) { uiKeyboard.text = "" } override fun keyPressed(e: KeyEvent) { uiKeyboard.text = "" // keyEvent(0x0u, e) } override fun keyReleased(e: KeyEvent) { uiKeyboard.text = "" // keyEvent(0x10u, e) } } addKeyListener(KL()) } // Lay 'em out in the grid... return JPanel(GridLayout(8, 1)).apply { add(getPanelWithBorderLayout().apply { add(JLabel("Unit Width in Pixels "), WEST) add(uiUnitWidthSelector, EAST) }) add(getPanelWithBorderLayout().apply { add(JLabel("Unit Height in Pixels "), WEST) add(uiUnitHeightSelector, EAST) }) add(getPanelWithBorderLayout().apply { add(JLabel("Display Width in Pixels "), WEST) add(uiWidthSelector, EAST) }) add(getPanelWithBorderLayout().apply { add(JLabel("Display Height in Pixels "), WEST) add(uiHeightSelector, EAST) }) add(getPanelWithBorderLayout().apply { add(JLabel("Base address for display "), WEST) add(uiBaseAddressSelector, EAST) }) add(getPanelWithBorderLayout().apply { add(uiKeyboardCheckbox, WEST) add(uiKeyboardAddress, EAST) }) add(getPanelWithBorderLayout().apply { add(JLabel("To use keyboard, put your cursor here >"), WEST) add(uiKeyboard) }) } } // UI components and layout for right half of GUI, the visualization display area. private fun buildVisualizationArea(): JComponent { canvas = GraphicsPanel() canvas.preferredSize = displayDimension canvas.toolTipText = "Bitmap display area" return canvas } private fun JComboBox.getInt() = selectedItem!!.toString().toInt() // Use this for consistent results. private fun getPanelWithBorderLayout(): JPanel = JPanel(BorderLayout(2, 2)).apply { border = emptyBorder } // Method to determine grid dimensions based on current control settings. // Each grid element corresponds to one visualization unit. private fun createNewGrid(): Grid { val rows = displayHeight / unitHeight val columns = displayWidth / unitWidth return Grid(rows, columns) } ////////////////////////////////////////////////////////////////////////////////////// // Specialized inner classes for modeling and animation. ////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // Class that represents the panel for visualizing and animating memory reference // patterns. private inner class GraphicsPanel : JPanel() { // override default paint method to assure display updated correctly every time // the panel is repainted. override fun paint(g: Graphics) = paintGrid(g, grid) // Paint the color codes. private fun paintGrid(g: Graphics, grid: Grid) { var upperLeftX = 0 var upperLeftY = 0 for (i in 0 until grid.rows) { for (j in 0 until grid.cols) { g.color = grid.getElementFast(i, j) g.fillRect(upperLeftX, upperLeftY, unitWidth, unitHeight) upperLeftX += unitWidth // faster than multiplying } // get ready for next row... upperLeftX = 0 upperLeftY += unitHeight // faster than multiplying } } } //////////////////////////////////////////////////////////////////////// // Represents grid of colors private class Grid(rows: Int, columns: Int) { var grid: Array> var rows: Int var cols: Int init { grid = Array(rows) { arrayOfNulls(columns) } this.rows = rows this.cols = columns reset() } // Returns value in given grid element; null if row or column is out of range. private fun getElement(row: Int, column: Int): Color? { return if (row in 0..rows && column >= 0 && column <= cols) grid[row][column] else null } // Returns value in given grid element without doing any row/column index checking. // Is faster than getElement but will throw array index out of bounds exception if // parameter values are outside the bounds of the grid. fun getElementFast(row: Int, column: Int): Color? { return grid[row][column] } // Set the grid element. fun setElement(row: Int, column: Int, color: Int) { grid[row][column] = Color(color) } // Set the grid element. private fun setElement(row: Int, column: Int, color: Color) { grid[row][column] = color } // Just set all grid elements to black. fun reset() { for (i in 0 until rows) { for (j in 0 until cols) { grid[i][j] = Color.BLACK } } } } companion object { private const val VERSION = "Version 1.0" private const val HEADING = "Bitmap Display++" /** * Main provided for pure stand-alone use. Recommended stand-alone use is to write a driver program that * instantiates a Bitmap object then invokes its go() method. "stand-alone" means it is not invoked from the MARS * Tools menu. "Pure" means there is no driver program to invoke the application. */ @JvmStatic fun main(args: Array) { BitmapDisplay("Bitmap Display stand-alone, " + VERSION, HEADING).go() } } }