mirror of
https://github.com/irmen/ksim65.git
synced 2024-06-01 21:41:31 +00:00
302 lines
15 KiB
Kotlin
302 lines
15 KiB
Kotlin
package razorvine.c64emu
|
|
|
|
import razorvine.ksim65.components.Address
|
|
import razorvine.ksim65.components.MemoryComponent
|
|
import razorvine.ksim65.components.Rom
|
|
import razorvine.ksim65.components.UByte
|
|
import java.awt.*
|
|
import java.awt.image.BufferedImage
|
|
import java.awt.image.DataBufferInt
|
|
import javax.swing.JPanel
|
|
|
|
/**
|
|
* The rendering logic of the screen of the C64.
|
|
* It supports: Character mode,
|
|
* High res bitmap mode (320*200), Multicolor bitmap mode (160*200).
|
|
* TODO: Multicolor character mode. Extended background color mode.
|
|
* TODO: Horizontal smooth scrolling (+38 col mode). Vertical smooth scrolling (+24 row mode).
|
|
*/
|
|
internal class Screen(private val chargen: Rom, val ram: MemoryComponent) : JPanel() {
|
|
|
|
private val fullscreenImage: BufferedImage
|
|
private val fullscreenG2d: Graphics2D
|
|
private val normalCharacters = loadCharacters(false)
|
|
private val shiftedCharacters = loadCharacters(true)
|
|
|
|
init {
|
|
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
|
|
val gd = ge.defaultScreenDevice.defaultConfiguration
|
|
fullscreenImage = gd.createCompatibleImage(ScreenDefs.SCREEN_WIDTH+2*ScreenDefs.BORDER_SIZE,
|
|
ScreenDefs.SCREEN_HEIGHT+2*ScreenDefs.BORDER_SIZE, Transparency.OPAQUE)
|
|
fullscreenG2d = fullscreenImage.graphics as Graphics2D
|
|
|
|
val size = Dimension(fullscreenImage.width*ScreenDefs.PIXEL_SCALING.toInt(),
|
|
(fullscreenImage.height*ScreenDefs.ASPECT_RATIO*ScreenDefs.PIXEL_SCALING).toInt())
|
|
minimumSize = size
|
|
maximumSize = size
|
|
preferredSize = size
|
|
isFocusable = true
|
|
isDoubleBuffered = false
|
|
requestFocusInWindow()
|
|
}
|
|
|
|
private fun loadCharacters(shifted: Boolean): Array<BufferedImage> {
|
|
val chars = Array(256) { BufferedImage(8, 8, BufferedImage.TYPE_BYTE_BINARY) }
|
|
val offset = if (shifted) 256*8 else 0
|
|
for (char in 0..255) {
|
|
for (line in 0..7) {
|
|
val charbyte = chargen[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) {
|
|
val windowG2d = graphics as Graphics2D
|
|
val vicSCROLY = ram[0xd011].toInt()
|
|
if (vicSCROLY and 0b10000 == 0) {
|
|
// screen blanked, only display border color
|
|
fullscreenG2d.background = ScreenDefs.colorPalette[ram[0xd020]]
|
|
fullscreenG2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH+2*ScreenDefs.BORDER_SIZE,
|
|
ScreenDefs.SCREEN_HEIGHT+2*ScreenDefs.BORDER_SIZE)
|
|
} else {
|
|
val vicSCROLX = ram[0xd016].toInt()
|
|
val vicVMCSB = ram[0xd018].toInt()
|
|
val multiColorMode = vicSCROLX and 0b00010000 != 0
|
|
val vicBank = when (ram[0xdd00].toInt() and 0b00000011) {
|
|
0b00 -> 0xc000
|
|
0b01 -> 0x8000
|
|
0b10 -> 0x4000
|
|
else -> 0x0000
|
|
}
|
|
|
|
if (vicSCROLY and 0b00100000 != 0) renderBitmapMode(vicBank, vicVMCSB, multiColorMode)
|
|
else {
|
|
fullscreenG2d.background = ScreenDefs.colorPalette[ram[0xd021]]
|
|
fullscreenG2d.clearRect(ScreenDefs.BORDER_SIZE, ScreenDefs.BORDER_SIZE, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT)
|
|
renderCharacterMode(vicBank, vicVMCSB, multiColorMode)
|
|
}
|
|
|
|
renderSprites(vicBank)
|
|
renderBorder()
|
|
}
|
|
|
|
// scale and draw the image to the window, and simulate a slight scan-line effect
|
|
windowG2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
|
|
windowG2d.drawImage(fullscreenImage, 0, 0, (fullscreenImage.width*ScreenDefs.PIXEL_SCALING).toInt(),
|
|
(fullscreenImage.height*ScreenDefs.ASPECT_RATIO*ScreenDefs.PIXEL_SCALING).toInt(), null)
|
|
windowG2d.color = Color(0, 0, 0, 40)
|
|
windowG2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
|
|
val width = fullscreenImage.width*ScreenDefs.PIXEL_SCALING.toInt()
|
|
val height = (fullscreenImage.height*ScreenDefs.ASPECT_RATIO*ScreenDefs.PIXEL_SCALING).toInt()
|
|
for (y in 0 until height step (ScreenDefs.ASPECT_RATIO*ScreenDefs.PIXEL_SCALING).toInt()) {
|
|
windowG2d.drawLine(0, y, width, y)
|
|
}
|
|
Toolkit.getDefaultToolkit().sync()
|
|
}
|
|
|
|
private fun renderBorder() {
|
|
// draw the screen border
|
|
fullscreenG2d.background = ScreenDefs.colorPalette[ram[0xd020]]
|
|
fullscreenG2d.clearRect(0, 0, ScreenDefs.SCREEN_WIDTH+2*ScreenDefs.BORDER_SIZE, ScreenDefs.BORDER_SIZE)
|
|
fullscreenG2d.clearRect(0, ScreenDefs.SCREEN_HEIGHT+ScreenDefs.BORDER_SIZE, ScreenDefs.SCREEN_WIDTH+2*ScreenDefs.BORDER_SIZE,
|
|
ScreenDefs.BORDER_SIZE)
|
|
fullscreenG2d.clearRect(0, ScreenDefs.BORDER_SIZE, ScreenDefs.BORDER_SIZE, ScreenDefs.SCREEN_HEIGHT)
|
|
fullscreenG2d.clearRect(ScreenDefs.SCREEN_WIDTH+ScreenDefs.BORDER_SIZE, ScreenDefs.BORDER_SIZE, ScreenDefs.BORDER_SIZE,
|
|
ScreenDefs.SCREEN_HEIGHT)
|
|
}
|
|
|
|
private fun renderSprites(vicBank: Address) {
|
|
// TODO sprite-background priorities, collisions
|
|
// TODO multicolor sprites
|
|
val spriteImage = fullscreenG2d.deviceConfiguration.createCompatibleImage(24, 21, Transparency.TRANSLUCENT)
|
|
val spriteGfx = spriteImage.graphics
|
|
spriteGfx.color = Color(255, 255, 255, 0)
|
|
val spritePixels = (spriteImage.raster.dataBuffer as DataBufferInt).data
|
|
val vicSPENA = ram[0xd015].toInt()
|
|
val vicXXPAND = ram[0xd01d].toInt()
|
|
val vicYXPAND = ram[0xd017].toInt()
|
|
for (sprite in 7 downTo 0) {
|
|
val bit = 1 shl sprite
|
|
if (vicSPENA and bit != 0) {
|
|
val mx = ram[0xd010].toInt() and (1 shl sprite) != 0
|
|
val xpos = ram[0xd000+sprite*2].toInt()+if (mx) 256 else 0
|
|
val ypos = ram[0xd001+sprite*2].toInt()
|
|
if (xpos in 1..343 && ypos in 30..249) {
|
|
spriteGfx.fillRect(0, 0, 24, 21)
|
|
renderSprite(sprite, spritePixels, vicBank)
|
|
fullscreenG2d.drawImage(spriteImage, xpos+ScreenDefs.BORDER_SIZE-24, ypos+ScreenDefs.BORDER_SIZE-50,
|
|
if (vicXXPAND and bit == 0) 24 else 48, if (vicYXPAND and bit == 0) 21 else 42, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun renderSprite(sprite: Int, spritePixels: IntArray, vicBank: Address) {
|
|
// note: the sprite pixels must all have been cleared to transparency already
|
|
val sprptr = ram[vicBank+2040+sprite]*64
|
|
val sprdata = ram.getBlock(sprptr, 21*3)
|
|
val color = ScreenDefs.colorPalette[ram[0xd027+sprite]].rgb
|
|
for (i in spritePixels.indices step 8) {
|
|
val bits = sprdata[i/8].toInt()
|
|
bits2Pixels(bits, spritePixels, i, color)
|
|
}
|
|
}
|
|
|
|
private fun renderCharacterMode(vicBank: Address, vicVMCSB: Int, multiColorMode: Boolean) {
|
|
if (multiColorMode) {
|
|
// TODO multicolor character mode, for now, falls back to normal char mode
|
|
}
|
|
|
|
// normal character mode
|
|
val screenAddress = vicBank+((vicVMCSB ushr 4) shl 10)
|
|
val charsetAddress = (vicVMCSB and 0b00001110) shl 10
|
|
for (y in 0 until ScreenDefs.SCREEN_HEIGHT_CHARS) {
|
|
for (x in 0 until ScreenDefs.SCREEN_WIDTH_CHARS) {
|
|
val char = ram[screenAddress+x+y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt()
|
|
val color = ram[0xd800+x+y*ScreenDefs.SCREEN_WIDTH_CHARS].toInt() // colors always at $d800
|
|
drawColoredChar(x, y, char, color, vicBank+charsetAddress)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun renderBitmapMode(vicBank: Address, vicVMCSB: Int, multiColorMode: Boolean) {
|
|
val bitmap = ram.getBlock(vicBank+if (vicVMCSB and 0b00001000 != 0) 32*256 else 0, 32*256)
|
|
val colorBytes = ram.getBlock(vicBank+((vicVMCSB ushr 4) shl 2)*256, 4*256)
|
|
val pixels: IntArray = (fullscreenImage.raster.dataBuffer as DataBufferInt).data
|
|
val screenColor = ScreenDefs.colorPalette[ram[0xd021]].rgb
|
|
if (multiColorMode) {
|
|
// multicolor bitmap mode 160x200
|
|
val fourColors = IntArray(4)
|
|
fourColors[0b00] = screenColor
|
|
for (y in 0 until ScreenDefs.SCREEN_HEIGHT) {
|
|
for (x in 0 until ScreenDefs.SCREEN_WIDTH/2 step 4) {
|
|
val colorIdx = ScreenDefs.SCREEN_WIDTH_CHARS*(y ushr 3)+(x ushr 2)
|
|
fourColors[0b01] = ScreenDefs.colorPalette[colorBytes[colorIdx].toInt() ushr 4].rgb
|
|
fourColors[0b10] = ScreenDefs.colorPalette[colorBytes[colorIdx].toInt()].rgb
|
|
fourColors[0b11] = ScreenDefs.colorPalette[ram[0xd800+colorIdx].toInt()].rgb
|
|
draw4bitmapPixelsMc(pixels, x, y, bitmap, fourColors)
|
|
}
|
|
}
|
|
} else {
|
|
// bitmap mode 320x200
|
|
for (y in 0 until ScreenDefs.SCREEN_HEIGHT) {
|
|
for (x in 0 until ScreenDefs.SCREEN_WIDTH step 8) {
|
|
val colorIdx = ScreenDefs.SCREEN_WIDTH_CHARS*(y ushr 3)+(x ushr 3)
|
|
val bgColor = ScreenDefs.colorPalette[colorBytes[colorIdx].toInt()].rgb
|
|
val fgColor = ScreenDefs.colorPalette[colorBytes[colorIdx].toInt() ushr 4].rgb
|
|
draw8bitmapPixels(pixels, x, y, bitmap, fgColor, bgColor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun draw8bitmapPixels(pixels: IntArray, xstart: Int, y: Int, bitmap: Array<UByte>, fgColorRgb: Int, bgColorRgb: Int) {
|
|
val offset = ScreenDefs.BORDER_SIZE+ScreenDefs.BORDER_SIZE*fullscreenImage.width+xstart+y*fullscreenImage.width
|
|
val byte = bitmap[ScreenDefs.SCREEN_WIDTH_CHARS*(y and 248)+(y and 7)+(xstart and 504)].toInt()
|
|
pixels[offset] = if (byte and 0b10000000 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+1] = if (byte and 0b01000000 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+2] = if (byte and 0b00100000 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+3] = if (byte and 0b00010000 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+4] = if (byte and 0b00001000 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+5] = if (byte and 0b00000100 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+6] = if (byte and 0b00000010 != 0) fgColorRgb else bgColorRgb
|
|
pixels[offset+7] = if (byte and 0b00000001 != 0) fgColorRgb else bgColorRgb
|
|
}
|
|
|
|
private fun draw4bitmapPixelsMc(pixels: IntArray, xstart: Int, y: Int, bitmap: Array<UByte>, fourColors: IntArray) {
|
|
val realx = xstart*2
|
|
val offset = ScreenDefs.BORDER_SIZE+ScreenDefs.BORDER_SIZE*fullscreenImage.width+realx+y*fullscreenImage.width
|
|
val byte = bitmap[ScreenDefs.SCREEN_WIDTH_CHARS*(y and 248)+(y and 7)+(realx and 504)].toInt()
|
|
val colors = listOf(byte and 0b11000000 ushr 6, byte and 0b00110000 ushr 4, byte and 0b00001100 ushr 2, byte and 0b00000011)
|
|
pixels[offset] = fourColors[colors[0]]
|
|
pixels[offset+1] = fourColors[colors[0]]
|
|
pixels[offset+2] = fourColors[colors[1]]
|
|
pixels[offset+3] = fourColors[colors[1]]
|
|
pixels[offset+4] = fourColors[colors[2]]
|
|
pixels[offset+5] = fourColors[colors[2]]
|
|
pixels[offset+6] = fourColors[colors[3]]
|
|
pixels[offset+7] = fourColors[colors[3]]
|
|
}
|
|
|
|
private val coloredCharacterImageCache = mutableMapOf<Triple<Int, Int, Boolean>, BufferedImage>()
|
|
|
|
private fun drawColoredChar(x: Int, y: Int, char: Int, color: Int, charsetAddr: Address) {
|
|
// The vic 'sees' the charset ROM at these addresses: $1000(normal) + $1800(shifted), $9000(normal) + $9800(shifted)
|
|
// so we can use pre-loaded images to efficiently draw the default ROM characters.
|
|
// If the address is different, the vic takes charset data from RAM instead (thus allowing user defined charsets)
|
|
// A user-supplied character set in RAM must begin at an address that is 2048 byte aligned,
|
|
// and lies within the same 16K VIC bank as the screen character memory.
|
|
when (charsetAddr) {
|
|
0x1000, 0x1800, 0x9000, 0x9800 -> {
|
|
val charImage = getCharFromROM(char, color, charsetAddr and 0x0800 != 0)
|
|
fullscreenG2d.drawImage(charImage, x*8+ScreenDefs.BORDER_SIZE, y*8+ScreenDefs.BORDER_SIZE, null)
|
|
}
|
|
else -> {
|
|
val charImg = getCharFromRAM(char, color, charsetAddr)
|
|
fullscreenG2d.drawImage(charImg, x*8+ScreenDefs.BORDER_SIZE, y*8+ScreenDefs.BORDER_SIZE, null)
|
|
}
|
|
}
|
|
}
|
|
|
|
private val userCharacter = fullscreenG2d.deviceConfiguration.createCompatibleImage(8, 8, BufferedImage.BITMASK)
|
|
|
|
private fun getCharFromRAM(char: Int, color: Int, charsetAddr: Address): BufferedImage {
|
|
val chardefBytes = ram.getBlock(charsetAddr+char*8, 8)
|
|
val charPixels = (userCharacter.raster.dataBuffer as DataBufferInt).data
|
|
val rgb = ScreenDefs.colorPalette[color].rgb
|
|
val gfx = userCharacter.graphics
|
|
gfx.color = ScreenDefs.colorPalette[ram[0xd021]]
|
|
gfx.fillRect(0, 0, 8, 8)
|
|
for (y in 0..7) {
|
|
val bits = chardefBytes[y].toInt()
|
|
bits2Pixels(bits, charPixels, y*8, rgb)
|
|
}
|
|
return userCharacter
|
|
}
|
|
|
|
private fun bits2Pixels(bits: Int, bitmap: IntArray, yoffset: Int, colorRgb: Int) {
|
|
if (bits and 0b10000000 != 0) bitmap[yoffset] = colorRgb
|
|
if (bits and 0b01000000 != 0) bitmap[yoffset+1] = colorRgb
|
|
if (bits and 0b00100000 != 0) bitmap[yoffset+2] = colorRgb
|
|
if (bits and 0b00010000 != 0) bitmap[yoffset+3] = colorRgb
|
|
if (bits and 0b00001000 != 0) bitmap[yoffset+4] = colorRgb
|
|
if (bits and 0b00000100 != 0) bitmap[yoffset+5] = colorRgb
|
|
if (bits and 0b00000010 != 0) bitmap[yoffset+6] = colorRgb
|
|
if (bits and 0b00000001 != 0) bitmap[yoffset+7] = colorRgb
|
|
}
|
|
|
|
private fun getCharFromROM(char: Int, color: Int, shifted: Boolean): BufferedImage {
|
|
val key = Triple(char, color, shifted)
|
|
|
|
fun makeCachedImage(): BufferedImage {
|
|
val monoImg = if (shifted) shiftedCharacters[char] else normalCharacters[char]
|
|
val coloredImg = fullscreenG2d.deviceConfiguration.createCompatibleImage(8, 8, BufferedImage.BITMASK)
|
|
val srcPixel = IntArray(4)
|
|
val rgbs = IntArray(4)
|
|
val javaColor = ScreenDefs.colorPalette[color]
|
|
rgbs[0] = javaColor.red
|
|
rgbs[1] = javaColor.green
|
|
rgbs[2] = javaColor.blue
|
|
rgbs[3] = javaColor.alpha
|
|
val sourceRaster = monoImg.raster
|
|
val coloredRaster = coloredImg.raster
|
|
for (pixelY in 0..7) {
|
|
for (pixelX in 0..7) {
|
|
val source = sourceRaster.getPixel(pixelX, pixelY, srcPixel)
|
|
if (source[0] != 0) {
|
|
coloredRaster.setPixel(pixelX, pixelY, rgbs)
|
|
}
|
|
}
|
|
}
|
|
coloredCharacterImageCache[key] = coloredImg
|
|
return coloredImg
|
|
}
|
|
|
|
return coloredCharacterImageCache[key] ?: makeCachedImage()
|
|
}
|
|
}
|