/* * 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.hardware; import jace.EmulatorUILogic; import jace.apple2e.MOS65C02; import jace.apple2e.RAM128k; import jace.config.ConfigurableField; import jace.config.Name; import jace.core.Card; import jace.core.Computer; import jace.core.PagedMemory; import jace.core.RAMEvent; import jace.core.RAMEvent.TYPE; import jace.state.Stateful; import jace.core.Utility; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.geometry.Point2D; import javafx.geometry.Rectangle2D; /** * Apple Mouse interface implementation. This is fully compatible with several * applications. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ @Stateful @Name("Apple Mouse") public class CardAppleMouse extends Card { @Stateful public int mode; @Stateful public boolean active; @Stateful public boolean interruptOnMove; @Stateful public boolean interruptOnPress; @Stateful public boolean interruptOnVBL; @Stateful public boolean button0press; @Stateful public boolean button1press; @Stateful public boolean button0pressLast; @Stateful public boolean button1pressLast; @Stateful public boolean isInterrupt; @Stateful public boolean isVBL; @Stateful public int statusByte; @Stateful public Point2D lastMouseLocation; @Stateful public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff); // By default, update 60 times a second -- roughly every VBL period (in theory) @ConfigurableField(name = "Update frequency", shortName = "updateFreq", category = "Mouse", description = "# of CPU cycles between updates; affects polling and interrupt-based routines") public static int CYCLES_PER_UPDATE = (int) (1020484L / 60L); @ConfigurableField(name = "Fullscreen fix", shortName = "fsfix", category = "Mouse", description = "If the mouse pointer is a little off when in fullscreen, this should fix it.") public boolean fullscreenFix = true; @ConfigurableField(name = "Blazing Paddles fix", shortName = "bpfix", category = "Mouse", description = "Use different clamping values to make Blazing Paddles work more reliably.") public boolean blazingPaddles = false; Label mouseActive = Utility.loadIconLabel("input-mouse.png").orElse(null); public boolean movedSinceLastTick = false; public boolean movedSinceLastRead = false; public CardAppleMouse(Computer computer) { super(computer); } @Override public String getDeviceName() { return "Apple Mouse"; } @Override public void reset() { mode = 0; clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff); deactivateMouse(); } EventHandler<MouseEvent> mouseHandler = this::processMouseEvent; private void processMouseEvent(MouseEvent event) { if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) { Node source = (Node) event.getSource(); updateLocation(event.getSceneX(), event.getSceneY(), source.getBoundsInLocal()); event.consume(); } if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) { mousePressed(event); event.consume(); } else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) { mouseReleased(event); event.consume(); } } private void updateLocation(double x, double y, Bounds bounds) { double scaledX = x / bounds.getWidth(); double scaledY = y / bounds.getHeight(); lastMouseLocation = new Point2D(scaledX, scaledY); movedSinceLastTick = true; movedSinceLastRead = true; } /* * Coded against this information * http://stason.org/TULARC/pc/apple2/programmer/012-How-do-I-write-programs-which-use-the-mouse.html */ @Override protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) { /* * Screen holes * $0478 + slot Low byte of absolute X position * $04F8 + slot Low byte of absolute Y position * $0578 + slot High byte of absolute X position * $05F8 + slot High byte of absolute Y position * $0678 + slot Reserved and used by the firmware * $06F8 + slot Reserved and used by the firmware * $0778 + slot Button 0/1 interrupt status byte * $07F8 + slot Mode byte * * Interrupt status byte: * Set by READMOUSE * Bit 7 6 5 4 3 2 1 0 * | | | | | | | | * | | | | | | | `--- Previously, button 1 was up (0) or down (1) * | | | | | | `----- Movement interrupt * | | | | | `------- Button 0/1 interrupt * | | | | `--------- VBL interrupt * | | | `----------- Currently, button 1 is up (0) or down (1) * | | `------------- X/Y moved since last READMOUSE * | `--------------- Previously, button 0 was up (0) or down (1) * `----------------- Currently, button 0 is up (0) or down (1) * * Mode byte * Valid after calling SERVEMOUSE, cleared with READMOUSE * Bit 7 6 5 4 3 2 1 0 * | | | | | | | | * | | | | | | | `--- Mouse off (0) or on (1) * | | | | | | `----- Interrupt if mouse is moved * | | | | | `------- Interrupt if button is pressed * | | | | `--------- Interrupt on VBL * | | | `----------- Reserved * | | `------------- Reserved * | `--------------- Reserved * `----------------- Reserved */ if (type == RAMEvent.TYPE.EXECUTE) { // This means the CPU is calling firmware at this location switch (offset - 0x080) { case 0: setMouse(); break; case 1: serveMouse(); break; case 2: readMouse(); break; case 3: clearMouse(); break; case 4: posMouse(); break; case 5: clampMouse(); break; case 6: homeMouse(); break; case 7: initMouse(); break; case 8: getMouseClamp(); break; } // Always pass back RTS e.setNewValue(0x060); } else if (type.isRead()) { /* Identification bytes * $Cn05 = $38 $Cn07 = $18 $Cn0B = $01 $Cn0C = $20 $CnFB = $D6 */ switch (offset) { case 0x05: e.setNewValue(0x038); break; case 0x07: e.setNewValue(0x018); break; case 0x0B: e.setNewValue(0x01); break; case 0x0C: e.setNewValue(0x020); break; case 0x0FB: e.setNewValue(0x0D6); break; // As per the //gs firmware reference manual case 0x08: // Pascal signature byte e.setNewValue(0x001); case 0x011: e.setNewValue(0x000); break; // Function call offsets case 0x12: e.setNewValue(0x080); break; case 0x13: e.setNewValue(0x081); break; case 0x14: e.setNewValue(0x082); break; case 0x15: e.setNewValue(0x083); break; case 0x16: e.setNewValue(0x084); break; case 0x17: e.setNewValue(0x085); break; case 0x18: e.setNewValue(0x086); break; case 0x19: e.setNewValue(0x087); break; case 0x1A: e.setNewValue(0x088); break; default: e.setNewValue(0x069); } // System.out.println("Read mouse firmware at "+Integer.toHexString(e.getAddress())+" == "+Integer.toHexString(e.getNewValue())); } } private MOS65C02 getCPU() { return (MOS65C02) computer.getCpu(); } /* * $Cn12 SETMOUSE Sets mouse mode * A = mouse operation mode (0-f) * C = 1 if illegal mode requested * mode byte updated */ private void setMouse() { mode = getCPU().A & 0x0ff; if (mode > 0x0f) { getCPU().C = 1; return; } else { getCPU().C = 0; } //Interrupt on VBL interruptOnVBL = ((mode & 8) != 0); //Mouse off (0) or on (1) if ((mode & 1) == 0) { deactivateMouse(); interruptOnMove = false; interruptOnPress = false; return; } //Interrupt if mouse is moved interruptOnMove = ((mode & 2) != 0); //Interrupt if button is pressed interruptOnPress = ((mode & 4) != 0); activateMouse(); } /* * $Cn13 SERVEMOUSE Services mouse interrupt * Test for interupt and clear mouse interrupt line * Return C=0 if mouse interrupt occurred * Updates screen hole interrupt status bits */ private void serveMouse() { // If any interrupts are registered then updateMouseState(); if (isInterrupt) { getCPU().C = 0; } else { getCPU().C = 1; // System.out.println("MOUSE TRIGGERED INTERRUPT!"); } // isInterrupt = false; // isVBL=false; } /* * $Cn14 READMOUSE Reads mouse position * Reads delta (X/Y) positions, updates abolute X/Y pos * and reads button statuses * Always returns C=0 * Interrupt status bits cleared * Screen hole positions for button/movement status bits updated */ private void readMouse() { updateMouseState(); isInterrupt = false; isVBL = false; // set screen holes getCPU().C = 0; } /* * $Cn16 POSMOUSE Sets mouse position to a user-defined pos * Caller puts new position in screenhole * Always returns C=0 */ private void posMouse() { // Ignore? getCPU().C = 0; } /* * $Cn17 CLAMPMOUSE Sets mouse bounds in a window * Sets up clamping window for mouse user * Power up defaults are 0 - 1023 (0 - 3ff) * Caller sets: * A = 0 if setting X, 1 if setting Y * $0478 = low byte of low clamp. * $04F8 = low byte of high clamp. * $0578 = high byte of low clamp. * $05F8 = high byte of high clamp. * //gs homes mouse to low address, but //c and //e do not */ private void clampMouse() { RAM128k memory = (RAM128k) computer.memory; byte clampMinLo = memory.getMainMemory().readByte(0x0478); byte clampMaxLo = memory.getMainMemory().readByte(0x04F8); byte clampMinHi = memory.getMainMemory().readByte(0x0578); byte clampMaxHi = memory.getMainMemory().readByte(0x05F8); int min = (clampMinLo & 0x0ff) | ((clampMinHi << 8) & 0x0FF00); int max = (clampMaxLo & 0x0ff) | ((clampMaxHi << 8) & 0x0FF00); if (min >= 32768) { min -= 65536; } if (max >= 32768) { max -= 65536; } if (getCPU().A == 0) { if (blazingPaddles) { min = -1; max = 281; } setClampWindowX(min, max); } else { if (blazingPaddles) { min = -1; max = 193; } setClampWindowY(min, max); } } /* * $Cn19 INITMOUSE Resets mouse clamps to default values; sets mouse position to 0,0 * Sets screen holes to default values and sets clamping * window to default value (000 - 3ff) for both X and Y * Exit:C=0 * Screen holes are updated */ private void initMouse() { mouseActive.setText("Active"); EmulatorUILogic.addIndicator(this, mouseActive, 2000); setClampWindowX(0, 0x3ff); setClampWindowY(0, 0x3ff); clearMouse(); } /* * $Cn15 CLEARMOUSE Clears mouse position to 0 (for delta mode) * Resets buttons, movement and interrupt status bits to 0 * Intended to be used for delta mouse positioning instead of absolute positioning * Always returns C=0 * Interrupt status bits cleared * Screen hole positions for button/movement status bits updated */ private void clearMouse() { isVBL = false; isInterrupt = false; button0press = false; button1press = false; button0pressLast = false; button1pressLast = false; homeMouse(); } /* * $Cn18 HOMEMOUSE Sets absolute position to upper-left corner of clamping window * Exit: c=0 * Screen hole positions are updated */ private void homeMouse() { lastMouseLocation = new Point2D(0, 0); updateMouseState(); getCPU().C = 0; } /** * Described in Apple Mouse technical note #7 * Cn1A: Read mouse clamping values * Register number is stored in $478 and ranges from x47 to x4e * Return value should be stored in $5782 * Values should be returned in this order: * MinXH, MinYH, MinXL, MinYL, MaxXH, MaxYH, MaxXL, MaxYL */ private void getMouseClamp() { byte reg = computer.getMemory().readRaw(0x0478); byte val = 0; switch (reg - 0x047) { case 0: val = (byte) ((int) clampWindow.getMinX() >> 8); break; case 1: val = (byte) ((int) clampWindow.getMinY() >> 8); break; case 2: val = (byte) ((int) clampWindow.getMinX() & 255); break; case 3: val = (byte) ((int) clampWindow.getMinY() & 255); break; case 4: val = (byte) ((int) clampWindow.getMaxX() >> 8); break; case 5: val = (byte) ((int) clampWindow.getMaxY() >> 8); break; case 6: val = (byte) ((int) clampWindow.getMaxX() & 255); break; case 7: val = (byte) ((int) clampWindow.getMaxY() & 255); break; } computer.getMemory().write(0x0578, val, false, false); } /* * This is called whenever the mouse firmware has been activated in software */ private void activateMouse() { active = true; EmulatorUILogic.addMouseListener(mouseHandler); } /* * This is called whenever there is a hard reset or when the mouse is turned off */ private void deactivateMouse() { active = false; mode = 0; interruptOnMove = false; interruptOnPress = false; EmulatorUILogic.removeMouseListener(mouseHandler); } @Override protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { // No IO access necessary (is there?) } private int delay = CYCLES_PER_UPDATE; @Override public void tick() { if (!active) { return; } delay--; if (delay > 0) { return; } delay = CYCLES_PER_UPDATE; // If interrupts not used, just move on if (!interruptOnMove && !interruptOnPress) { return; } if (interruptOnPress) { if (button0press != button0pressLast || button1press != button1pressLast) { isInterrupt = true; getCPU().generateInterrupt(); return; } } if (interruptOnMove) { if (movedSinceLastTick) { isInterrupt = true; getCPU().generateInterrupt(); } } movedSinceLastTick = false; } @Override public void notifyVBLStateChanged(boolean state) { // VBL is false when it is the vertical blanking period if (!state && interruptOnVBL) { isVBL = true; isInterrupt = true; getCPU().generateInterrupt(); } } private void updateMouseState() { double x = lastMouseLocation.getX(); x *= clampWindow.getWidth(); x += clampWindow.getMinX(); x = Math.min(Math.max(x, clampWindow.getMinX()), clampWindow.getMaxX()); double y = lastMouseLocation.getY(); y *= clampWindow.getHeight(); y += clampWindow.getMinY(); y = Math.min(Math.max(y, clampWindow.getMinY()), clampWindow.getMaxY()); PagedMemory m = ((RAM128k) computer.getMemory()).getMainMemory(); int s = getSlot(); /* * $0478 + slot Low byte of absolute X position * $04F8 + slot Low byte of absolute Y position */ m.writeByte(0x0478 + s, (byte) ((int) x & 0x0ff)); m.writeByte(0x04F8 + s, (byte) ((int) y & 0x0ff)); /* * $0578 + slot High byte of absolute X position * $05F8 + slot High byte of absolute Y position */ m.writeByte(0x0578 + s, (byte) (((int) x & 0x0ff00) >> 8)); m.writeByte(0x05F8 + s, (byte) (((int) y & 0x0ff00) >> 8)); /* * $0678 + slot Reserved and used by the firmware * $06F8 + slot Reserved and used by the firmware * * Interrupt status byte: * Set by READMOUSE * Bit 7 6 5 4 3 2 1 0 * | | | | | | | | * | | | | | | | `--- Previously, button 1 was up (0) or down (1) * | | | | | | `----- Movement interrupt * | | | | | `------- Button 0/1 interrupt * | | | | `--------- VBL interrupt * | | | `----------- Currently, button 1 is up (0) or down (1) * | | `------------- X/Y moved since last READMOUSE * | `--------------- Previously, button 0 was up (0) or down (1) * `----------------- Currently, button 0 is up (0) or down (1) */ int status = 0; if (button1pressLast) { status |= 1; } if (interruptOnMove && movedSinceLastRead) { status |= 2; } if (interruptOnPress && (button0press != button0pressLast || button1press != button1pressLast)) { status |= 4; } if (isVBL) { status |= 8; } if (button1press) { status |= 16; } if (movedSinceLastRead) { status |= 32; } if (button0pressLast) { status |= 64; } if (button0press) { status |= 128; } /* * $0778 + slot Button 0/1 interrupt status byte */ m.writeByte(0x0778 + s, (byte) (status)); /* * $07F8 + slot Mode byte */ m.writeByte(0x07F8 + s, (byte) (mode)); button0pressLast = button0press; button1pressLast = button1press; movedSinceLastRead = false; } public void mousePressed(MouseEvent me) { MouseButton button = me.getButton(); if (button == MouseButton.PRIMARY || button == MouseButton.MIDDLE) { button0press = true; } if (button == MouseButton.SECONDARY || button == MouseButton.MIDDLE) { button1press = true; } } public void mouseReleased(MouseEvent me) { MouseButton button = me.getButton(); if (button == MouseButton.PRIMARY || button == MouseButton.MIDDLE) { button0press = false; } if (button == MouseButton.SECONDARY || button == MouseButton.MIDDLE) { button1press = false; } } private void setClampWindowX(int min, int max) { // Fix for GEOS clamping funkiness if (max == 32767) { max = 560; } clampWindow = new Rectangle2D(min, clampWindow.getMinY(), max, clampWindow.getMaxY()); } private void setClampWindowY(int min, int max) { // Fix for GEOS clamping funkiness if (max == 32767) { max = 192; } clampWindow = new Rectangle2D(clampWindow.getMinX(), min, clampWindow.getMaxX(), max); } @Override protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { // Do nothing, there is no need to emulate c8 rom } }