replaced fixed 8x16 bitmap font by psf font

This commit is contained in:
Irmen de Jong 2020-03-17 00:01:45 +01:00
parent 3925c80258
commit d1d433c3a6
11 changed files with 231 additions and 57 deletions

View File

@ -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.

View File

@ -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<BufferedImage> {
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) {

View File

@ -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

View File

@ -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

View File

@ -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<ByteArray>
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<BufferedImage> {
// 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()
// }

View File

@ -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)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB