/*
VdpTMS9918A.java
(c) 2008-2016 Edward Swartz
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
*/
package v9t9.engine.video.tms9918a;
import static v9t9.common.hardware.VdpTMS9918AConsts.*;
import java.io.PrintWriter;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
import ejs.base.properties.IProperty;
import ejs.base.properties.IPropertyListener;
import ejs.base.settings.ISettingSection;
import ejs.base.settings.Logging;
import ejs.base.utils.HexUtils;
import ejs.base.utils.ListenerList;
import v9t9.common.client.ISettingsHandler;
import v9t9.common.cpu.ICpu;
import v9t9.common.hardware.ICruChip;
import v9t9.common.hardware.IVdpChip;
import v9t9.common.hardware.IVdpTMS9918A;
import v9t9.common.machine.IMachine;
import v9t9.common.machine.IRegisterAccess;
import v9t9.common.memory.ByteMemoryAccess;
import v9t9.common.memory.IMemoryDomain;
import v9t9.common.settings.Settings;
import v9t9.engine.demos.actors.VdpDataDemoActor;
import v9t9.engine.demos.actors.VdpRegisterDemoActor;
import v9t9.engine.hardware.BaseCruChip;
import v9t9.engine.memory.VdpMmio;
/**
* This is the 99/4A VDP chip.
* <p>
* Mode bits:
* <p>
* R0: M3 @ 1
* R1: M1 @ 4, M2 @ 3
* <p>
* <pre>
* M1 M2 M3
* Text 1 mode: 1 0 0 = 1
* Multicolor: 0 1 0 = 2
* Graphics 1 mode: 0 0 0 = 0
* Graphics 2 mode: 0 0 1 = 4
* </pre>
* @author ejs
*/
public class VdpTMS9918A implements IVdpChip, IVdpTMS9918A {
private final static Map<Integer, String> regNames = new HashMap<Integer, String>();
private final static Map<String, Integer> regIds = new HashMap<String, Integer>();
private static void register(int reg, String id) {
regNames.put(reg, id);
regIds.put(id, reg);
}
static {
for (int i = 0; i < 8; i++) {
register(i, "VR" + i);
}
register(REG_ST, "ST");
register(REG_SCANLINE, "SCAN");
}
protected IMemoryDomain vdpMemory;
protected byte vdpregs[];
protected byte vdpStatus;
protected VdpMmio vdpMmio;
/** The circular counter for VDP interrupt timing. */
private int vdpInterruptFrac;
/** The number of CPU cycles corresponding to 1/60 second */
private int vdpInterruptLimit;
private int vdpInterruptDelta;
/** The circular counter for VDP scanline timing. */
private int vdpScanlineFrac;
/** The number of CPU cycles corresponding to one scanline */
private int vdpScanlineLimit;
private int throttleCount;
protected final IMachine machine;
private int fixedTimeVdpInterruptDelta;
private IProperty cyclesPerSecond;
protected IProperty vdpInterruptRate;
private IProperty realTime;
private IProperty cpuSynchedVdpInterrupt;
private boolean isCpuSynchedVdpInterrupt;
protected IProperty dumpVdpAccess;
protected IProperty dumpFullInstructions;
private IProperty throttleInterrupts;
protected ListenerList<IRegisterWriteListener> listeners = new ListenerList<IRegisterWriteListener>();
protected int modeNumber;
private int vdpScanline;
protected int width;
protected int scanlineCount;
public VdpTMS9918A(IMachine machine) {
this.machine = machine;
ISettingsHandler settings = Settings.getSettings(machine);
cyclesPerSecond = settings.get(ICpu.settingCyclesPerSecond);
vdpInterruptRate = settings.get(settingVdpInterruptRate);
realTime = settings.get(ICpu.settingRealTime);
cpuSynchedVdpInterrupt = settings.get(settingCpuSynchedVdpInterrupt);
isCpuSynchedVdpInterrupt = cpuSynchedVdpInterrupt.getBoolean();
throttleInterrupts = settings.get(IMachine.settingThrottleInterrupts);
dumpFullInstructions = settings.get(ICpu.settingDumpFullInstructions);
dumpVdpAccess = settings.get(settingDumpVdpAccess);
vdpStatus = (byte) VDP_INTERRUPT;
vdpInterruptRate.addListener(new IPropertyListener() {
public void propertyChanged(IProperty setting) {
recalcInterruptTiming();
}
});
cyclesPerSecond.addListenerAndFire(new IPropertyListener() {
public void propertyChanged(IProperty setting) {
recalcInterruptTiming();
}
});
realTime.addListener(new IPropertyListener() {
public void propertyChanged(IProperty setting) {
cpuSynchedVdpInterrupt.setBoolean(setting.getBoolean());
isCpuSynchedVdpInterrupt = cpuSynchedVdpInterrupt.getBoolean();
}
});
this.vdpMemory = machine.getMemory().getDomain(IMemoryDomain.NAME_VIDEO);
this.vdpregs = allocVdpRegs();
initRegisters();
machine.getDemoManager().registerActorProvider(new VdpDataDemoActor.Provider());
machine.getDemoManager().registerActorProvider(new VdpRegisterDemoActor.Provider());
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpChip#getMachine()
*/
@Override
public IMachine getMachine() {
return machine;
}
public void initRegisters() {
// zeroes are fine
}
protected void recalcInterruptTiming() {
if (vdpInterruptRate.getInt() > 0)
vdpInterruptLimit = cyclesPerSecond.getInt() / vdpInterruptRate.getInt();
else
vdpInterruptLimit = Integer.MAX_VALUE;
vdpInterruptFrac = 0;
vdpScanlineLimit = vdpInterruptLimit / 192;
vdpScanlineFrac = 0;
if (scanlineCount > 0) {
vdpScanlineLimit = vdpInterruptLimit / scanlineCount;
}
fixedTimeVdpInterruptDelta = (int) ((long) vdpInterruptRate.getInt() * 65536 / machine.getTicksPerSec());
//System.out.println("VDP interrupt target: " + Cpu.settingCyclesPerSecond.getInt() + " / " + settingVdpInterruptRate.getInt() + " = " + vdpInterruptLimit);
}
public void log(String msg) {
if (dumpVdpAccess.getBoolean()) {
PrintWriter pw = Logging.getLog(dumpFullInstructions);
if (pw != null)
pw.println("[VDP] " + msg);
}
}
public IMemoryDomain getVideoMemory() {
return vdpMemory;
}
public void setVdpMmio(VdpMmio vdpMmio) {
this.vdpMmio = vdpMmio;
}
public VdpMmio getVdpMmio() {
return vdpMmio;
}
protected byte[] allocVdpRegs() {
return new byte[8];
}
/**
* @param reg
* @param value new value
*/
protected void fireRegisterChanged(final int reg, final int value) {
if (!listeners.isEmpty()) {
for (Object listener : listeners.toArray()) {
try {
((IRegisterWriteListener)listener).registerChanged(reg, value);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
/* (non-Javadoc)
* @see v9t9.handlers.VdpHandler#readVdpStatus()
*/
public byte readVdpStatus() {
/* >8802, status read and acknowledge interrupt */
byte ret = vdpStatus;
setRegister(REG_ST, vdpStatus & ~VDP_INTERRUPT);
machine.getCru().acknowledgeInterrupt(VDP_INTERRUPT);
return ret;
}
public void touchAbsoluteVdpMemory(int vdpaddr) {
vdpMemory.touchMemory(vdpaddr & getModeAddressMask());
}
public byte readAbsoluteVdpMemory(int vdpaddr) {
return vdpMmio.readFlatMemory(vdpaddr);
}
public void writeAbsoluteVdpMemory(int vdpaddr, byte byt) {
vdpMmio.writeFlatMemory(vdpaddr, byt);
}
public ByteMemoryAccess getByteReadMemoryAccess(int addr) {
return vdpMmio.getByteReadMemoryAccess(addr);
}
public void tick() {
if (isCpuSynchedVdpInterrupt)
return;
// in this model, we use the system clock to ensure reliable VDP
// interrupts, because the CPU speed is unbounded and unreliable.
if (machine.isExecuting()) {
vdpInterruptDelta += fixedTimeVdpInterruptDelta;
//System.out.print("[VDP delt:" + vdpInterruptDelta + "]");
if (vdpInterruptDelta >= 65536) {
vdpInterruptDelta -= 65536;
for (int row = 0; row < scanlineCount; row++) {
onScanline(row);
}
doTick();
onScanline(-1);
}
}
}
public void syncVdpInterrupt(IMachine machine) {
if (!isCpuSynchedVdpInterrupt)
return;
// in this model, the CPU is running at a fixed rate,
// so we can trigger VDP interrupts in lockstep
// with the CPU.
if (vdpInterruptFrac < 0)
vdpInterruptFrac = 0;
if (vdpScanlineFrac < 0)
vdpScanlineFrac = 0;
boolean shouldTick = false;
if (vdpInterruptFrac >= vdpInterruptLimit) {
vdpInterruptFrac -= vdpInterruptLimit;
log("INT: interrupt tick");
shouldTick = true;
}
else {
while (vdpScanlineFrac >= vdpScanlineLimit) {
vdpScanlineFrac -= vdpScanlineLimit;
onScanline(vdpScanline + 1);
}
if (vdpScanline >= scanlineCount) {
//log("INT: scanline tick");
//shouldTick = true;
vdpScanline -= scanlineCount;
}
}
if (shouldTick) {
doTick();
onScanline(-1);
}
}
/**
*/
protected void onScanline(int vdpScanline) {
setRegister(REG_SCANLINE, vdpScanline);
}
/**
*
*/
protected void doTick() {
if (throttleInterrupts.getBoolean()) {
if (throttleCount-- < 0) {
throttleCount = 6;
} else {
return;
}
}
// a real interrupt only occurs if wanted, but always marked
setRegister(REG_ST, vdpStatus | VDP_INTERRUPT);
if ((vdpregs[1] & R1_INT) != 0) {
triggerInterrupt();
}
}
/**
*
*/
protected void triggerInterrupt() {
machine.getExecutor().vdpInterrupt();
ICruChip cru = machine.getCru();
if (cru instanceof BaseCruChip) {
cru.triggerInterrupt(((BaseCruChip) cru).intVdp);
}
}
public boolean isThrottled() {
return true;
}
public void work() {
}
public void saveState(ISettingSection section) {
String[] regState = new String[vdpregs.length];
for (int i = 0; i < vdpregs.length; i++) {
regState[i] = HexUtils.toHex2(vdpregs[i]);
}
section.put("Registers", regState);
//settingDumpVdpAccess.saveState(section);
cpuSynchedVdpInterrupt.saveState(section);
vdpInterruptRate.saveState(section);
}
public void loadState(ISettingSection section) {
if (section == null) return;
String[] regState = section.getArray("Registers");
if (regState != null) {
for (int i = 0; i < regState.length; i++) {
byte val = (byte) Integer.parseInt(regState[i], 16);
loadVdpReg(i, val);
}
}
//settingDumpVdpAccess.loadState(section);
cpuSynchedVdpInterrupt.loadState(section);
isCpuSynchedVdpInterrupt = cpuSynchedVdpInterrupt.getBoolean();
vdpInterruptRate.loadState(section);
}
protected void loadVdpReg(int num, byte val) {
setRegister(num, val);
}
public void addCpuCycles(int cycles) {
vdpInterruptFrac += cycles;
vdpScanlineFrac += cycles;
}
@Override
public void addWriteListener(IRegisterWriteListener listener) {
listeners.add(listener);
}
@Override
public void removeWriteListener(IRegisterWriteListener listener) {
listeners.remove(listener);
}
public String getGroupName() {
return "VDP TMS9918A Registers";
}
/* (non-Javadoc)
* @see v9t9.common.machine.IRegisterAccess#getFirstRegister()
*/
@Override
public int getFirstRegister() {
// note: not REG_SCANLINE
return REG_ST;
}
/* (non-Javadoc)
* @see v9t9.engine.VdpHandler#getRegisterCount()
*/
@Override
public int getRegisterCount() {
return 8 - getFirstRegister();
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpChip#getRecordableRegs()
*/
@Override
public BitSet getRecordableRegs() {
BitSet bs = new BitSet();
int first = getFirstRegister();
//bs.set(REG_SCANLINE - first);
bs.set(0 - first, 8);
return bs;
}
/* (non-Javadoc)
* @see v9t9.engine.VdpHandler#getRegister(int)
*/
@Override
public int getRegister(int reg) {
if (reg == REG_ST) {
return vdpStatus;
} else if (reg == REG_SCANLINE) {
return vdpScanline;
} else if (reg < vdpregs.length) {
return vdpregs[reg] & 0xff;
} else {
return 0;
}
}
/* (non-Javadoc)
* @see v9t9.common.machine.IRegisterAccess#getRegisterInfo(int)
*/
@Override
public RegisterInfo getRegisterInfo(int reg) {
String id = getRegisterId(reg);
if (id == null)
return null;
return new RegisterInfo(id,
getRegisterFlags(reg),
getRegisterSize(reg),
getRegisterName(reg));
}
/**
* @param reg
* @return
*/
protected int getRegisterSize(int reg) {
return 1;
}
protected int getRegisterFlags(int reg) {
return IRegisterAccess.FLAG_ROLE_GENERAL +
(reg == REG_ST ? IRegisterAccess.FLAG_VOLATILE : 0);
}
protected String getRegisterId(int reg) {
return regNames.get(reg);
}
@Override
public int getRegisterNumber(String id) {
Integer num = regIds.get(id);
return num != null ? num : Integer.MIN_VALUE;
}
protected String getRegisterName(int reg) {
switch (reg) {
case REG_SCANLINE:
return "Scanline";
case REG_ST:
return "Status";
}
switch (reg) {
case 0:
return "Mode Reg 0";
case 1:
return "Mode Reg 1";
case 2:
return "Screen Offset";
case 3:
return "Color Table";
case 4:
return "Pattern Table";
case 5:
return "Sprite Table";
case 6:
return "Sprite Patterns";
case 7:
return "Backdrop/Text Colors";
}
return null;
}
protected String yOrN(String label, int i) {
return i != 0 ? label : "";
}
/* (non-Javadoc)
* @see v9t9.engine.VdpHandler#getRegisterTooltip(int)
*/
@Override
public String getRegisterTooltip(int reg) {
switch (reg) {
case REG_ST:
return getStatusString(vdpStatus);
}
byte val = vdpregs[reg];
switch (reg) {
case 0:
return caten(yOrN("Bitmap", val & 0x2), yOrN("Ext Vid", val & 0x1))
+ " (" + getModeName() + ")";
case 1:
return caten(yOrN("16K", val & 0x80), yOrN("Blank", val & 0x40),
yOrN("Int on", val & 0x20), yOrN("Multi", val & 0x10),
yOrN("Text", val & 0x08),
yOrN("Size 4", val & 0x02), yOrN("Mag", val & 0x01))
+ " (" + getModeName() + ")";
case 2:
return "Screen: " + HexUtils.toHex4(getScreenTableBase());
case 3:
return "Colors: " + HexUtils.toHex4(getColorTableBase())
+ (isBitmapMode() ?
" | Mask: " + HexUtils.toHex4(getBitmapModeColorMask())
: "");
case 4:
return "Patterns: " + HexUtils.toHex4(getPatternTableBase())
+ (isBitmapMode() ?
" | Mask: " + HexUtils.toHex4(getBitmapModePatternMask())
: "");
case 5:
return "Sprites: " + HexUtils.toHex4(getSpriteTableBase());
case 6:
return "Sprite patterns: " + HexUtils.toHex4(getSpritePatternTableBase());
case 7:
return "Color BG: " + HexUtils.toHex2(val & 0x7)
+ " | FG: " + HexUtils.toHex2((val & 0xf0) >> 4);
}
return null;
}
protected String getStatusString(byte s) {
return caten(yOrN("Int", s & 0x80),
yOrN("5 Sprites", s & 0x40),
yOrN("Coinc", s & 0x20))
+ " | 5th: " + (s & 0x1f);
}
/**
* @param yOrN
* @param yOrN2
* @param yOrN3
* @return
*/
protected String caten(String... vals) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String v : vals) {
if (first)
first = false;
else
sb.append(" | ");
sb.append(v.length() == 0 ? "0" : v);
}
return sb.toString();
}
/* (non-Javadoc)
* @see v9t9.engine.VdpHandler#setRegister(int, byte)
*/
@Override
public int setRegister(int reg, int value) {
int old;
if (reg == REG_SCANLINE) {
old = vdpScanline;
vdpScanline = value;
} else if (reg == REG_ST) {
old = vdpStatus & 0xff;
value &= 0xff;
vdpStatus = (byte) value;
} else {
if (reg >= vdpregs.length)
return 0;
old = vdpregs[reg] & 0xff;
value &= 0xff;
vdpregs[reg] = (byte) value;
doSetVdpReg(reg, (byte) old, (byte) value);
modeNumber = calculateModeNumber();
updateForMode();
}
if (dumpFullInstructions.getBoolean() && dumpVdpAccess.getBoolean())
log("register " + getRegisterName(reg) + " " + HexUtils.toHex2(old) + " -> " + HexUtils.toHex2(value));
fireRegisterChanged(reg, value);
return old;
}
/**
* @param reg
* @param b
* @param val
*/
protected void doSetVdpReg(int reg, byte old, byte val) {
/* if interrupts enabled, and interrupt was pending, trigger it */
if ((val & R1_INT) != 0
&& (old & R1_INT) == 0
&& (vdpStatus & VDP_INTERRUPT) != 0)
{
triggerInterrupt();
}
}
/**
* @param modeNumber
*/
protected void updateForMode() {
setSize(256, 192);
if ((vdpregs[1] & R1_NOBLANK) == 0) {
vdpMmio.setMemoryAccessCycles(0);
return;
}
switch (modeNumber) {
case MODE_GRAPHICS:
vdpMmio.setMemoryAccessCycles(8);
break;
case MODE_MULTI:
vdpMmio.setMemoryAccessCycles(2);
break;
case MODE_TEXT:
vdpMmio.setMemoryAccessCycles(1);
break;
case MODE_BITMAP:
vdpMmio.setMemoryAccessCycles(8);
break;
}
}
/**
* @param width
* @param height
*/
protected void setSize(int width, int height) {
this.width = width;
this.scanlineCount = height;
}
/**
* @return
*/
public String getModeName() {
switch (getModeNumber()) {
case MODE_BITMAP: return "Bitmap";
case MODE_GRAPHICS: return "Graphics";
case MODE_MULTI: return "MultiColor";
case MODE_TEXT: return "Text";
}
return null;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpChip#isInterlacedEvenOdd()
*/
@Override
public boolean isInterlacedEvenOdd() {
return false;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpChip#getGraphicsPageSize()
*/
@Override
public int getGraphicsPageSize() {
return 0;
}
/**
* Get the address a table will take given the mode and memory size
* @return
*/
protected int getModeAddressMask() {
return vdpMmio.getMemorySize() - 1;
}
@Override
public int getScreenTableBase() {
return (vdpregs[2] * 0x400) & getModeAddressMask();
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getScreenTableSize()
*/
@Override
public int getScreenTableSize() {
return getModeNumber() == MODE_TEXT ? 960 : 768;
}
@Override
public int getSpritePatternTableBase() {
return (vdpregs[6] * 0x800) & getModeAddressMask();
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getSpritePatternTableSize()
*/
@Override
public int getSpritePatternTableSize() {
return 2048;
}
@Override
public int getSpriteTableBase() {
return (vdpregs[5] * 0x80) & getModeAddressMask();
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getSpriteTableSize()
*/
@Override
public int getSpriteTableSize() {
return 128;
}
protected boolean isBitmapMode() {
return modeNumber == MODE_BITMAP;
}
@Override
public int getPatternTableBase() {
if (isBitmapMode())
return (vdpregs[4] & 0x04) * 0x800;
else
return ((vdpregs[4] & 0xff) * 0x800) & getModeAddressMask();
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getPatternTableSize()
*/
@Override
public int getPatternTableSize() {
return isBitmapMode() ? 0x1800 : 0x800;
}
@Override
public int getColorTableBase() {
return isBitmapMode() ? (vdpregs[3] & 0x80) * 0x40
: ((vdpregs[3] & 0xff) * 0x40) & getModeAddressMask();
}
@Override
public int getColorTableSize() {
return isBitmapMode() ? 0x1800 : 32;
}
final public int getModeNumber() {
return modeNumber;
}
protected int calculateModeNumber() {
int reg0 = vdpregs[0] & R0_M3;
int reg1 = vdpregs[1] & R1_M1 + R1_M2;
if (reg0 == R0_M3) {
// can support multi+bitmap or text+bitmap modes too... but not now
return MODE_BITMAP;
}
if (reg1 == R1_M2)
return MODE_MULTI;
if (reg1 == R1_M1)
return MODE_TEXT;
return MODE_GRAPHICS;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getBitmapModeColorMask()
*/
@Override
public int getBitmapModeColorMask() {
return (short) (vdpregs[3] & 0x7f) << 6 | 0x3f;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getBitmapModePatternMask()
*/
@Override
public int getBitmapModePatternMask() {
// thanks, Thierry!
// in "bitmap text" mode, the full pattern table is always addressed,
// otherwise, the color bits are used in the pattern masking
if ((vdpregs[1] & 0x10) != 0)
return (vdpregs[4] & 0x03) << 11 | 0x7ff;
else
return (vdpregs[4] & 0x03) << 11 | getBitmapModeColorMask() & 0x7ff;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#isBitmapMonoMode()
*/
@Override
public boolean isBitmapMonoMode() {
boolean isMono = isBitmapMode() && getBitmapModeColorMask() != 0x1fff;
return isMono;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpTMS9918A#getVdpRegisterCount()
*/
@Override
public int getVdpRegisterCount() {
return 8;
}
/* (non-Javadoc)
* @see v9t9.common.hardware.IVdpChip#getMemorySize()
*/
@Override
public int getMemorySize() {
return vdpMmio.getMemorySize();
}
/** Tell if the registers indicate a blank screen. */
public boolean isBlank() {
return (vdpregs[1] & R1_NOBLANK) == 0;
}
@Override
public BitSet getVisibleMemory(int granularityShift) {
BitSet bs = new BitSet();
if (isBlank())
return bs;
populateBits(bs, granularityShift,
getScreenTableBase(),
getScreenTableSize());
populateBits(bs, granularityShift,
getPatternTableBase(),
getPatternTableSize());
populateBits(bs, granularityShift,
getColorTableBase(),
getColorTableSize());
populateBits(bs, granularityShift,
getSpriteTableBase(),
getSpriteTableSize());
populateBits(bs, granularityShift,
getSpritePatternTableBase(),
getSpritePatternTableSize());
return bs;
}
private void populateBits(BitSet bs, int granularityShift,
int base, int size) {
int round = ~0 >>> (32 - granularityShift);
bs.set(base >>> granularityShift,
(base + size + round) >>> granularityShift);
}
}