diff --git a/README.md b/README.md index 1e77d88..02f4eaa 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,6 @@ JCenter: [![Download from Jcenter](https://api.bintray.com/packages/irmen/maven/ *Written by Irmen de Jong (irmen@razorvine.net)* -*Software license: MIT, see file LICENSE* - - ![6502](https://upload.wikimedia.org/wikipedia/commons/thumb/4/43/KL_MOS_6502.jpg/320px-KL_MOS_6502.jpg) This is a Kotlin/JVM library that simulates the 8-bit 6502 and 65C02 microprocessors, @@ -67,3 +64,36 @@ various timers and IRQs. It's not cycle perfect, and the video display is drawn so raster splits/rasterbars are impossible. But many other things work fine. ![C64 emulation](c64.png) + + +### License information + +Ksim65 itself is licensed under the MIT software license, see file LICENSE. + +It includes the 'Spleen' bitmap font (https://github.com/fcambus/spleen), +which has the following license (BSD): + +Copyright (c) 2018-2020, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/main/kotlin/razorvine/examplemachines/GUI.kt b/src/main/kotlin/razorvine/examplemachines/GUI.kt index 9c07468..4ce8dac 100644 --- a/src/main/kotlin/razorvine/examplemachines/GUI.kt +++ b/src/main/kotlin/razorvine/examplemachines/GUI.kt @@ -1,57 +1,27 @@ package razorvine.examplemachines +import razorvine.fonts.PsfFont import razorvine.ksim65.* import java.awt.* import java.awt.event.* import java.awt.image.BufferedImage import java.util.* -import javax.imageio.ImageIO import javax.swing.* import javax.swing.event.MouseInputListener /** - * Define a monochrome screen that can display 640x480 pixels - * and/or 80x30 characters (these are 8x16 pixels). + * Define a monochrome screen that can display 80x30 charaacters + * (usually equivalent to 640x480 pixels, but depends on the font size) */ object ScreenDefs { - const val SCREEN_WIDTH_CHARS = 80 - const val SCREEN_HEIGHT_CHARS = 30 - const val SCREEN_WIDTH = SCREEN_WIDTH_CHARS*8 - const val SCREEN_HEIGHT = SCREEN_HEIGHT_CHARS*16 - const val PIXEL_SCALING = 1.5 + const val COLUMNS = 80 + const val ROWS = 30 const val BORDER_SIZE = 32 val BG_COLOR = Color(0, 10, 20) val FG_COLOR = Color(200, 255, 230) val BORDER_COLOR = Color(20, 30, 40) - val Characters = loadCharacters() - - private fun loadCharacters(): Array { - val img = ImageIO.read(javaClass.getResourceAsStream("/charset/unscii8x16.png")) - val charactersImage = BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_ARGB) - charactersImage.createGraphics().drawImage(img, 0, 0, null) - - val black = Color(0, 0, 0).rgb - val foreground = FG_COLOR.rgb - val nopixel = Color(0, 0, 0, 0).rgb - for (y in 0 until charactersImage.height) { - for (x in 0 until charactersImage.width) { - val col = charactersImage.getRGB(x, y) - if (col == black) charactersImage.setRGB(x, y, nopixel) - else charactersImage.setRGB(x, y, foreground) - } - } - - val numColumns = charactersImage.width/8 - val charImages = (0..255).map { - val charX = it%numColumns - val charY = it/numColumns - charactersImage.getSubimage(charX*8, charY*16, 8, 16) - } - - return charImages.toTypedArray() - } } private class BitmapScreenPanel : JPanel() { @@ -61,15 +31,21 @@ private class BitmapScreenPanel : JPanel() { private var cursorX: Int = 0 private var cursorY: Int = 0 private var cursorState: Boolean = false + private val screenFont = PsfFont("spleen-12x24") // nice fonts: sun12x22, iso01-12x22, ter-124b, spleen-12x24, default8x16 + private val PIXEL_SCALING: Double = if(screenFont.width <= 8) 1.5 else 1.0 + private val screenFontImage: BufferedImage init { + println("SCREENFONT WIDTH: ${screenFont.width}") + val ge = GraphicsEnvironment.getLocalGraphicsEnvironment() val gd = ge.defaultScreenDevice.defaultConfiguration - image = gd.createCompatibleImage(ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT, Transparency.OPAQUE) + image = gd.createCompatibleImage(ScreenDefs.COLUMNS*screenFont.width, ScreenDefs.ROWS*screenFont.height, Transparency.OPAQUE) g2d = image.graphics as Graphics2D + screenFontImage = screenFont.convertToImage(g2d, ScreenDefs.FG_COLOR) - val size = Dimension((image.width*ScreenDefs.PIXEL_SCALING).toInt(), - (image.height*ScreenDefs.PIXEL_SCALING).toInt()) + val size = Dimension((image.width*PIXEL_SCALING).toInt(), + (image.height*PIXEL_SCALING).toInt()) minimumSize = size maximumSize = size preferredSize = size @@ -82,13 +58,13 @@ private class BitmapScreenPanel : JPanel() { override fun paint(graphics: Graphics) { val g2d = graphics as Graphics2D g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) - g2d.drawImage(image, 0, 0, (image.width*ScreenDefs.PIXEL_SCALING).toInt(), - (image.height*ScreenDefs.PIXEL_SCALING).toInt(), null) + g2d.drawImage(image, 0, 0, (image.width*PIXEL_SCALING).toInt(), + (image.height*PIXEL_SCALING).toInt(), null) if (cursorState) { - val scx = (cursorX*ScreenDefs.PIXEL_SCALING*8).toInt() - val scy = (cursorY*ScreenDefs.PIXEL_SCALING*16).toInt() - val scw = (8*ScreenDefs.PIXEL_SCALING).toInt() - val sch = (16*ScreenDefs.PIXEL_SCALING).toInt() + val scx = (cursorX*PIXEL_SCALING*screenFont.width).toInt() + val scy = (cursorY*PIXEL_SCALING*screenFont.height).toInt() + val scw = (screenFont.width*PIXEL_SCALING).toInt() + val sch = (screenFont.height*PIXEL_SCALING).toInt() g2d.setXORMode(Color.CYAN) g2d.fillRect(scx, scy, scw, sch) g2d.setPaintMode() @@ -98,7 +74,7 @@ private class BitmapScreenPanel : JPanel() { fun clearScreen() { g2d.background = ScreenDefs.BG_COLOR - g2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT) + g2d.clearRect(0, 0, ScreenDefs.COLUMNS*screenFont.width, ScreenDefs.ROWS*screenFont.height) cursorPos(0, 0) } @@ -110,20 +86,26 @@ private class BitmapScreenPanel : JPanel() { fun getPixel(x: Int, y: Int) = image.getRGB(x, y) != ScreenDefs.BG_COLOR.rgb fun setChar(x: Int, y: Int, character: Char) { - g2d.clearRect(8*x, 16*y, 8, 16) - val coloredImage = ScreenDefs.Characters[character.toInt()] - g2d.drawImage(coloredImage, 8*x, 16*y, null) + val charnum = character.toInt() + val cx = charnum % (screenFontImage.width/screenFont.width) + val cy = charnum / (screenFontImage.width/screenFont.width) + g2d.clearRect(x*screenFont.width, y*screenFont.height, screenFont.width, screenFont.height) + g2d.drawImage(screenFontImage, x*screenFont.width, y*screenFont.height, (x+1)*screenFont.width, + (y+1)*screenFont.height, cx*screenFont.width, cy*screenFont.height, + (cx+1)*screenFont.width, (cy+1)*screenFont.height, null) } fun scrollUp() { - g2d.copyArea(0, 16, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT-16, 0, -16) + g2d.copyArea(0, screenFont.height, + ScreenDefs.COLUMNS*screenFont.width, (ScreenDefs.ROWS-1)*screenFont.height, + 0, -screenFont.height) g2d.background = ScreenDefs.BG_COLOR - g2d.clearRect(0, ScreenDefs.SCREEN_HEIGHT-16, ScreenDefs.SCREEN_WIDTH, 16) + g2d.clearRect(0, (ScreenDefs.ROWS-1)*screenFont.height, ScreenDefs.COLUMNS*screenFont.width, screenFont.height) } fun mousePixelPosition(): Point? { val pos = mousePosition ?: return null - return Point((pos.x/ScreenDefs.PIXEL_SCALING).toInt(), (pos.y/ScreenDefs.PIXEL_SCALING).toInt()) + return Point((pos.x/PIXEL_SCALING).toInt(), (pos.y/PIXEL_SCALING).toInt()) } fun cursorPos(x: Int, y: Int) { diff --git a/src/main/kotlin/razorvine/examplemachines/ehBasicMain.kt b/src/main/kotlin/razorvine/examplemachines/ehBasicMain.kt index e89ff6f..9fc77d3 100644 --- a/src/main/kotlin/razorvine/examplemachines/ehBasicMain.kt +++ b/src/main/kotlin/razorvine/examplemachines/ehBasicMain.kt @@ -21,7 +21,7 @@ class EhBasicMachine(title: String) { val rom = Rom(0xc000, 0xffff).also { it.load(javaClass.getResourceAsStream("/ehbasic_C000.bin").readBytes()) } private val hostDisplay = MainWindow(title) - private val display = Display(0xd000, 0xd00a, hostDisplay, ScreenDefs.SCREEN_WIDTH_CHARS, ScreenDefs.SCREEN_HEIGHT_CHARS) + private val display = Display(0xd000, 0xd00a, hostDisplay, ScreenDefs.COLUMNS, ScreenDefs.ROWS) private val keyboard = Keyboard(0xd400, 0xd400, hostDisplay) private var paused = false diff --git a/src/main/kotlin/razorvine/examplemachines/machineMain.kt b/src/main/kotlin/razorvine/examplemachines/machineMain.kt index a4f204e..3d9cb2f 100644 --- a/src/main/kotlin/razorvine/examplemachines/machineMain.kt +++ b/src/main/kotlin/razorvine/examplemachines/machineMain.kt @@ -20,7 +20,7 @@ class VirtualMachine(title: String) : IVirtualMachine { private val monitor = Monitor(bus, cpu) private val debugWindow = DebugWindow(this) private val hostDisplay = MainWindow(title) - private val display = Display(0xd000, 0xd00a, hostDisplay, ScreenDefs.SCREEN_WIDTH_CHARS, ScreenDefs.SCREEN_HEIGHT_CHARS) + private val display = Display(0xd000, 0xd00a, hostDisplay, ScreenDefs.COLUMNS, ScreenDefs.ROWS) private val mouse = Mouse(0xd300, 0xd305, hostDisplay) private val keyboard = Keyboard(0xd400, 0xd400, hostDisplay) private var paused = false diff --git a/src/main/kotlin/razorvine/fonts/PsfFont.kt b/src/main/kotlin/razorvine/fonts/PsfFont.kt new file mode 100644 index 0000000..8281211 --- /dev/null +++ b/src/main/kotlin/razorvine/fonts/PsfFont.kt @@ -0,0 +1,138 @@ +package razorvine.fonts + +import java.awt.Color +import java.awt.Graphics2D +import java.awt.Transparency +import java.awt.image.BufferedImage +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.util.zip.GZIPInputStream + + +class PsfFont(name: String) { + + // font format info: https://www.win.tue.nl/~aeb/linux/kbd/font-formats-1.html + + val numChars: Int + val bytesPerChar: Int + val height: Int + val width: Int + private val hasUnicodeTable: Boolean + private val rawBitmaps: List + + init { + var data = ByteArray(0) + val fontsDirectory = "/usr/share/kbd/consolefonts" + var stream = javaClass.getResourceAsStream("/charset/$name.psfu.gz") ?: + javaClass.getResourceAsStream("/charset/$name.psf.gz") ?: + (if(File("$fontsDirectory/$name.psfu.gz").exists()) FileInputStream("$fontsDirectory/$name.psfu.gz") else null ) ?: + (if(File("$fontsDirectory/$name.psf.gz").exists()) FileInputStream("$fontsDirectory/$name.psf.gz") else null ) ?: + (if(File("$fontsDirectory/$name.fnt.gz").exists()) FileInputStream("$fontsDirectory/$name.fnt.gz") else null ) + if(stream==null) { + stream = javaClass.getResourceAsStream("/charset/$name.psfu") ?: + javaClass.getResourceAsStream("/charset/$name.psf") ?: + (if(File("$fontsDirectory/$name.psfu").exists()) FileInputStream("$fontsDirectory/$name.psfu") else null ) ?: + (if(File("$fontsDirectory/$name.psf").exists()) FileInputStream("$fontsDirectory/$name.psf") else null ) ?: + (if(File("$fontsDirectory/$name.fnt").exists()) FileInputStream("$fontsDirectory/$name.fnt") else null ) ?: + throw IOException("no such font: $name") + data = stream.readBytes() + } else { + GZIPInputStream(stream).use { data = it.readBytes() } + } + stream.close() + + if (data[0] == 0x36.toByte() && data[1] == 0x04.toByte()) { + // continue reading PSF1 font + val mode = data[2].toInt() + numChars = if (mode and 1 != 0) 512 else 256 + bytesPerChar = data[3].toInt() + hasUnicodeTable = mode and 2 != 0 + height = bytesPerChar + width = 8 + rawBitmaps = (0..numChars).map { + data.sliceArray(3+it*bytesPerChar..3+(it+1)*bytesPerChar) + } + // ignore unicode table for now: val table = stream.readAllBytes() + } else { + if (data[0] == 0x72.toByte() && data[1] == 0xb5.toByte() && data[2] == 0x4a.toByte() && data[3] == 0x86.toByte()) { + // continue reading PSF2 font + // skip the version val version = makeInt(data, 4) + val headersize = makeInt(data, 8) + val flags = makeInt(data, 12) + hasUnicodeTable = flags and 1 != 0 + numChars = makeInt(data, 16) + bytesPerChar = makeInt(data, 20) + height = makeInt(data, 24) + width = makeInt(data, 28) + rawBitmaps = (0..numChars).map { + data.sliceArray(headersize+it*bytesPerChar..headersize+(it+1)*bytesPerChar) + } + } else { + hasUnicodeTable = false + numChars = 0 + bytesPerChar = 0 + height = 0 + width = 0 + rawBitmaps = emptyList() + } + } + } + + fun convertToImage(gfx: Graphics2D, textColor: Color): BufferedImage { + // create a single image with all the characters in a vertical column from top to bottom. + val bitmap = gfx.deviceConfiguration.createCompatibleImage((width+7) and 0b11111000, height*numChars, Transparency.BITMASK) + val bytesHoriz = (width+7)/8 + val color = textColor.rgb + val nopixel = Color(0, 0, 0, 0).rgb + for (char in 0 until numChars) { + for (b in 0 until bytesPerChar) { + val c = rawBitmaps[char][b].toInt() + val ix = 8*(b%bytesHoriz) + val iy = b/bytesHoriz+char*height + bitmap.setRGB(ix, iy, if (c and 0b10000000 != 0) color else nopixel) + bitmap.setRGB(ix+1, iy, if (c and 0b01000000 != 0) color else nopixel) + bitmap.setRGB(ix+2, iy, if (c and 0b00100000 != 0) color else nopixel) + bitmap.setRGB(ix+3, iy, if (c and 0b00010000 != 0) color else nopixel) + bitmap.setRGB(ix+4, iy, if (c and 0b00001000 != 0) color else nopixel) + bitmap.setRGB(ix+5, iy, if (c and 0b00000100 != 0) color else nopixel) + bitmap.setRGB(ix+6, iy, if (c and 0b00000010 != 0) color else nopixel) + bitmap.setRGB(ix+7, iy, if (c and 0b00000001 != 0) color else nopixel) + } + } + return bitmap + } + + private fun makeInt(bytes: ByteArray, offset: Int) = + makeInt(bytes[offset], bytes[offset+1], bytes[offset+2], bytes[offset+3]) + private fun makeInt(b0: Byte, b1: Byte, b2: Byte, b3: Byte) = + b0.toInt() or (b1.toInt() shl 8) or (b2.toInt() shl 16) or (b3.toInt() shl 24) +} + + + +// private fun loadFallbackCharacters(): Array { +// val img = ImageIO.read(javaClass.getResourceAsStream("/charset/unscii8x16.png")) +// val charactersImage = BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_ARGB) +// charactersImage.createGraphics().drawImage(img, 0, 0, null) +// +// val black = Color(0, 0, 0).rgb +// val foreground = FG_COLOR.rgb +// val nopixel = Color(0, 0, 0, 0).rgb +// for (y in 0 until charactersImage.height) { +// for (x in 0 until charactersImage.width) { +// val col = charactersImage.getRGB(x, y) +// if (col == black) charactersImage.setRGB(x, y, nopixel) +// else charactersImage.setRGB(x, y, foreground) +// } +// } +// +// val numColumns = charactersImage.width/8 +// val charImages = (0..255).map { +// val charX = it%numColumns +// val charY = it/numColumns +// charactersImage.getSubimage(charX*8, charY*16, 8, 16) +// } +// +// return charImages.toTypedArray() +// } diff --git a/src/main/kotlin/razorvine/ksim65/components/Display.kt b/src/main/kotlin/razorvine/ksim65/components/Display.kt index dc05320..dc72489 100644 --- a/src/main/kotlin/razorvine/ksim65/components/Display.kt +++ b/src/main/kotlin/razorvine/ksim65/components/Display.kt @@ -102,7 +102,7 @@ class Display(startAddress: Address, endAddress: Address, private val host: IHos 0x05 -> pixelY = (pixelY and 0xff00) or data.toInt() 0x06 -> pixelY = (pixelY and 0x00ff) or (data.toInt() shl 8) 0x07 -> { - if (pixelX in 0 until ScreenDefs.SCREEN_WIDTH && pixelY in 0 until ScreenDefs.SCREEN_HEIGHT) { + if (pixelX in 0 until ScreenDefs.COLUMNS*charWidth && pixelY in 0 until ScreenDefs.ROWS*charHeight) { if (data == 0.toShort()) host.clearPixel(pixelX, pixelY) else host.setPixel(pixelX, pixelY) } diff --git a/src/main/resources/charset/spleen-12x24.psfu.gz b/src/main/resources/charset/spleen-12x24.psfu.gz new file mode 100644 index 0000000..13cc98b Binary files /dev/null and b/src/main/resources/charset/spleen-12x24.psfu.gz differ diff --git a/src/main/resources/charset/spleen-16x32.psfu.gz b/src/main/resources/charset/spleen-16x32.psfu.gz new file mode 100644 index 0000000..118592e Binary files /dev/null and b/src/main/resources/charset/spleen-16x32.psfu.gz differ diff --git a/src/main/resources/charset/spleen-8x16.psfu.gz b/src/main/resources/charset/spleen-8x16.psfu.gz new file mode 100644 index 0000000..f3bd142 Binary files /dev/null and b/src/main/resources/charset/spleen-8x16.psfu.gz differ diff --git a/src/main/resources/charset/spleen-LICENSE b/src/main/resources/charset/spleen-LICENSE new file mode 100644 index 0000000..1e2aa5d --- /dev/null +++ b/src/main/resources/charset/spleen-LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2018-2020, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/main/resources/charset/unscii8x16.png b/src/main/resources/charset/unscii8x16.png deleted file mode 100644 index da5ae79..0000000 Binary files a/src/main/resources/charset/unscii8x16.png and /dev/null differ