diff --git a/src/main/java/jace/apple2e/Apple2e.java b/src/main/java/jace/apple2e/Apple2e.java index 95e6f4b..c5fb923 100644 --- a/src/main/java/jace/apple2e/Apple2e.java +++ b/src/main/java/jace/apple2e/Apple2e.java @@ -38,6 +38,7 @@ import jace.hardware.CardExt80Col; import jace.hardware.ConsoleProbe; import jace.hardware.Joystick; import jace.hardware.NoSlotClock; +import jace.hardware.ZipWarpAccelerator; import jace.hardware.massStorage.CardMassStorage; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -96,6 +97,8 @@ public class Apple2e extends Computer { public boolean joy2enabled = false; @ConfigurableField(name = "No-Slot Clock Enabled", shortName = "clock", description = "If checked, no-slot clock will be enabled", enablesDevice = true) public boolean clockEnabled = true; + @ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true) + public boolean acceleratorEnabled = true; public Joystick joystick1; public Joystick joystick2; @@ -103,6 +106,7 @@ public class Apple2e extends Computer { public ClassSelection cheatEngine = new ClassSelection(Cheats.class, null); public Cheats activeCheatEngine = null; public NoSlotClock clock; + public ZipWarpAccelerator accelerator; /** * Creates a new instance of Apple2e @@ -154,7 +158,8 @@ public class Apple2e extends Computer { Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); } finally { getCpu().resume(); - reboot(); + reboot(); + resume(); } } @@ -244,39 +249,42 @@ public class Apple2e extends Computer { currentMemory.reconfigure(); if (motherboard != null) { + if (accelerator == null) { + accelerator = new ZipWarpAccelerator(this); + } + if (acceleratorEnabled) { + motherboard.addChildDevice(accelerator); + } else { + motherboard.removeChildDevice(accelerator); + } + if (joy1enabled) { if (joystick1 == null) { joystick1 = new Joystick(0, this); - motherboard.miscDevices.add(joystick1); - joystick1.attach(); + motherboard.addChildDevice(joystick1); } } else if (joystick1 != null) { - joystick1.detach(); - motherboard.miscDevices.remove(joystick1); + motherboard.removeChildDevice(joystick1); joystick1 = null; } if (joy2enabled) { if (joystick2 == null) { joystick2 = new Joystick(1, this); - motherboard.miscDevices.add(joystick2); - joystick2.attach(); + motherboard.addChildDevice(joystick2); } } else if (joystick2 != null) { - joystick2.detach(); - motherboard.miscDevices.remove(joystick2); + motherboard.removeChildDevice(joystick2); joystick2 = null; } if (clockEnabled) { if (clock == null) { clock = new NoSlotClock(this); - motherboard.miscDevices.add(clock); - clock.attach(); + motherboard.addChildDevice(clock); } } else if (clock != null) { - motherboard.miscDevices.remove(clock); - clock.detach(); + motherboard.removeChildDevice(clock); clock = null; } } @@ -336,7 +344,7 @@ public class Apple2e extends Computer { if (cheatEngine.getValue() == null) { if (activeCheatEngine != null) { activeCheatEngine.detach(); - motherboard.miscDevices.remove(activeCheatEngine); + motherboard.addChildDevice(activeCheatEngine); } activeCheatEngine = null; } else { @@ -345,9 +353,8 @@ public class Apple2e extends Computer { if (activeCheatEngine.getClass().equals(cheatEngine.getValue())) { startCheats = false; } else { - activeCheatEngine.detach(); + motherboard.removeChildDevice(activeCheatEngine); activeCheatEngine = null; - motherboard.miscDevices.remove(activeCheatEngine); } } if (startCheats) { @@ -356,8 +363,7 @@ public class Apple2e extends Computer { } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException ex) { Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); } - activeCheatEngine.attach(); - motherboard.miscDevices.add(activeCheatEngine); + motherboard.addChildDevice(activeCheatEngine); } } } catch (IOException ex) { diff --git a/src/main/java/jace/core/Device.java b/src/main/java/jace/core/Device.java index 07bba73..166ba37 100644 --- a/src/main/java/jace/core/Device.java +++ b/src/main/java/jace/core/Device.java @@ -20,6 +20,10 @@ package jace.core; import jace.state.Stateful; import jace.config.Reconfigurable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -39,9 +43,14 @@ import javafx.beans.property.SimpleBooleanProperty; @Stateful public abstract class Device implements Reconfigurable { protected Computer computer; - private Device() { + private List children; + + private Device() { + children = Collections.synchronizedList(new ArrayList<>()); } + public Device(Computer computer) { + this(); this.computer = computer; } @@ -52,6 +61,31 @@ public abstract class Device implements Reconfigurable { private final BooleanProperty run = new SimpleBooleanProperty(true); @Stateful public boolean isPaused = false; + @Stateful + public boolean isAttached = false; + + public void addChildDevice(Device d) { + children.add(d); + if (isAttached) { + d.attach(); + } + } + + public void removeChildDevice(Device d) { + children.remove(d); + d.suspend(); + if (isAttached) { + d.detach(); + } + } + + public void addAllDevices(Collection devices) { + devices.forEach(this::addChildDevice); + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } public BooleanProperty getRunningProperty() { return run; @@ -86,6 +120,7 @@ public abstract class Device implements Reconfigurable { return; } // Implicit else... + children.forEach(Device::doTick); tick(); } @@ -113,17 +148,24 @@ public abstract class Device implements Reconfigurable { setRun(false); return true; } + children.forEach(Device::suspend); return false; } public void resume() { setRun(true); waitCycles = 0; + children.forEach(Device::resume); } - public abstract void attach(); + public void attach() { + isAttached = true; + children.forEach(Device::attach); + } public void detach() { Keyboard.unregisterAllHandlers(this); + children.forEach(Device::suspend); + children.forEach(Device::detach); } } diff --git a/src/main/java/jace/core/Motherboard.java b/src/main/java/jace/core/Motherboard.java index 117d20e..0877d62 100644 --- a/src/main/java/jace/core/Motherboard.java +++ b/src/main/java/jace/core/Motherboard.java @@ -22,7 +22,6 @@ import jace.apple2e.SoftSwitches; import jace.apple2e.Speaker; import jace.config.ConfigurableField; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -39,8 +38,7 @@ import java.util.logging.Logger; * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class Motherboard extends TimedDevice { - - final public Set miscDevices = new LinkedHashSet<>(); + @ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true") public static boolean enableSpeaker = true; public Speaker speaker; @@ -63,8 +61,11 @@ public class Motherboard extends TimedDevice { public Motherboard(Computer computer, Motherboard oldMotherboard) { super(computer); if (oldMotherboard != null) { - miscDevices.addAll(oldMotherboard.miscDevices); + addAllDevices(oldMotherboard.getChildren()); speaker = oldMotherboard.speaker; + accelorationRequestors.addAll(oldMotherboard.accelorationRequestors); + setSpeedInHz(oldMotherboard.getSpeedInHz()); + setMaxSpeed(oldMotherboard.isMaxSpeed()); } } @@ -93,11 +94,8 @@ public class Motherboard extends TimedDevice { clockCounter = cpuPerClock; computer.getVideo().doTick(); for (Optional card : cards) { - card.ifPresent(c -> c.doTick()); + card.ifPresent(Card::doTick); } - miscDevices.stream().forEach((m) -> { - m.doTick(); - }); } catch (Throwable t) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, t); } @@ -125,7 +123,7 @@ public class Motherboard extends TimedDevice { speaker = new Speaker(computer); if (computer.mixer.lineAvailable) { speaker.attach(); - miscDevices.add(speaker); + addChildDevice(speaker); } else { System.out.print("No lines available! Speaker not running."); } @@ -133,21 +131,19 @@ public class Motherboard extends TimedDevice { speaker.reconfigure(); } catch (Throwable t) { System.out.println("Unable to initalize sound -- deactivating speaker out"); - speaker.detach(); - miscDevices.remove(speaker); + removeChildDevice(speaker); } } else { System.out.println("Speaker not enabled, leaving it off."); if (speaker != null) { - speaker.detach(); - miscDevices.remove(speaker); + removeChildDevice(speaker); } } if (startAgain && computer.getMemory() != null) { resume(); } } - static HashSet accelorationRequestors = new HashSet<>(); + HashSet accelorationRequestors = new HashSet<>(); public void requestSpeed(Object requester) { accelorationRequestors.add(requester); @@ -198,11 +194,6 @@ public class Motherboard extends TimedDevice { @Override public void detach() { System.out.println("Detaching motherboard"); - miscDevices.stream().forEach((d) -> { - d.suspend(); - d.detach(); - }); - miscDevices.clear(); // halt(); super.detach(); } diff --git a/src/main/java/jace/hardware/ZipWarpAccelerator.java b/src/main/java/jace/hardware/ZipWarpAccelerator.java new file mode 100644 index 0000000..b70d308 --- /dev/null +++ b/src/main/java/jace/hardware/ZipWarpAccelerator.java @@ -0,0 +1,227 @@ +/* + * Copyright 2018 org.badvision. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jace.hardware; + +import jace.Emulator; +import jace.config.ConfigurableField; +import jace.core.Computer; +import jace.core.Device; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; + +/** + * Implements a basic hardware accelerator that is able to adjust the speed of the emulator + */ +public class ZipWarpAccelerator extends Device { + @ConfigurableField(category = "debug", name = "Debug messages") + public boolean debugMessagesEnabled = false; + + public static final int ENABLE_ADDR = 0x0c05a; + public static final int MAX_SPEED = 0x0c05b; + public static final int REGISTERS = 0x0c05c; + public static final int SET_SPEED = 0x0c05d; + public static final int UNLOCK_VAL = 0x05A; + public static final int LOCK_VAL = 0x0A5; + public static final double UNLOCK_PENALTY_PER_TICK = 0.19; + public static final double UNLOCK_MIN = 4.0; + + public static final int TRANSWARP = 0x0c074; + public static final int TRANSWARP_ON = 1; // Any other value written disables acceleration + + boolean zipLocked = true; + double zipUnlockCount = 0; + int zipRegisters = 0; + int speedValue = 0; + + RAMListener zipListener; + RAMListener transwarpListener; + + public ZipWarpAccelerator(Computer computer) { + super(computer); + zipListener = computer.memory.observe(RAMEvent.TYPE.ANY, ENABLE_ADDR, SET_SPEED, this::handleZipChipEvent); + transwarpListener = computer.memory.observe(RAMEvent.TYPE.ANY, TRANSWARP, this::handleTranswarpEvent); + } + + private void handleZipChipEvent(RAMEvent e) { + boolean isWrite = e.getType() == RAMEvent.TYPE.WRITE; + if (ENABLE_ADDR == e.getAddress() && isWrite) { + if (e.getNewValue() == UNLOCK_VAL) { + zipUnlockCount = Math.ceil(zipUnlockCount) + 1.0; + if (debugMessagesEnabled) { + System.out.println("Unlock sequence detected, new lock value at " + zipUnlockCount + " of " + UNLOCK_MIN); + } + if (zipUnlockCount >= UNLOCK_MIN) { + zipLocked = false; + if (debugMessagesEnabled) { + System.out.println("Zip unlocked!"); + } + } + } else { + zipLocked = true; + if (debugMessagesEnabled) { + System.out.println("Zip locked!"); + } + zipUnlockCount = 0; + if ((e.getNewValue() & 0x0ff) != LOCK_VAL) { + if (debugMessagesEnabled) { + System.out.println("Warp disabled."); + } + turnOffAcceleration(); + } + } + } else if (!zipLocked && isWrite) { + switch (e.getAddress()) { + case MAX_SPEED: + setSpeed(SPEED.MAX); + if (debugMessagesEnabled) { + System.out.println("MAXIMUM WARP!"); + } + break; + case SET_SPEED: + SPEED s = lookupSpeedSetting(e.getNewValue()); + setSpeed(s); + if (debugMessagesEnabled) { + System.out.println("Set speed to " + s.ratio); + } + break; + case REGISTERS: + zipRegisters = e.getNewValue(); + break; + default: + break; + } + } else if (!zipLocked && e.getAddress() == REGISTERS) { + e.setNewValue(zipRegisters); + } + } + + private void handleTranswarpEvent(RAMEvent e) { + if (e.getType().isRead()) { + e.setNewValue(speedValue); + } else { + if (e.getNewValue() == TRANSWARP_ON) { + setSpeed(SPEED.MAX); + if (debugMessagesEnabled) { + System.out.println("MAXIMUM WARP!"); + } + } else { + turnOffAcceleration(); + if (debugMessagesEnabled) { + System.out.println("Warp disabled."); + } + } + } + } + + @Override + protected String getDeviceName() { + return "ZipChip Accelerator"; + } + + public static enum SPEED { + MAX(4.0, 0b000000000, 0b011111100), + _2_667(2.6667, 0b000000100, 0b011111100), + _3(3.0, 0b000001000, 0b011111000), + _3_2(3.2, 0b000010000, 0b011110000), + _3_333(3.333, 0b000100000, 0b011100000), + _2(2.0, 0b001000000, 0b011111100), + _1_333(1.333, 0b001000100, 0b011111100), + _1_5(1.5, 0b001001000, 0b011111000), + _1_6(1.6, 0b001010000, 0b011110000), + _1_667(1.6667, 0b001100000, 0b011100000), + _1b(1.0, 0b010000000, 0b011111100), + _0_667(0.6667, 0b010000100, 0b011111100), + _0_75(0.75, 0b010001000, 0b011111000), + _0_8(0.8, 0b010010000, 0b011110000), + _0_833(0.833, 0b010100000, 0b011100000), + _1_33b(1.333, 0b011000000, 0b011111100), + _0_889(0.8889, 0b011000100, 0b011111100), + _1(1.0, 0b011001000, 0b011111000), + _1_067(1.0667, 0b011010000, 0b011110000), + _1_111(1.111, 0b011100000, 0b011100000); + double ratio; + int val; + int mask; + boolean max; + + SPEED(double speed, int val, int mask) { + this.ratio = speed; + this.val = val; + this.mask = mask; + this.max = speed >= 4.0; + } + } + + private SPEED lookupSpeedSetting(int v) { + for (SPEED s : SPEED.values()) { + if ((v & s.mask) == s.val) { + return s; + } + } + return SPEED._1; + } + + private void setSpeed(SPEED speed) { + speedValue = speed.val; + if (speed.max) { + Emulator.computer.getMotherboard().setMaxSpeed(true); + Motherboard.cpuPerClock = 3; + } else { + Emulator.computer.getMotherboard().setMaxSpeed(false); + Emulator.computer.getMotherboard().setSpeedInPercentage((int) (speed.ratio * 100)); + Motherboard.cpuPerClock = 1; + } + Emulator.computer.getMotherboard().reconfigure(); + } + + private void turnOffAcceleration() { + // The UI Logic retains the user's desired normal speed, reset to that + Emulator.logic.reconfigure(); + } + + @Override + public void tick() { + if (zipUnlockCount > 0) { + zipUnlockCount -= UNLOCK_PENALTY_PER_TICK; + } + } + + @Override + public void attach() { + computer.memory.addListener(zipListener); + computer.memory.addListener(transwarpListener); + } + + @Override + public void detach() { + super.detach(); + computer.memory.removeListener(zipListener); + computer.memory.removeListener(transwarpListener); + } + + @Override + public String getShortName() { + return "zip"; + } + + @Override + public void reconfigure() { + zipUnlockCount = 0; + zipLocked = true; + } + +}