jace/src/main/java/jace/core/Video.java

300 lines
9.7 KiB
Java

/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.state.Stateful;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
/**
* Generic abstraction of a 560x192 video output device which renders 40 columns
* per scanline. This also triggers VBL and updates the physical screen.
* Subclasses are used to manage actual rendering via ScreenWriter
* implementations.
* Created on November 10, 2006, 4:29 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public abstract class Video extends Device {
@Stateful
BufferedImage video;
VideoWriter currentWriter;
Graphics screen;
private byte floatingBus = 0;
private int width = 560;
private int height = 192;
@Stateful
public int x = 0;
@Stateful
public int y = 0;
@Stateful
public int scannerAddress;
@Stateful
public int vPeriod = 0;
@Stateful
public int hPeriod = 0;
static final public int CYCLES_PER_LINE = 65;
static final public int TOTAL_LINES = 262;
static final public int APPLE_CYCLES_PER_LINE = 40;
static final public int APPLE_SCREEN_LINES = 192;
static final public int HBLANK = CYCLES_PER_LINE - APPLE_CYCLES_PER_LINE;
static final public int VBLANK = (TOTAL_LINES - APPLE_SCREEN_LINES) * CYCLES_PER_LINE;
static public int[] textOffset;
static public int[] hiresOffset;
static public int[] textRowLookup;
static public int[] hiresRowLookup;
private boolean screenDirty;
private boolean lineDirty;
private boolean isVblank = false;
static VideoWriter[][] writerCheck = new VideoWriter[40][192];
static {
textOffset = new int[192];
hiresOffset = new int[192];
textRowLookup = new int[0x0400];
hiresRowLookup = new int[0x02000];
for (int i = 0; i < 192; i++) {
textOffset[i] = calculateTextOffset(i >> 3);
hiresOffset[i] = calculateHiresOffset(i);
}
for (int i = 0; i < 0x0400; i++) {
textRowLookup[i] = identifyTextRow(i);
}
for (int i = 0; i < 0x2000; i++) {
hiresRowLookup[i] = identifyHiresRow(i);
}
}
private int forceRedrawRowCount = 0;
Thread updateThread;
/**
* Creates a new instance of Video
*/
public Video() {
suspend();
video = new BufferedImage(560, 192, BufferedImage.TYPE_INT_RGB);
vPeriod = 0;
hPeriod = 0;
forceRefresh();
}
public void setWidth(int w) {
width = w;
}
public int getWidth() {
return width;
}
public void setHeight(int h) {
height = h;
}
public int getHeight() {
return height;
}
public void setScreen(Graphics g) {
screen = g;
}
public Graphics getScreen() {
return screen;
}
public VideoWriter getCurrentWriter() {
return currentWriter;
}
public void setCurrentWriter(VideoWriter currentWriter) {
if (this.currentWriter != currentWriter || currentWriter.isMixed()) {
this.currentWriter = currentWriter;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
}
}
@ConfigurableField(category = "video", name = "Min. Screen Refesh", defaultValue = "15", description = "Minimum number of miliseconds to wait before trying to redraw.")
public static int MIN_SCREEN_REFRESH = 15;
public void redraw() {
if (screen == null || video == null) {
return;
}
screenDirty = false;
screen.drawImage(video, 0, 0, width, height, null);
if (Emulator.getFrame() != null) {
Emulator.getFrame().repaintIndicators();
}
}
public void vblankStart() {
if (screenDirty && isRunning()) {
redraw();
}
}
abstract public void vblankEnd();
abstract public void hblankStart(BufferedImage screen, int y, boolean isDirty);
public void setScannerLocation(int loc) {
scannerAddress = loc;
}
@Override
public void tick() {
setFloatingBus(Computer.getComputer().getMemory().readRaw(scannerAddress + x));
if (hPeriod > 0) {
hPeriod--;
if (hPeriod == 0) {
x = -1;
setScannerLocation(currentWriter.getYOffset(y));
}
} else {
if (!isVblank) {
draw();
}
if (x >= APPLE_CYCLES_PER_LINE - 1) {
int yy = y + hblankOffsetY;
if (yy < 0) {
yy += APPLE_SCREEN_LINES;
}
if (yy >= APPLE_SCREEN_LINES) {
yy -= (TOTAL_LINES - APPLE_SCREEN_LINES);
}
setScannerLocation(currentWriter.getYOffset(yy) + hblankOffsetX + (yy < 64 ? 128 : 0));
x = -1;
if (!isVblank) {
if (lineDirty) {
screenDirty = true;
currentWriter.clearDirty(y);
}
hblankStart(video, y, lineDirty);
lineDirty = false;
forceRedrawRowCount--;
}
hPeriod = HBLANK;
y++;
if (y >= APPLE_SCREEN_LINES) {
if (!isVblank) {
y = APPLE_SCREEN_LINES - (TOTAL_LINES - APPLE_SCREEN_LINES);
isVblank = true;
vblankStart();
Motherboard.vblankStart();
} else {
y = 0;
isVblank = false;
vblankEnd();
Motherboard.vblankEnd();
}
}
}
}
x++;
}
abstract public void configureVideoMode();
protected static int byteDoubler(byte b) {
int num =
// Skip hi-bit because it's not used in display
// ((b&0x080)<<7) |
((b & 0x040) << 6)
| ((b & 0x020) << 5)
| ((b & 0x010) << 4)
| ((b & 0x08) << 3)
| ((b & 0x04) << 2)
| ((b & 0x02) << 1)
| (b & 0x01);
return num | (num << 1);
}
@ConfigurableField(name = "Waits per cycle", category = "Advanced", description = "Adjust the delay for the scanner")
public static int waitsPerCycle = 0;
@ConfigurableField(name = "Hblank X offset", category = "Advanced", description = "Adjust where the hblank period starts relative to the start of the line")
public static int hblankOffsetX = -29;
@ConfigurableField(name = "Hblank Y offset", category = "Advanced", description = "Adjust which line the HBLANK starts on (0=current, 1=next, etc)")
public static int hblankOffsetY = 1;
private void draw() {
if (lineDirty || forceRedrawRowCount > 0 || currentWriter.isRowDirty(y)) {
lineDirty = true;
currentWriter.displayByte(video, x, y, textOffset[y], hiresOffset[y]);
}
setWaitCycles(waitsPerCycle);
doPostDraw();
}
static public int calculateHiresOffset(int y) {
return calculateTextOffset(y >> 3) + ((y & 7) << 10);
}
static public int calculateTextOffset(int y) {
return ((y & 7) << 7) + 40 * (y >> 3);
}
static public int identifyTextRow(int y) {
//floor((x-1024)/128) + floor(((x-1024)%128)/40)*8
// Caller must check result is <= 23, if so then they are in a screenhole!
return (y >> 7) + (((y & 0x7f) / 40) << 3);
}
static public int identifyHiresRow(int y) {
int blockOffset = identifyTextRow(y & 0x03ff);
// Caller must check results is > 0, if not then they are in a screenhole!
if (blockOffset > 23) {
return -1;
}
return ((y >> 10) & 7) + (blockOffset << 3);
}
public abstract void doPostDraw();
public byte getFloatingBus() {
return floatingBus;
}
private void setFloatingBus(byte floatingBus) {
this.floatingBus = floatingBus;
}
@InvokableAction(name = "Refresh screen",
category = "display",
description = "Marks screen contents as changed, forcing full screen redraw",
alternatives = "redraw")
public final void forceRefresh() {
lineDirty = true;
screenDirty = true;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
}
@Override
public String getShortName() {
return "vid";
}
public BufferedImage getFrameBuffer() {
return video;
}
}