package razorvine.c64emu import razorvine.ksim65.components.MemoryComponent import java.awt.* import java.awt.image.BufferedImage import java.awt.event.* import java.awt.image.ByteLookupTable import java.awt.image.LookupOp import java.io.CharConversionException import java.util.* import javax.swing.* import javax.swing.Timer /** * Define the C64 character screen matrix: 320x200 pixels, * 40x25 characters (of 8x8 pixels), and a colored border. */ object ScreenDefs { const val SCREEN_WIDTH_CHARS = 40 const val SCREEN_HEIGHT_CHARS = 25 const val SCREEN_WIDTH = SCREEN_WIDTH_CHARS * 8 const val SCREEN_HEIGHT = SCREEN_HEIGHT_CHARS * 8 const val DISPLAY_PIXEL_SCALING: Double = 2.0 val colorPalette = listOf( // this is Pepto's Commodore-64 palette http://www.pepto.de/projects/colorvic/ Color(0x000000), // 0 = black Color(0xFFFFFF), // 1 = white Color(0x813338), // 2 = red Color(0x75cec8), // 3 = cyan Color(0x8e3c97), // 4 = purple Color(0x56ac4d), // 5 = green Color(0x2e2c9b), // 6 = blue Color(0xedf171), // 7 = yellow Color(0x8e5029), // 8 = orange Color(0x553800), // 9 = brown Color(0xc46c71), // 10 = light red Color(0x4a4a4a), // 11 = dark grey Color(0x7b7b7b), // 12 = medium grey Color(0xa9ff9f), // 13 = light green Color(0x706deb), // 14 = light blue Color(0xb2b2b2) // 15 = light grey ) } private class BitmapScreenPanel(val chargenData: ByteArray, val ram: MemoryComponent) : JPanel() { private val image = BufferedImage(ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT, BufferedImage.TYPE_INT_ARGB) private val g2d = image.graphics as Graphics2D private val normalCharacters = loadCharacters(false) init { val size = Dimension( (image.width * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(), (image.height * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt() ) minimumSize = size maximumSize = size preferredSize = size isFocusable = true requestFocusInWindow() } private fun loadCharacters(shifted: Boolean): Array { val chars = Array(256) { BufferedImage(8, 8, BufferedImage.TYPE_BYTE_BINARY) } val offset = if(shifted) 256*8 else 0 // val color = ScreenDefs.colorPalette[14].rgb for(char in 0..255) { for(line in 0..7) { val charbyte = chargenData[offset + char*8 + line].toInt() for(x in 0..7) { if(charbyte and (0b10000000 ushr x) !=0 ) chars[char].setRGB(x, line, 0xffffff) } } } return chars } override fun paint(graphics: Graphics?) { redrawCharacters() val g2d = graphics as Graphics2D? g2d!!.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF) g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE) g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) g2d.drawImage( image, 0, 0, (image.width * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(), (image.height * ScreenDefs.DISPLAY_PIXEL_SCALING).toInt(), null ) } private fun redrawCharacters() { val screen = 0x0400 val colors = 0xd800 g2d.background = ScreenDefs.colorPalette[ram[0xd021].toInt()] g2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT) for(y in 0 until ScreenDefs.SCREEN_HEIGHT_CHARS) { for(x in 0 until ScreenDefs.SCREEN_WIDTH_CHARS) { val char = ram[screen + x + y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt() val color = ram[colors + x + y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt() drawColoredChar(x, y, char, color and 15) } } } private val coloredCharacters = mutableMapOf, BufferedImage>() private fun drawColoredChar(x: Int, y: Int, char: Int, color: Int) { var cached = coloredCharacters[Pair(char, color)] if(cached==null) { cached = normalCharacters.get(char) val colored = g2d.deviceConfiguration.createCompatibleImage(8, 8, BufferedImage.BITMASK) val sourceRaster = cached.raster val coloredRaster = colored.raster val pixelArray = IntArray(4) val javaColor = ScreenDefs.colorPalette[color] val coloredPixel = listOf(javaColor.red, javaColor.green, javaColor.blue, javaColor.alpha).toIntArray() for(y in 0..7) { for(x in 0..7) { val source = sourceRaster.getPixel(x, y, pixelArray) if(source[0]!=0) { coloredRaster.setPixel(x, y, coloredPixel) } } } coloredCharacters[Pair(char, color)] = colored cached = colored } g2d.drawImage(cached, x * 8, y * 8, null) } } class MainWindow(title: String, chargenData: ByteArray, val ram: MemoryComponent) : JFrame(title), KeyListener { private val canvas = BitmapScreenPanel(chargenData, ram) val keyboardBuffer = ArrayDeque() private var borderTop: JPanel private var borderBottom: JPanel private var borderLeft: JPanel private var borderRight: JPanel init { val borderWidth = 24 layout = GridBagLayout() defaultCloseOperation = EXIT_ON_CLOSE isResizable = false isFocusable = true // the borders (top, left, right, bottom) borderTop = JPanel().apply { preferredSize = Dimension( (ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(), (ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt() ) background = ScreenDefs.colorPalette[14] } borderBottom = JPanel().apply { preferredSize = Dimension( (ScreenDefs.DISPLAY_PIXEL_SCALING * (ScreenDefs.SCREEN_WIDTH + 2 * borderWidth)).toInt(), (ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt() ) background = ScreenDefs.colorPalette[14] } borderLeft = JPanel().apply { preferredSize = Dimension( (ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(), (ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt() ) background = ScreenDefs.colorPalette[14] } borderRight = JPanel().apply { preferredSize = Dimension( (ScreenDefs.DISPLAY_PIXEL_SCALING * borderWidth).toInt(), (ScreenDefs.DISPLAY_PIXEL_SCALING * ScreenDefs.SCREEN_HEIGHT).toInt() ) background = ScreenDefs.colorPalette[14] } var c = GridBagConstraints() c.gridx = 0; c.gridy = 1; c.gridwidth = 3 add(borderTop, c) c = GridBagConstraints() c.gridx = 0; c.gridy = 2 add(borderLeft, c) c = GridBagConstraints() c.gridx = 2; c.gridy = 2 add(borderRight, c) c = GridBagConstraints() c.gridx = 0; c.gridy = 3; c.gridwidth = 3 add(borderBottom, c) // the screen canvas(bitmap) c = GridBagConstraints() c.gridx = 1; c.gridy = 2 add(canvas, c) addKeyListener(this) pack() setLocationRelativeTo(null) setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf()) setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, mutableSetOf()) requestFocusInWindow() } fun start() { // repaint the screen's back buffer ~60 times per second val repaintTimer = Timer(1000 / 60) { repaint() borderTop.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15] borderBottom.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15] borderLeft.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15] borderRight.background = ScreenDefs.colorPalette[ram[0xd020].toInt() and 15] if(keyboardBuffer.isNotEmpty()) { // inject keystrokes directly into the c64's keyboard buffer (translate to petscii first) var kbbLen = ram[0xc6] while(kbbLen<=10 && keyboardBuffer.isNotEmpty()) { try { val char = keyboardBuffer.pop() // print("CHAR: '$char' ${char.toShort()} -> ") val petscii = when(char) { '\n' -> 13 // enter '\b' -> 20 // backspace ('delete') else -> Petscii.encodePetscii(char.toString(), true)[0] } // println("$petscii") ram[0x277 + kbbLen] = petscii kbbLen++ } catch(ccx: CharConversionException) { // ignore character // println("ignored") } } ram[0xc6] = kbbLen } } repaintTimer.initialDelay = 0 repaintTimer.start() } // keyboard events: override fun keyTyped(event: KeyEvent) { // println("KEY TYPED: $event '${event.keyChar}'") keyboardBuffer.add(event.keyChar) while (keyboardBuffer.size > 8) keyboardBuffer.pop() } override fun keyPressed(event: KeyEvent) {} override fun keyReleased(event: KeyEvent) {} }