Package org.javatari.atari.tia

Source Code of org.javatari.atari.tia.TIA

// Copyright 2011-2012 Paulo Augusto Peccin. See licence.txt distributed with this file.

package org.javatari.atari.tia;   


import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;

import org.javatari.atari.board.BUS;
import org.javatari.atari.controls.ConsoleControls;
import org.javatari.atari.controls.ConsoleControlsInput;
import org.javatari.atari.tia.audio.AudioGenerator;
import org.javatari.atari.tia.audio.AudioMonoGenerator;
import org.javatari.atari.tia.video.NTSCPalette;
import org.javatari.atari.tia.video.PALPalette;
import org.javatari.atari.tia.video.VideoGenerator;
import org.javatari.general.av.video.VideoStandard;
import org.javatari.general.board.BUS16Bits;
import org.javatari.general.board.ClockDriven;
import org.javatari.parameters.Parameters;
import org.javatari.utils.ArraysCopy;


public final class TIA implements BUS16Bits, ClockDriven, ConsoleControlsInput {

  public TIA() {
    videoOutput = new VideoGenerator();
    audioOutput = new AudioMonoGenerator();
  }

  public void connectBus(BUS bus) {
    this.bus = bus;
  }

  public VideoGenerator videoOutput() {  // VideoSignal
    return videoOutput;
  }

  public AudioGenerator audioOutput() {  // AudioSignal
    return audioOutput;
  }

  public void videoStandard(VideoStandard standard) {
    videoOutput.standard(standard);
    palette = standard.equals(VideoStandard.NTSC) ? NTSCPalette.getPalette() : PALPalette.getPalette();
  }
 
  public double desiredClockForVideoStandard() {
    if (FORCED_CLOCK != 0) return FORCED_CLOCK;
    return videoOutput.standard().fps;
  }
 
  public void powerOn() {
    framePatternPosition = 0;
    Arrays.fill(linePixels, HBLANK_COLOR);
    Arrays.fill(debugPixels, 0);
    audioOutput.channel0().setVolume(0);
    audioOutput.channel1().setVolume(0);
    initLatchesAtPowerOn();
    observableChangeExtended();
    powerOn = true;
  }

  public void powerOff() {
    powerOn = false;
    // Let monitors know that the signals are off
    videoOutput.signalOff();   
    audioOutput.signalOff();   
  }

  @Override
  // To perform better, TIA is using one clock cycle per frame
  public void clockPulse() {
   
    int frames = 0;
    if (powerOn) {
      if (debugPause) {
        if (debugPausedMoreFrames()) frames = 1;
      } else {
        if (--framePatternPosition < 0) framePatternPosition = framePattern.length - 1;
        frames = framePattern[framePatternPosition];
      }
    }
   
    if (frames <= 0) return;

     while (frames > 0 && powerOn) {
      clock = 0;
      // Send the first clock/3 pulse to the CPU and PIA, perceived by TIA at clock 0
      bus.clockPulse();
      // Releases the CPU at the beginning of the line in case a WSYNC has halted it
      if (!bus.cpu.RDY) bus.cpu.RDY = true;
      // HBLANK period
      for (clock = 3; clock < HBLANK_DURATION; clock += 3) {    // 3 .. 66
        if (!repeatLastLine) checkRepeatMode();
        // Send clock/3 pulse to the CPU and PIA each 3rd TIA cycle
        bus.clockPulse();
      }
      // 67
      // First Audio Sample. 2 samples per scan line ~ 31440 KHz
      audioOutput.clockPulse();
      // Display period
      int subClock3 = 2// To control the clock/3 cycles. First at clock 69
      for (clock = 68; clock < LINE_WIDTH; clock++) {      // 68 .. 227
        if (!repeatLastLine) checkRepeatMode();
        // Clock delay decodes
        if (vBlankDecodeActive) vBlankClockDecode();
        // Send clock/3 pulse to the CPU and PIA each 3rd TIA cycle
        if (--subClock3 == 0) {
          bus.clockPulse();
          subClock3 = 3;
        }
        objectsClockCounters();
        if (!repeatLastLine && (clock >= 76 || !hMoveHitBlank))
          setPixelValue();
        // else linePixels[clock] |= 0x88800080;  // Add a pink dye to show pixels repeated
      }
      // End of scan line
      // Second Audio Sample. 2 samples per scan line ~ 31440 KHz
      audioOutput.clockPulse();
      // Handle Paddles capacitor charging
      if (paddle0Position >= 0 && !paddleCapacitorsGrounded) paddlesChargeCapacitors()// Only if paddles are connected (position >= 0)
      finishLine();
      // Send the finished line to the output and check if monitor vSynched
      if (videoOutput.nextLine(linePixels, vSyncOn)) frames--;
    }

    if (powerOn) {
      audioOutput.finishFrame();
      videoOutput.finishFrame();
    }
  }

  private void checkRepeatMode() {
    // If one entire line since last observable change has just completed, enter repeatLastLine mode
    if (clock == lastObservableChangeClock) {
      repeatLastLine = true;
      lastObservableChangeClock = -1;
    }
  }

  private void setPixelValue() {
    // No need to calculate all possibilities in vSync/vBlank. TODO No collisions will be detected
    if (vSyncOn) { linePixels[clock] = vSyncColor; return; }
    if (vBlankOn) { linePixels[clock] = vBlankColor; return; }
    // Updates the current PlayFiled pixel to draw only each 4 pixels, or at the first calculated pixel after stopped using cached line
    if ((clock & 0x03) == 0 || clock == lastObservableChangeClock)    // clock & 0x03 is the same as clock % 4
      playfieldUpdateCurrentPixel();
    // Pixel color
    int color = 1;    // All valid colors are between 0xffffffff ad 0x00000000, therefore <= 0
    // Flags for Collision latches
    boolean P0 = false, P1 = false, M0 = false, M1 = false, FL = false, BL = false;
    // Get the value for the PlayField and Ball first only if PlayField and Ball have higher priority
    if (playfieldPriority) {
      // Get the value for the Ball
      if (ballScanCounter >= 0 && ballScanCounter <= 7) {
        playersPerformDelayedSpriteChanges();    // May trigger Ball delayed enablement
        if (ballEnabled) {
          BL = true;
          color = ballColor;
        }
      }
      if (playfieldCurrentPixel) {
        FL = true;
        if (color > 0) color = playfieldColor;  // No Score Mode in priority mode
      }
    }
    // Get the value for Player0
    if (player0ScanCounter >= 0 && player0ScanCounter <= 31) {
      playersPerformDelayedSpriteChanges();
      int sprite = player0VerticalDelay ? player0ActiveSprite : player0DelayedSprite;
      if (sprite != 0)
        if (((sprite >> (player0Reflected ? (7 - (player0ScanCounter >>> 2)) : (player0ScanCounter >>> 2))) & 0x01) != 0) {
          P0 = true;
          if (color > 0) color = player0Color;
        }
    }
    if (missile0ScanCounter >= 0 && missile0Enabled && missile0ScanCounter <= 7 && !missile0ResetToPlayer) {
      M0 = true;
      if (color > 0) color = missile0Color;
    }
    // Get the value for Player1
    if (player1ScanCounter >= 0 && player1ScanCounter <= 31) {
      playersPerformDelayedSpriteChanges();
      int sprite = player1VerticalDelay ? player1ActiveSprite : player1DelayedSprite;
      if (sprite != 0)
        if (((sprite >> (player1Reflected ? (7 - (player1ScanCounter >>> 2)) : (player1ScanCounter >>> 2))) & 0x01) != 0) {
          P1 = true;
          if (color > 0) color = player1Color;
        }
    }
    if (missile1ScanCounter >= 0 && missile1Enabled &&  missile1ScanCounter <= 7 && !missile1ResetToPlayer) {
      M1 = true;
      if (color > 0) color = missile1Color;
    }
    if (!playfieldPriority) {
      // Get the value for the Ball (low priority)
      if (ballScanCounter >= 0 && ballScanCounter <= 7) {
        playersPerformDelayedSpriteChanges();    // May trigger Ball delayed enablement
        if (ballEnabled) {
          BL = true;
          if (color > 0) color = ballColor;
        }
      }
      // Get the value for the the PlayField (low priority)
      if (playfieldCurrentPixel) {
        FL = true;
        if (color > 0) color = !playfieldScoreMode ? playfieldColor : (clock < 148 ? player0Color : player1Color);
      }
    }
    // If nothing more is showing, get the PlayField background value (low priority)
    if (color > 0) color = playfieldBackground;
    // Set the correct pixel color
    linePixels[clock] = color;
    // Finish collision latches
    if (debugNoCollisions) return;
    if (P0 && FL)
      CXP0FB |= 0x80;
    if (P1) {
      if (FL) CXP1FB |= 0x80;
      if (P0) CXPPMM |= 0x80;
    }
    if (BL) {
      if (FL) CXBLPF |= 0x80;
      if (P0) CXP0FB |= 0x40;
      if (P1) CXP1FB |= 0x40;
    }
    if (M0) {
      if (P1) CXM0P  |= 0x80;
      if (P0) CXM0P  |= 0x40;
      if (FL) CXM0FB |= 0x80;
      if (BL) CXM0FB |= 0x40;
    }
    if (M1) {
      if (P0) CXM1P  |= 0x80;
      if (P1) CXM1P  |= 0x40;
      if (FL) CXM1FB |= 0x80;
      if (BL) CXM1FB |= 0x40;
      if (M0) CXPPMM |= 0x40;
    }
  }

  private void objectsClockCounters() {
    player0ClockCounter();
    player1ClockCounter();
    missile0ClockCounter();
    missile1ClockCounter();
    ballClockCounter();
  }

  private void player0ClockCounter() {
    if (++player0Counter == 160) player0Counter = 0;
    if (player0ScanCounter >= 0) {
      // If missileResetToPlayer is on and the player scan has started the FIRST copy
      if (missile0ResetToPlayer && player0Counter < 12 && player0ScanCounter >= 28 && player0ScanCounter <= 31)
        missile0Counter = 156;
      player0ScanCounter -= player0ScanSpeed;
    }
    // Start scans 4 clocks before each copy. Scan is between 0 and 31, each pixel = 4 scan clocks
    if (player0Counter == 156) {
      if (player0RecentReset) player0RecentReset = false;
      else player0ScanCounter = 31 + player0ScanSpeed * (player0ScanSpeed == 4 ? 5 : 6)// If Double or Quadruple size, delays 1 additional pixel
    }
    else if (player0Counter == 12) {
      if (player0CloseCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
    }
    else if (player0Counter == 28) {
      if (player0MediumCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
    }
    else if (player0Counter == 60) {
      if (player0WideCopy) player0ScanCounter = 31 + player0ScanSpeed * 5;
    }
  }

  private void player1ClockCounter() {
    if (++player1Counter == 160) player1Counter = 0;
    if (player1ScanCounter >= 0) {
      // If missileResetToPlayer is on and the player scan has started the FIRST copy
      if (missile1ResetToPlayer && player1Counter < 12 && player1ScanCounter >= 28 && player1ScanCounter <= 31)
        missile1Counter = 156;
      player1ScanCounter -= player1ScanSpeed;
    }
    // Start scans 4 clocks before each copy. Scan is between 0 and 31, each pixel = 4 scan clocks
    if (player1Counter == 156) {
      if (player1RecentReset) player1RecentReset = false;
      else player1ScanCounter = 31 + player1ScanSpeed * (player1ScanSpeed == 4 ? 5 : 6)// If Double or Quadruple size, delays 1 additional pixel
    }
    else if (player1Counter == 12) {
      if (player1CloseCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
    }
    else if (player1Counter == 28) {
      if (player1MediumCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
    }
    else if (player1Counter == 60) {
      if (player1WideCopy) player1ScanCounter = 31 + player1ScanSpeed * 5;
    }
  }

  private void missile0ClockCounter() {
    if (++missile0Counter == 160) missile0Counter = 0;
    if (missile0ScanCounter >= 0) missile0ScanCounter -= missile0ScanSpeed;
    // Start scans 4 clocks before each copy. Scan is between 0 and 7, each pixel = 8 scan clocks
    if (missile0Counter == 156) {
      if (missile0RecentReset) missile0RecentReset = false;
      else missile0ScanCounter = 7 + missile0ScanSpeed * 4;
    }
    else if (missile0Counter == 12) {
      if (player0CloseCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
    }
    else if (missile0Counter == 28) {
      if (player0MediumCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
    }
    else if (missile0Counter == 60) {
      if (player0WideCopy) missile0ScanCounter = 7 + missile0ScanSpeed * 4;
    }
  }

  private void missile1ClockCounter() {
    if (++missile1Counter == 160) missile1Counter = 0;
    if (missile1ScanCounter >= 0) missile1ScanCounter -= missile1ScanSpeed;
    // Start scans 4 clocks before each copy. Scan is between 0 and 7, each pixel = 8 scan clocks
    if (missile1Counter == 156) {
      if (missile1RecentReset) missile1RecentReset = false;
      else missile1ScanCounter = 7 + missile1ScanSpeed * 4;
    }
    else if (missile1Counter == 12) {
      if (player1CloseCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
    }
    else if (missile1Counter == 28) {
      if (player1MediumCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
    }
    else if (missile1Counter == 60) {
      if (player1WideCopy) missile1ScanCounter = 7 + missile1ScanSpeed * 4;
    }
  }

  private void ballClockCounter() {
    if (++ballCounter == 160) ballCounter = 0;
    if (ballScanCounter >= 0) ballScanCounter -= ballScanSpeed;
    // The ball does not have copies and does not wait for the next scanline to start even if recently reset
    // Start scans 4 clocks before. Scan is between 0 and 7, each pixel = 8 scan clocks
    if (ballCounter == 156) ballScanCounter = 7 + ballScanSpeed * 4;
  }

  private void playfieldDelaySpriteChange(int part, int sprite) {
    observableChange();
    if (debug) debugPixel(DEBUG_PF_GR_COLOR);
    playfieldPerformDelayedSpriteChange(true);
    playfieldDelayedChangeClock = clock;
    playfieldDelayedChangePart = part;
    playfieldDelayedChangePattern = sprite;
  }

  private void playfieldPerformDelayedSpriteChange(boolean force) {
    // Only commits change if there is one and the delay has passed
    if (playfieldDelayedChangePart == -1) return;
    if (!force) {
      int dif = clock - playfieldDelayedChangeClock;
      if (dif == 0 || dif == 1) return;
    }
    observableChange();
    if     (playfieldDelayedChangePart == 0) PF0 = playfieldDelayedChangePattern;
    else if  (playfieldDelayedChangePart == 1) PF1 = playfieldDelayedChangePattern;
    else if (playfieldDelayedChangePart == 2) PF2 = playfieldDelayedChangePattern;
    playfieldPatternInvalid = true;
    playfieldDelayedChangePart = -1;    // Marks the delayed change as nothing
  }

  private void playfieldUpdateCurrentPixel() {
    playfieldPerformDelayedSpriteChange(false);
    if (playfieldPatternInvalid) {
      playfieldPatternInvalid = false;
      // Shortcut if the Playfield is all clear
      if (PF0 == 0 && PF1 == 0 && PF2 == 0) {
        Arrays.fill(playfieldPattern, false);
        playfieldCurrentPixel = false;
        return;
      }
      int s, i;
      if (playfieldReflected) {
        s = 40; i = -1;
      } else {
        s = 19; i = 1;
      }
      playfieldPattern[0= playfieldPattern[s+=i] = (PF0 & 0x10) != 0;
      playfieldPattern[1= playfieldPattern[s+=i] = (PF0 & 0x20) != 0;
      playfieldPattern[2= playfieldPattern[s+=i] = (PF0 & 0x40) != 0;
      playfieldPattern[3= playfieldPattern[s+=i] = (PF0 & 0x80) != 0;
      playfieldPattern[4= playfieldPattern[s+=i] = (PF1 & 0x80) != 0;
      playfieldPattern[5= playfieldPattern[s+=i] = (PF1 & 0x40) != 0;
      playfieldPattern[6= playfieldPattern[s+=i] = (PF1 & 0x20) != 0;
      playfieldPattern[7= playfieldPattern[s+=i] = (PF1 & 0x10) != 0;
      playfieldPattern[8= playfieldPattern[s+=i] = (PF1 & 0x08) != 0;
      playfieldPattern[9= playfieldPattern[s+=i] = (PF1 & 0x04) != 0;
      playfieldPattern[10] = playfieldPattern[s+=i] = (PF1 & 0x02) != 0;
      playfieldPattern[11] = playfieldPattern[s+=i] = (PF1 & 0x01) != 0;
      playfieldPattern[12] = playfieldPattern[s+=i] = (PF2 & 0x01) != 0;
      playfieldPattern[13] = playfieldPattern[s+=i] = (PF2 & 0x02) != 0;
      playfieldPattern[14] = playfieldPattern[s+=i] = (PF2 & 0x04) != 0;
      playfieldPattern[15] = playfieldPattern[s+=i] = (PF2 & 0x08) != 0;
      playfieldPattern[16] = playfieldPattern[s+=i] = (PF2 & 0x10) != 0;
      playfieldPattern[17] = playfieldPattern[s+=i] = (PF2 & 0x20) != 0;
      playfieldPattern[18] = playfieldPattern[s+=i] = (PF2 & 0x40) != 0;
      playfieldPattern[19] = playfieldPattern[s+=i] = (PF2 & 0x80) != 0;
    }
    playfieldCurrentPixel = playfieldPattern[((clock - HBLANK_DURATION) >>> 2)];
  }

  private void playerDelaySpriteChange(int player, int sprite) {
    observableChange();
    if (debug) debugPixel(player == 0 ? DEBUG_P0_GR_COLOR : DEBUG_P1_GR_COLOR);
    if (playersDelayedSpriteChangesCount >= PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT) {
      debugInfo(">>> Max player delayed changes reached: " + PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT);
      return;
   
    playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][0] = clock;
    playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][1] = player;
    playersDelayedSpriteChanges[playersDelayedSpriteChangesCount][2] = sprite;
    playersDelayedSpriteChangesCount++;
  }

  private void playersPerformDelayedSpriteChanges() {
    if (playersDelayedSpriteChangesCount == 0 || playersDelayedSpriteChanges[0][0] == clock) return;
    for (int i = 0; i < playersDelayedSpriteChangesCount; i++) {
      int[] change = playersDelayedSpriteChanges[i];
      if (change[1] == 0) {
        player0DelayedSprite = change[2];
        player1ActiveSprite = player1DelayedSprite;
      } else {
        player1DelayedSprite = change[2];
        player0ActiveSprite = player0DelayedSprite;
        ballEnabled = ballDelayedEnablement;
      }
    }
    playersDelayedSpriteChangesCount = 0;
  }

  private void ballSetGraphic(int value) {
    observableChange();
    ballDelayedEnablement = (value & 0x02) != 0;
    if (!ballVerticalDelay) ballEnabled = ballDelayedEnablement;
  }

  private void player0SetShape(int shape) {
    observableChange();
    // Missile size
    int speed = shape & 0x30;
    if     (speed == 0x00) speed = 8;    // Normal size = 8 = full speed = 1 pixel per clock
    else if  (speed == 0x10speed = 4;
    else if  (speed == 0x20) speed = 2;
    else if  (speed == 0x30speed = 1;
    if (missile0ScanSpeed != speed) {
      // if a copy is about to start, adjust for the new speed
      if (missile0ScanCounter > 7) missile0ScanCounter = 7 + (missile0ScanCounter - 7) / missile0ScanSpeed * speed;
      // if a copy is being scanned, kill the scan
      else if (missile0ScanCounter >= 0) missile0ScanCounter = -1;
      missile0ScanSpeed = speed;
    }
    // Player size and copies
    if ((shape & 0x07) == 0x05) {      // Double size = 1/2 speed
      speed = 2;
      player0CloseCopy = player0MediumCopy = player0WideCopy = false
    } else if ((shape & 0x07) == 0x07) {  // Quad size = 1/4 speed
      speed = 1;
      player0CloseCopy = player0MediumCopy = player0WideCopy = false;
    } else {
      speed = 4;              // Normal size = 4 = full speed = 1 pixel per clock
      player0CloseCopy =  (shape & 0x01) != 0
      player0MediumCopy = (shape & 0x02) != 0
      player0WideCopy =   (shape & 0x04) != 0;
    }
    if (player0ScanSpeed != speed) {
      // if a copy is about to start, adjust for the new speed
      if (player0ScanCounter > 31) player0ScanCounter = 31 + (player0ScanCounter - 31) / player0ScanSpeed * speed;
      // if a copy is being scanned, kill the scan
      else if (player0ScanCounter >= 0) player0ScanCounter = -1;
      player0ScanSpeed = speed;
    }
  }

  private void player1SetShape(int shape) {
    observableChange();
    // Missile size
    int speed = shape & 0x30;
    if     (speed == 0x00) speed = 8;    // Normal size = 8 = full speed = 1 pixel per clock
    else if  (speed == 0x10speed = 4;
    else if  (speed == 0x20) speed = 2;
    else if  (speed == 0x30speed = 1;
    if (missile1ScanSpeed != speed) {
      // if a copy is about to start, adjust for the new speed
      if (missile1ScanCounter > 7) missile1ScanCounter = 7 + (missile1ScanCounter - 7) / missile1ScanSpeed * speed;
      // if a copy is being scanned, kill the scan
      else if (missile1ScanCounter >= 0) missile1ScanCounter = -1;
      missile1ScanSpeed = speed;
    }
    // Player size and copies
    if ((shape & 0x07) == 0x05) {      // Double size = 1/2 speed
      speed = 2;
      player1CloseCopy = player1MediumCopy = player1WideCopy = false
    } else if ((shape & 0x07) == 0x07) {  // Quad size = 1/4 speed
      speed = 1;
      player1CloseCopy = player1MediumCopy = player1WideCopy = false;
    } else {
      speed = 4;              // Normal size = 4 = full speed = 1 pixel per clock
      player1CloseCopy =  (shape & 0x01) != 0
      player1MediumCopy = (shape & 0x02) != 0
      player1WideCopy =   (shape & 0x04) != 0;
    }
    if (player1ScanSpeed != speed) {
      // if a copy is about to start, adjust to produce the same start position
      if (player1ScanCounter > 31) player1ScanCounter = 31 + (player1ScanCounter - 31) / player1ScanSpeed * speed;
      // if a copy is being scanned, kill the scan
      else if (player1ScanCounter >= 0) player1ScanCounter = -1;
      player1ScanSpeed = speed;
    }
  }

  private void playfieldAndBallSetShape(int shape) {
    observableChange();
    final boolean reflect = (shape & 0x01) != 0;
    if (playfieldReflected != reflect) {
      playfieldReflected = reflect;
      playfieldPatternInvalid = true;
    }
    playfieldScoreMode = (shape & 0x02) != 0;
    playfieldPriority = (shape & 0x04) != 0;
    int speed = shape & 0x30;
    if     (speed == 0x00) speed = 8;    // Normal size = 8 = full speed = 1 pixel per clock
    else if  (speed == 0x10) speed = 4;
    else if  (speed == 0x20) speed = 2;
    else if  (speed == 0x30) speed = 1;
    if (ballScanSpeed != speed) {
      // if a copy is about to start, adjust for the new speed
      if (ballScanCounter > 7) ballScanCounter = 7 + (ballScanCounter - 7) / ballScanSpeed * speed;
      // if a copy is being scanned, kill the scan
      else if (ballScanCounter >= 0) ballScanCounter = -1;
      ballScanSpeed = speed;
    }
  }

  private void hitRESP0() {
    observableChangeExtended();
    if (debug) debugPixel(DEBUG_P0_RES_COLOR);

    // Hit in last pixel of HBLANK or after
    if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
      if (player0Counter != 155) player0RecentReset = true;       
      player0Counter = 155;
      return;
    }

    // Hit before last pixel of HBLANK
    int d = 0;                  // No HMOVE, displacement = 0
    if (hMoveHitBlank) {            // With HMOVE
      if (clock >= HBLANK_DURATION)      // During extended HBLANK
        d = (HBLANK_DURATION - clock) + 8;
      else {
        d = (clock - hMoveHitClock - 4) >> 2;
        if (d > 8) d = 8;
      }
    }
   
    player0Counter = 157 - d; 
    player0RecentReset = player0Counter <= 155;
  }
 
  private void hitRESP1() {
    observableChangeExtended();
    if (debug) debugPixel(DEBUG_P1_RES_COLOR);

    // Hit in last pixel of HBLANK or after
    if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
      if (player1Counter != 155) player1RecentReset = true;       
      player1Counter = 155;
      return;
    }

    // Hit before last pixel of HBLANK
    int d = 0;                  // No HMOVE, displacement = 0
    if (hMoveHitBlank) {            // With HMOVE
      if (clock >= HBLANK_DURATION)      // During extended HBLANK
        d = (HBLANK_DURATION - clock) + 8;
      else {
        d = (clock - hMoveHitClock - 4) >> 2;
        if (d > 8) d = 8;
      }
    }

    player1Counter = 157 - d; 
    player1RecentReset = player1Counter <= 155;
  }
 
  private void hitRESM0() {
    observableChangeExtended();
    if (debug) debugPixel(DEBUG_M0_COLOR);

    // Hit in last pixel of HBLANK or after
    if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
      if (missile0Counter != 155) missile0RecentReset = true;       
      missile0Counter = 155;
      return;
    }

    // Hit before last pixel of HBLANK
    int d = 0;                  // No HMOVE, displacement = 0
    if (hMoveHitBlank) {            // With HMOVE
      if (clock >= HBLANK_DURATION)      // During extended HBLANK
        d = (HBLANK_DURATION - clock) + 8;
      else {
        d = (clock - hMoveHitClock - 4) >> 2;
        if (d > 8) d = 8;
      }
    }

    missile0Counter = 157 - d; 
    missile0RecentReset = missile0Counter <= 155;
  }
 
  private void hitRESM1() {
    observableChangeExtended();
    if (debug) debugPixel(DEBUG_M1_COLOR);

    // Hit in last pixel of HBLANK or after
    if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
      if (missile1Counter != 155) missile1RecentReset = true;       
      missile1Counter = 155;
      return;
    }

    // Hit before last pixel of HBLANK
    int d = 0;                  // No HMOVE, displacement = 0
    if (hMoveHitBlank) {            // With HMOVE
      if (clock >= HBLANK_DURATION)      // During extended HBLANK
        d = (HBLANK_DURATION - clock) + 8;
      else {
        d = (clock - hMoveHitClock - 4) >> 2;
        if (d > 8) d = 8;
      }
    }

    missile1Counter = 157 - d; 
    missile1RecentReset = missile1Counter <= 155;
  }
 
  private void hitRESBL() {
    observableChange();
    if (debug) debugPixel(DEBUG_BL_COLOR);

    // Hit in last pixel of HBLANK or after
    if (clock >= HBLANK_DURATION + (hMoveHitBlank ? 8-1 : 0)) {
      ballCounter = 155;
      return;
    }

    // Hit before last pixel of HBLANK
    int d = 0;        // No HMOVE, displacement = 0
    if (hMoveHitBlank)    // With HMOVE
      if (clock >= HBLANK_DURATION)        // During extended HBLANK
        d = (HBLANK_DURATION - clock) + 8;
      else {
        d = (clock - hMoveHitClock - 4) >> 2;
        if (d > 8) d = 8;
      }

    ballCounter = 157 - d; 
  }
 
  private void hitHMOVE() {
    if (debug) debugPixel(DEBUG_HMOVE_COLOR);
    // Normal HMOVE
    if (clock < HBLANK_DURATION) {
      hMoveHitClock = clock;
      hMoveHitBlank = true;
      performHMOVE();
      return;
    }
    // Unsupported HMOVE
    if (clock < 219) {
      debugInfo("Unsupported HMOVE hit");
      return;
    }
    // Late HMOVE: Clocks [219-224] hide HMOVE blank next line, clocks [225, 0] produce normal behavior next line
    hMoveHitClock = 160 - clock;
    hMoveLateHit = true;
    hMoveLateHitBlank = clock >= 225
  }
 
  private void performHMOVE() {
    int add;
    boolean vis = false;
    add = (hMoveHitBlank ? HMP0 : HMP0 + 8); if (add != 0) {
      vis = true;
      if (add > 0) for (int i = add; i > 0; i--) player0ClockCounter();
      else {
        player0Counter += add; if (player0Counter < 0) player0Counter += 160;
        if (player0ScanCounter >= 0) player0ScanCounter -= player0ScanSpeed * add;
      }
    }
    add = (hMoveHitBlank ? HMP1 : HMP1 + 8); if (add != 0) {
      vis = true;
      if (add > 0) for (int i = add; i > 0; i--) player1ClockCounter();
      else {
        player1Counter += add; if (player1Counter < 0) player1Counter += 160;
        if (player1ScanCounter >= 0) player1ScanCounter -= player1ScanSpeed * add;
      }
    }
    add = (hMoveHitBlank ? HMM0 : HMM0 + 8); if (add != 0) {
      vis = true;
      if (add > 0) for (int i = add; i > 0; i--) missile0ClockCounter();
      else {
        missile0Counter += add; if (missile0Counter < 0) missile0Counter += 160;
        if (missile0ScanCounter >= 0) missile0ScanCounter -= missile0ScanSpeed * add;
      }
    }
    add = (hMoveHitBlank ? HMM1 : HMM1 + 8); if (add != 0) {
      vis = true;
      if (add > 0) for (int i = add; i > 0; i--) missile1ClockCounter();
      else {
        missile1Counter += add; if (missile1Counter < 0) missile1Counter += 160;
        if (missile1ScanCounter >= 0) missile1ScanCounter -= missile1ScanSpeed * add;
      }
    }
    add = (hMoveHitBlank ? HMBL : HMBL + 8); if (add != 0) {
      vis = true;
      if (add > 0) for (int i = add; i > 0; i--) ballClockCounter();
      else {
        ballCounter += add; if (ballCounter < 0) ballCounter += 160;
        if (ballScanCounter >= 0) ballScanCounter -= ballScanSpeed * add;
      }
    }
    if (vis) observableChange();
 
 
  private void missile0SetResetToPlayer(int res) {
    observableChange();
    if (missile0ResetToPlayer = (res & 0x02) != 0) missile0Enabled = false;
  }

  private void missile1SetResetToPlayer(int res) {
    observableChange();
    if (missile1ResetToPlayer = (res & 0x02) != 0) missile1Enabled = false;
  }

  private void vBlankSet(int blank) {
    if (((blank & 0x02) != 0) != vBlankOn) {  // Start the delayed decode for vBlank state change
      vBlankDecodeActive = true;
      vBlankNewState = !vBlankOn;
    }
    if ((blank & 0x40) != 0) {
      controlsButtonsLatched = true;      // Enable Joystick Button latches
    } else {               
      controlsButtonsLatched = false;      // Disable latches and update registers with the current button state
      if (controlsJOY0ButtonPressed) INPT4 &= 0x7f; else INPT4 |= 0x80;
      if (controlsJOY1ButtonPressed) INPT5 &= 0x7f; else INPT5 |= 0x80;
    }
    if ((blank & 0x80) != 0) {          // Ground paddle capacitors
      paddleCapacitorsGrounded = true;
      paddle0CapacitorCharge = paddle1CapacitorCharge = 0;
      INPT0 &= 0x7f; INPT1 &= 0x7f; INPT2 &= 0x7f; INPT3 &= 0x7f;
    } else
      paddleCapacitorsGrounded = false;
  }

  private void vBlankClockDecode() {
    vBlankDecodeActive = false;
    vBlankOn = vBlankNewState;
    if (debug) debugPixel(DEBUG_VBLANK_COLOR);
    observableChange();
  }

  private void finishLine() {
     // Fills the extended HBLANK portion of the current line if needed
    if (hMoveHitBlank) {
      linePixels[HBLANK_DURATION] =
      linePixels[HBLANK_DURATION + 1] =
      linePixels[HBLANK_DURATION + 2] =
      linePixels[HBLANK_DURATION + 3] =
      linePixels[HBLANK_DURATION + 4] =
      linePixels[HBLANK_DURATION + 5] =
      linePixels[HBLANK_DURATION + 6] =
      linePixels[HBLANK_DURATION + 7] = hBlankColor;    // This is faster than Arrays.fill()
      hMoveHitBlank = false;
    }
    // Perform late HMOVE hit if needed
    if (hMoveLateHit) {
      hMoveLateHit = false;
      hMoveHitBlank = hMoveLateHitBlank;
      performHMOVE();
    }
    // Extend pixel computation to the entire next line if needed
    if (observableChangeExtended) {
      lastObservableChangeClock = 227;
      observableChangeExtended = false;
    }
    // Inject debugging information in the line if needed
    if (debugLevel >= 2) processDebugPixelsInLine();
  }
 
  private void observableChange() {
    lastObservableChangeClock = clock;
    if (repeatLastLine) repeatLastLine = false
  }

  private void observableChangeExtended() {
    observableChange();
    observableChangeExtended = true
  }

  private void debug(int level) {
    debugLevel = level > 4 ? 0 : level;
    debug = debugLevel != 0;
    videoOutput.showOSD(debug ? "Debug Level " + debugLevel : "Debug OFF", true);
    bus.cpu.debug = debug;
    bus.pia.debug = debug;
    if (debug) debugSetColors();
    else debugRestoreColors();
  }

  private void debugSetColors() {
    player0Color = DEBUG_P0_COLOR;
    player1Color = DEBUG_P1_COLOR;
    missile0Color = DEBUG_M0_COLOR;
    missile1Color = DEBUG_M1_COLOR;
    ballColor = DEBUG_BL_COLOR;
    playfieldColor = DEBUG_PF_COLOR;
    playfieldBackground = DEBUG_BK_COLOR;
    hBlankColor = debugLevel >= 1 ? DEBUG_HBLANK_COLOR : HBLANK_COLOR;
    vBlankColor = debugLevel >= 2 ? DEBUG_VBLANK_COLOR : VBLANK_COLOR;
  }

  private void debugRestoreColors() {
    hBlankColor = HBLANK_COLOR;
    vBlankColor = VBLANK_COLOR;
    playfieldBackground = palette[0];
    Arrays.fill(linePixels, hBlankColor);
    observableChange();
  }

  private void debugInfo(String str) {
    if (debug) System.out.printf("Line: %3d, Pixel: %3d, " + str + "\n", videoOutput.monitor().currentLine(), clock);
  }
 
  private void debugPixel(int color) {
    debugPixels[clock] = color;
  }
 
  private boolean debugPausedMoreFrames() {
    if (debugPauseMoreFrames <= 0) return false;
    debugPauseMoreFrames--;
    return true;
  }

  private void processDebugPixelsInLine() {
    Arrays.fill(linePixels, 0, HBLANK_DURATION, hBlankColor);
    if (debugLevel >= 4 && videoOutput.monitor().currentLine() % 10 == 0)
      for (int i = 0; i < LINE_WIDTH; i++) {
        if (debugPixels[i] != 0) continue;
        if (i < HBLANK_DURATION) {
          if (i % 6 == 0 || i == 66 || i == 63)
            debugPixels[i] = DEBUG_MARKS_COLOR;
        } else {
          if ((i - HBLANK_DURATION - 1) % 6 == 0)
            debugPixels[i] = DEBUG_MARKS_COLOR;
        }
      }
    if (debugLevel >= 3)
      for (int i = 0; i < LINE_WIDTH; i++)
        if (debugPixels[i] != 0) {
          linePixels[i] = debugPixels[i];
          debugPixels[i] = 0;
        }
    observableChange();
  }
 
  private void paddlesChargeCapacitors() {
    if (INPT0 < 0x80 && ++paddle0CapacitorCharge >= paddle0Position) INPT0 |= 0x80;
    if (INPT1 < 0x80 && ++paddle1CapacitorCharge >= paddle1Position) INPT1 |= 0x80;
  }

  private void initLatchesAtPowerOn() {
    CXM0P = CXM1P = CXP0FB = CXP1FB = CXM0FB = CXM1FB = CXBLPF = CXPPMM = 0;
    INPT0 = INPT1 = INPT2 = INPT3 = 0;
    INPT4 = INPT5 = 0x80;
  }

  @Override
  public byte readByte(int address) {
    final int reg = address & READ_ADDRESS_MASK;

    if (reg == 0x00) return (byte) CXM0P;
    if (reg == 0x01) return (byte) CXM1P;
    if (reg == 0x02) return (byte) CXP0FB;
    if (reg == 0x03) return (byte) CXP1FB;
    if (reg == 0x04) return (byte) CXM0FB;
    if (reg == 0x05) return (byte) CXM1FB;
    if (reg == 0x06) return (byte) CXBLPF;
    if (reg == 0x07) return (byte) CXPPMM;
    if (reg == 0x08) return (byte) INPT0;
    if (reg == 0x09) return (byte) INPT1;
    if (reg == 0x0A) return (byte) INPT2;
    if (reg == 0x0B) return (byte) INPT3;
    if (reg == 0x0C) return (byte) INPT4;
    if (reg == 0x0D) return (byte) INPT5;

    // debugInfo(String.format("Invalid TIA read register address: %04x", address));
    return 0;
 

  @Override
  public void writeByte(int address, byte b) {
    final int i = b & 0xff;
    final int reg = address & WRITE_ADDRESS_MASK;
   
    if (reg == 0x1B) { /*GRP0   = i;*/ playerDelaySpriteChange(0, i); return; }
    if (reg == 0x1C) { /*GRP1   = i;*/ playerDelaySpriteChange(1, i); return; }
    if (reg == 0x02) { /*WSYNC  = i;*/ bus.cpu.RDY = false; if (debug) debugPixel(DEBUG_WSYNC_COLOR); return; }   // <STROBE> Halts the CPU until the next HBLANK
    if (reg == 0x2A) { /*HMOVE  = i;*/ hitHMOVE()return; }                
    if (reg == 0x0D) { if (PF0 != i || playfieldDelayedChangePart == 0) playfieldDelaySpriteChange(0, i); return; }
    if (reg == 0x0E) { if (PF1 != i || playfieldDelayedChangePart == 1) playfieldDelaySpriteChange(1, i); return; }
    if (reg == 0x0F) { if (PF2 != i || playfieldDelayedChangePart == 2) playfieldDelaySpriteChange(2, i); return; }
    if (reg == 0x06) { /*COLUP0 = i;*/ observableChange(); if (!debug) player0Color = missile0Color = palette[i]; return; }
    if (reg == 0x07) { /*COLUP1 = i;*/ observableChange(); if (!debug) player1Color = missile1Color = palette[i]; return; }
    if (reg == 0x08) { /*COLUPF = i;*/ observableChange(); if (!debug) playfieldColor = ballColor = palette[i]; return; }
    if (reg == 0x09) { /*COLUBK = i;*/ observableChange(); if (!debug) playfieldBackground = palette[i]; return; }
    if (reg == 0x1D) { /*ENAM0  = i;*/ observableChange(); missile0Enabled = (i & 0x02) != 0; return; }
    if (reg == 0x1E) { /*ENAM1  = i;*/ observableChange(); missile1Enabled = (i & 0x02) != 0; return; }
    if (reg == 0x14) { /*RESBL  = i;*/ hitRESBL(); return; }
    if (reg == 0x10) { /*RESP0  = i;*/ hitRESP0(); return; }
    if (reg == 0x11) { /*RESP1  = i;*/ hitRESP1(); return; }
    if (reg == 0x12) { /*RESM0  = i;*/ hitRESM0(); return; }
    if (reg == 0x13) { /*RESM1  = i;*/ hitRESM1(); return; }
    if (reg == 0x20) { HMP0   = (b >> 4); return; }
    if (reg == 0x21) { HMP1   = (b >> 4); return; }
    if (reg == 0x22) { HMM0   = (b >> 4); return; }
    if (reg == 0x23) { HMM1   = (b >> 4); return; }
    if (reg == 0x24) { HMBL   = (b >> 4); return; }
    if (reg == 0x2B) { /*HMCLR  = i;*/ HMP0 = HMP1 = HMM0 = HMM1 = HMBL = 0; return; }
    if (reg == 0x1F) { /*ENABL  = i;*/ ballSetGraphic(i); return; }
    if (reg == 0x04) { /*NUSIZ0 = i;*/ player0SetShape(i); return; }
    if (reg == 0x05) { /*NUSIZ1 = i;*/ player1SetShape(i); return; }
    if (reg == 0x0A) { /*CTRLPF = i;*/ playfieldAndBallSetShape(i); return; }
    if (reg == 0x0B) { /*REFP0  = i;*/ observableChange(); player0Reflected = (i & 0x08) != 0; return; }
    if (reg == 0x0C) { /*REFP1  = i;*/ observableChange(); player1Reflected = (i & 0x08) != 0; return; }
    if (reg == 0x25) { /*VDELP0 = i;*/ observableChange(); player0VerticalDelay = (i & 0x01) != 0; return; }
    if (reg == 0x26) { /*VDELP1 = i;*/ observableChange(); player1VerticalDelay = (i & 0x01) != 0; return; }
    if (reg == 0x27) { /*VDELBL = i;*/ observableChange(); ballVerticalDelay = (i & 0x01) != 0; return; }
    if (reg == 0x15) { AUDC0  = i; audioOutput.channel0().setControl(i & 0x0f); return; }
    if (reg == 0x16) { AUDC1  = i; audioOutput.channel1().setControl(i & 0x0f); return; }
    if (reg == 0x17) { AUDF0  = i; audioOutput.channel0().setDivider((i & 0x1f) + 1); return; }    // Bits 0-4, Divider from 1 to 32 )
    if (reg == 0x18) { AUDF1  = i; audioOutput.channel1().setDivider((i & 0x1f) + 1); return; }    // Bits 0-4, Divider from 1 to 32 )
    if (reg == 0x19) { AUDV0  = i; audioOutput.channel0().setVolume(i & 0x0f); return; }      // Bits 0-3, Volume from 0 to 15 )
    if (reg == 0x1A) { AUDV1  = i; audioOutput.channel1().setVolume(i & 0x0f); return; }      // Bits 0-3, Volume from 0 to 15 )
    if (reg == 0x28) { /*RESMP0 = i;*/ missile0SetResetToPlayer(i); return; }
    if (reg == 0x29) { /*RESMP1 = i;*/ missile1SetResetToPlayer(i); return; }
    if (reg == 0x01) { /*VBLANK = i;*/ vBlankSet(i); return; }
    if (reg == 0x00) { /*VSYNC  = i;*/ observableChange(); vSyncOn = (i & 0x02) != 0; if (debug) debugPixel(VSYNC_COLOR); return; }
    if (reg == 0x2C) { /*CXCLR  = i;*/ observableChange(); CXM0P = CXM1P = CXP0FB = CXP1FB = CXM0FB = CXM1FB = CXBLPF = CXPPMM = 0; return; }
    if (reg == 0x03) { /*RSYNC  = i;*/ /* clock = 0; */ return; }

    // debugInfo(String.format("Invalid TIA write register address: %04x value %d", address, b));
  }

  @Override
  public void controlStateChanged(ConsoleControls.Control control, boolean state) {
    switch (control) {
      case JOY0_BUTTON:
        if (state) {
          controlsJOY0ButtonPressed = true;
          INPT4 &= 0x7f;
        } else {
          controlsJOY0ButtonPressed = false;
          if (!controlsButtonsLatched)      // Does not lift the button if Latched Mode is on
            INPT4 |= 0x80;
        }
        return;
      case JOY1_BUTTON:
        if (state) {
          controlsJOY1ButtonPressed = true;
          INPT5 &= 0x7f;
        } else {
          controlsJOY1ButtonPressed = false;
          if (!controlsButtonsLatched)      // Does not lift the button if Latched Mode is on
            INPT5 |= 0x80;
        }
        return;
    }
    // Toggles
    if (!state) return;
    switch (control) {
      case DEBUG:
        debug(debugLevel + 1); return;
      case NO_COLLISIONS:
        debugNoCollisions = !debugNoCollisions;
        videoOutput.showOSD(debugNoCollisions ? "Collisions OFF" : "Collisions ON", true);
        return;
      case PAUSE:
        debugPause = !debugPause; debugPauseMoreFrames = 0;
        videoOutput.showOSD(debugPause ? "PAUSE" : "RESUME", true);
        return;
      case FRAME:
        debugPauseMoreFrames++; return;
      case TRACE:
        bus.cpu.trace = !bus.cpu.trace; return;
    }
  }

  @Override
  public void controlStateChanged(ConsoleControls.Control control, int position) {
    switch (control) {
      case PADDLE0_POSITION:
        paddle0Position = position; return;
      case PADDLE1_POSITION:
        paddle1Position = position; return;
    }
  }

  @Override
  public void controlsStateReport(Map<ConsoleControls.Control, Boolean> report) {
    //  No TIA controls visible outside by now
  }

  public TIAState saveState() {
    TIAState state = new TIAState();
    state.linePixels             =  linePixels.clone();
    state.lastObservableChangeClock       =  lastObservableChangeClock;
    state.observableChangeExtended       =  observableChangeExtended;
    state.repeatLastLine            =  repeatLastLine;
    state.vSyncOn                          =  vSyncOn;                   
    state.vBlankOn                         =  vBlankOn;
    state.vBlankDecodeActive         =  vBlankDecodeActive;
    state.vBlankNewState           =  vBlankNewState;
    state.playfieldPattern                 =  playfieldPattern.clone();
    state.playfieldPatternInvalid          =  playfieldPatternInvalid;   
    state.playfieldCurrentPixel            =  playfieldCurrentPixel;     
    state.playfieldColor                   =  playfieldColor;            
    state.playfieldBackground              =  playfieldBackground;       
    state.playfieldReflected               =  playfieldReflected;        
    state.playfieldScoreMode               =  playfieldScoreMode;        
    state.playfieldPriority                =  playfieldPriority;         
    state.player0ActiveSprite              =  player0ActiveSprite;       
    state.player0DelayedSprite             =  player0DelayedSprite;
    state.player0Color                     =  player0Color;              
    state.player0RecentReset              =  player0RecentReset;     
    state.player0Counter                 =  player0Counter;          
    state.player0ScanCounter             =  player0ScanCounter;      
    state.player0ScanSpeed                 =  player0ScanSpeed;          
    state.player0VerticalDelay             =  player0VerticalDelay;      
    state.player0CloseCopy                 =  player0CloseCopy;          
    state.player0MediumCopy                =  player0MediumCopy;
    state.player0WideCopy                  =  player0WideCopy;           
    state.player0Reflected                 =  player0Reflected;
    state.player1ActiveSprite              =  player1ActiveSprite;       
    state.player1DelayedSprite             =  player1DelayedSprite;
    state.player1Color                    =  player1Color;              
    state.player1RecentReset         =  player1RecentReset;     
    state.player1Counter                   =  player1Counter;            
    state.player1ScanCounter           =  player1ScanCounter;           
    state.player1ScanSpeed             =  player1ScanSpeed;           
    state.player1VerticalDelay             =  player1VerticalDelay;      
    state.player1CloseCopy                 =  player1CloseCopy;          
    state.player1MediumCopy                =  player1MediumCopy;
    state.player1WideCopy                  =  player1WideCopy;           
    state.player1Reflected                 =  player1Reflected;
    state.missile0Enabled                  =  missile0Enabled;           
    state.missile0Color                    =  missile0Color;             
    state.missile0RecentReset             =  missile0RecentReset;    
    state.missile0Counter                  =  missile0Counter;           
    state.missile0ScanCounter              =  missile0ScanCounter;       
    state.missile0ScanSpeed             =  missile0ScanSpeed;           
    state.missile0ResetToPlayer           =  missile0ResetToPlayer;         
    state.missile1Enabled                  =  missile1Enabled;           
    state.missile1Color                    =  missile1Color;             
    state.missile1RecentReset             =  missile1RecentReset;    
    state.missile1Counter                  =  missile1Counter;           
    state.missile1ScanCounter              =  missile1ScanCounter;       
    state.missile1ScanSpeed             =  missile1ScanSpeed;           
    state.missile1ResetToPlayer           =  missile1ResetToPlayer;         
    state.ballEnabled                      =  ballEnabled;               
    state.ballDelayedEnablement            =  ballDelayedEnablement;     
    state.ballColor                        =  ballColor;                 
    state.ballCounter                      =  ballCounter;               
    state.ballScanCounter                  =  ballScanCounter;           
    state.ballScanSpeed               =  ballScanSpeed;           
    state.ballVerticalDelay                =  ballVerticalDelay;         
    state.playfieldDelayedChangeClock     =  playfieldDelayedChangeClock;
    state.playfieldDelayedChangePart     =  playfieldDelayedChangePart;
    state.playfieldDelayedChangePattern     =  playfieldDelayedChangePattern;
    state.playersDelayedSpriteChanges      =  ArraysCopy.copy2D(playersDelayedSpriteChanges);
    state.playersDelayedSpriteChangesCount =  playersDelayedSpriteChangesCount;
    state.hMoveHitBlank             =  hMoveHitBlank;
    state.hMoveHitClock             =  hMoveHitClock;
    state.PF0                   =  PF0;   
    state.PF1                   =  PF1;
    state.PF2                   =  PF2;     
    state.AUDC0                   =  AUDC0;
    state.AUDC1                   =  AUDC1;
    state.AUDF0                   =  AUDF0;
    state.AUDF1                   =  AUDF1;
    state.AUDV0                   =  AUDV0;
    state.AUDV1                   =  AUDV1;
    state.HMP0                   =  HMP0;
    state.HMP1                   =  HMP1;   
    state.HMM0                   =  HMM0;   
    state.HMM1                   =  HMM1;   
    state.HMBL                   =  HMBL;   
    state.CXM0P                  =  CXM0P; 
    state.CXM1P                  =  CXM1P; 
    state.CXP0FB                  =  CXP0FB;
    state.CXP1FB                  =  CXP1FB;
    state.CXM0FB                  =  CXM0FB;
    state.CXM1FB                  =  CXM1FB;
    state.CXBLPF                  =  CXBLPF;
    state.CXPPMM                  =  CXPPMM;
    return state;
  }

  public void loadState(TIAState state) {
    linePixels             =  state.linePixels;
    lastObservableChangeClock     =  state.lastObservableChangeClock;
    observableChangeExtended     =  state.observableChangeExtended;
    repeatLastLine            =  state.repeatLastLine;
    vSyncOn                        =  state.vSyncOn;                    
    vBlankOn                       =  state.vBlankOn;
    vBlankDecodeActive         =  state.vBlankDecodeActive;
    vBlankNewState            =  state.vBlankNewState;
    playfieldPattern               =  state.playfieldPattern;           
    playfieldPatternInvalid        =  state.playfieldPatternInvalid;    
    playfieldCurrentPixel          =  state.playfieldCurrentPixel;      
    playfieldColor                 =  state.playfieldColor;             
    playfieldBackground            =  state.playfieldBackground;        
    playfieldReflected             =  state.playfieldReflected;         
    playfieldScoreMode             =  state.playfieldScoreMode;         
    playfieldPriority              =  state.playfieldPriority;          
    player0ActiveSprite            =  state.player0ActiveSprite;        
    player0DelayedSprite           =  state.player0DelayedSprite;
    player0Color                   =  state.player0Color;               
    player0RecentReset             =  state.player0RecentReset;      
    player0Counter                 =  state.player0Counter;           
    player0ScanCounter             =  state.player0ScanCounter;       
    player0ScanSpeed               =  state.player0ScanSpeed;           
    player0VerticalDelay           =  state.player0VerticalDelay;       
    player0CloseCopy               =  state.player0CloseCopy;           
    player0MediumCopy              =  state.player0MediumCopy;
    player0WideCopy                =  state.player0WideCopy;            
    player0Reflected               =  state.player0Reflected;
    player1ActiveSprite            =  state.player1ActiveSprite;        
    player1DelayedSprite           =  state.player1DelayedSprite;
    player1Color                   =  state.player1Color;               
    player1RecentReset            =  state.player1RecentReset;      
    player1Counter                 =  state.player1Counter;             
    player1ScanCounter         =  state.player1ScanCounter;      
    player1ScanSpeed         =  state.player1ScanSpeed;        
    player1VerticalDelay           =  state.player1VerticalDelay;       
    player1CloseCopy               =  state.player1CloseCopy;           
    player1MediumCopy              =  state.player1MediumCopy;
    player1WideCopy                =  state.player1WideCopy;            
    player1Reflected               =  state.player1Reflected;
    missile0Enabled                =  state.missile0Enabled;            
    missile0Color                  =  state.missile0Color;              
    missile0RecentReset            =  state.missile0RecentReset;     
    missile0Counter                =  state.missile0Counter;            
    missile0ScanCounter            =  state.missile0ScanCounter;        
    missile0ScanSpeed         =  state.missile0ScanSpeed;        
    missile0ResetToPlayer       =  state.missile0ResetToPlayer;      
    missile1Enabled                =  state.missile1Enabled;            
    missile1Color                  =  state.missile1Color;              
    missile1RecentReset            =  state.missile1RecentReset;     
    missile1Counter                =  state.missile1Counter;            
    missile1ScanCounter            =  state.missile1ScanCounter;        
    missile1ScanSpeed         =  state.missile1ScanSpeed;        
    missile1ResetToPlayer       =  state.missile1ResetToPlayer;      
    ballEnabled                    =  state.ballEnabled;                
    ballDelayedEnablement          =  state.ballDelayedEnablement;      
    ballColor                      =  state.ballColor;                  
    ballCounter                    =  state.ballCounter;                
    ballScanCounter                =  state.ballScanCounter;            
    ballScanSpeed           =  state.ballScanSpeed;        
    ballVerticalDelay              =  state.ballVerticalDelay;          
    playfieldDelayedChangeClock     =  state.playfieldDelayedChangeClock;
    playfieldDelayedChangePart     =  state.playfieldDelayedChangePart;
    playfieldDelayedChangePattern   =  state.playfieldDelayedChangePattern;
    playersDelayedSpriteChanges      =  state.playersDelayedSpriteChanges;     
    playersDelayedSpriteChangesCount =  state.playersDelayedSpriteChangesCount;
    hMoveHitBlank           =  state.hMoveHitBlank;
    hMoveHitClock           =  state.hMoveHitClock;
    PF0                 =  state.PF0;
    PF1                 =  state.PF1;
    PF2                 =  state.PF2;
    AUDC0               =  state.AUDC0; audioOutput.channel0().setControl(AUDC0 & 0x0f);    // Also update the Audio Generator
    AUDC1               =  state.AUDC1; audioOutput.channel1().setControl(AUDC1 & 0x0f);
    AUDF0               =  state.AUDF0; audioOutput.channel0().setDivider((AUDF0 & 0x1f) + 1);  
    AUDF1               =  state.AUDF1; audioOutput.channel1().setDivider((AUDF1 & 0x1f) + 1);
    AUDV0               =  state.AUDV0; audioOutput.channel0().setVolume(AUDV0 & 0x0f);
    AUDV1               =  state.AUDV1; audioOutput.channel1().setVolume(AUDV1 & 0x0f);
    HMP0               =  state.HMP0;
    HMP1               =  state.HMP1;   
    HMM0               =  state.HMM0;
    HMM1               =  state.HMM1;   
    HMBL               =  state.HMBL;
    CXM0P                =  state.CXM0P;
    CXM1P                =  state.CXM1P;
    CXP0FB               =  state.CXP0FB;
    CXP1FB               =  state.CXP1FB;
    CXM0FB               =  state.CXM0FB;
    CXM1FB               =  state.CXM1FB;
    CXBLPF               =  state.CXBLPF;
    CXPPMM               =  state.CXPPMM;
    if (debug) debugSetColors();            // IF debug is on, ensure debug colors are used
  }
 
 
  // Variables ----------------------------------------------
 
  private final VideoGenerator videoOutput;
  private final AudioMonoGenerator audioOutput;
 
  private int clock = 0;

  private BUS bus;

  private boolean powerOn = false;
  private final int debugPixels[] = new int[LINE_WIDTH];
 
  private int[] palette;
  private int vSyncColor = VSYNC_COLOR;
  private int vBlankColor = VBLANK_COLOR;
  private int hBlankColor = VBLANK_COLOR;

  private boolean debugPause = false;
  private int debugPauseMoreFrames = 0;
 
  private int[] framePattern = { 1 };
  private int   framePatternPosition = 0;
 

  // State Variables ----------------------------------------------

  private boolean debug = false;
  private int debugLevel = 0;
  private boolean debugNoCollisions = false;

  private int[] linePixels = new int[LINE_WIDTH];
  private int lastObservableChangeClock = -1;
  private boolean observableChangeExtended = false;
  private boolean repeatLastLine;

  private boolean vSyncOn = false;

  private boolean vBlankOn = false;
  private boolean vBlankDecodeActive = false;
  private boolean vBlankNewState;

  private boolean hMoveHitBlank = false;
  private int hMoveHitClock = -1;
  private boolean hMoveLateHit = false;
  private boolean hMoveLateHitBlank = false;
 
  private boolean[] playfieldPattern = new boolean[40];
  private boolean playfieldPatternInvalid = true;
  private boolean playfieldCurrentPixel = false;
  private int playfieldColor = 0xff000000;
  private int playfieldBackground = 0xff000000;
  private boolean playfieldReflected = false;
  private boolean playfieldScoreMode = false;
  private boolean playfieldPriority = false;

  private int playfieldDelayedChangeClock = -1;
  private int playfieldDelayedChangePart = -1;      // Supports only one delayed change at a time.
  private int playfieldDelayedChangePattern = -1;
 
  private int player0ActiveSprite = 0;
  private int player0DelayedSprite = 0;
  private int player0Color = 0xff000000;
  private boolean player0RecentReset = false;
  private int player0Counter = 0;              // Position! 
  private int player0ScanCounter = -1;          // 31 down to 0. Current scan position. Negative = scan not happening 
  private int player0ScanSpeed = 4;            // Decrement ScanCounter. 4 per clock = 1 pixel wide
  private boolean player0VerticalDelay = false;
  private boolean player0CloseCopy = false;
  private boolean player0MediumCopy = false;
  private boolean player0WideCopy = false;
  private boolean player0Reflected = false;
 
  private int player1ActiveSprite = 0;
  private int player1DelayedSprite = 0;
  private int player1Color = 0xff000000;
  private boolean player1RecentReset = false;
  private int player1Counter = 0;
  private int player1ScanCounter = -1;
  private int player1ScanSpeed = 4;
  private boolean player1VerticalDelay = false;
  private boolean player1CloseCopy = false;
  private boolean player1MediumCopy = false;
  private boolean player1WideCopy = false;
  private boolean player1Reflected = false;
 
  private boolean missile0Enabled = false;
  private int missile0Color = 0xff000000;
  private boolean missile0RecentReset = false;
  private int missile0Counter = 0;
  private int missile0ScanCounter = -1;
  private int missile0ScanSpeed = 8;            // 8 per clock = 1 pixel wide
  private boolean missile0ResetToPlayer = false;

  private boolean missile1Enabled = false;
  private int missile1Color = 0xff000000;
  private boolean missile1RecentReset = false;
  private int missile1Counter = 0;
  private int missile1ScanCounter = -1;
  private int missile1ScanSpeed = 8;
  private boolean missile1ResetToPlayer = false;
 
  private boolean ballEnabled = false;
  private boolean ballDelayedEnablement = false;
  private int ballColor = 0xff000000;
  private int ballCounter = 0;
  private int ballScanCounter = -1;
  private int ballScanSpeed = 8;              // 8 per clock = 1 pixel wide     
  private boolean ballVerticalDelay = false;

  private int[][] playersDelayedSpriteChanges = new int[PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT][3];
  private int playersDelayedSpriteChangesCount = 0;

  private boolean controlsButtonsLatched = false;
  private boolean controlsJOY0ButtonPressed = false;
  private boolean controlsJOY1ButtonPressed = false;

  private boolean paddleCapacitorsGrounded = false;
  private int paddle0Position = -1;      // 380 = Left, 190 = Middle, 0 = Right. -1 = disconnected, won't charge POTs
  private int paddle0CapacitorCharge = 0;
  private int paddle1Position = -1;
  private int paddle1CapacitorCharge = 0;

  // Read registers -------------------------------------------

  private int CXM0P;     // collision M0-P1, M0-P0 (Bit 7,6)
  private int CXM1P;     // collision M1-P0, M1-P1
  private int CXP0FB;    // collision P0-PF, P0-BL
  private int CXP1FB;    // collision P1-PF, P1-BL
  private int CXM0FB;    // collision M0-PF, M0-BL
  private int CXM1FB;    // collision M1-PF, M1-BL
  private int CXBLPF;    // collision BL-PF, unused
  private int CXPPMM;    // collision P0-P1, M0-M1
  private int INPT0;     // Paddle0 Left pot port
  private int INPT1;     // Paddle0 Right pot port
  private int INPT2;     // Paddle1 Left pot port
  private int INPT3;     // Paddle1 Right pot port
  private int INPT4;     // input (Joy0 button)
  private int INPT5;     // input (Joy1 button)

 
  // Write registers -------------------------------------------

//  private int VSYNC;    // ......1.  vertical sync set-clear
//  private int VBLANK;    // 11....1.  vertical blank set-clear
//  private int WSYNC;    // <strobe>  wait for leading edge of horizontal blank
//  private int RSYNC;    // <strobe>  reset horizontal sync counter
//  private int NUSIZ0;    // ..111111  number-size player-missile 0
//  private int NUSIZ1;    // ..111111  number-size player-missile 1
//  private int COLUP0;    // 1111111.  color-lum player 0 and missile 0
//  private int COLUP1;    // 1111111.  color-lum player 1 and missile 1
//  private int COLUPF;    // 1111111.  color-lum playfield and ball
//  private int COLUBK;    // 1111111.  color-lum background
//  private int CTRLPF;    // ..11.111  control playfield ball size & collisions
//  private int REFP0;    // ....1...  reflect player 0
//  private int REFP1;    // ....1...  reflect player 1
  private int PF0;    // 1111....  playfield register byte 0
  private int PF1;    // 11111111  playfield register byte 1
  private int PF2;    // 11111111  playfield register byte 2
//  private int RESP0;    // <strobe>  reset player 0
//  private int RESP1;    // <strobe>  reset player 1
//  private int RESM0;    // <strobe>  reset missile 0
//  private int RESM1;    // <strobe>  reset missile 1
//  private int RESBL;    // <strobe>  reset ball
  private int AUDC0;    // ....1111  audio control 0
  private int AUDC1;    // ....1111  audio control 1
  private int AUDF0;    // ...11111  audio frequency 0
  private int AUDF1;    // ...11111  audio frequency 1
  private int AUDV0;    // ....1111  audio volume 0
  private int AUDV1;    // ....1111  audio volume 1
//  private int GRP0;    // 11111111  graphics player 0
//  private int GRP1;    // 11111111  graphics player 1
//  private int ENAM0;    // ......1.  graphics (enable) missile 0
//  private int ENAM1;    // ......1.  graphics (enable) missile 1
//  private int ENABL;    // ......1.  graphics (enable) ball
  private int HMP0;    // 1111....  horizontal motion player 0
  private int HMP1;    // 1111....  horizontal motion player 1
  private int HMM0;    // 1111....  horizontal motion missile 0
  private int HMM1;    // 1111....  horizontal motion missile 1
  private int HMBL;    // 1111....  horizontal motion ball
//  private int VDELP0;    // .......1  vertical delay player 0
//  private int VDELP1;    // .......1  vertical delay player 1
//  private int VDELBL;    // .......1  vertical delay ball
//  private int RESMP0;    // ......1.  reset missile 0 to player 0
//  private int RESMP1;    // ......1.  reset missile 1 to player 1
//  private int HMOVE;    // <strobe>  apply horizontal motion
//  private int HMCLR;    // <strobe>  clear horizontal motion registers
//  private int CXCLR;    // <strobe>  clear collision latches


  // Constants --------------------------------------------------
 
  public static final int CHIP_MASK = 0x1080;
  public static final int CHIP_SELECT = 0x0000;
 
  private static final int READ_ADDRESS_MASK  = 0x000f;
  private static final int WRITE_ADDRESS_MASK = 0x003f;
 
  private static final int VBLANK_COLOR = 0x00000000;    // Full transparency needed for CRT emulation modes
  private static final int HBLANK_COLOR = 0xff000000;
  private static final int VSYNC_COLOR = 0xffdddddd;

  private static final int HBLANK_DURATION = 68;
  private static final int LINE_WIDTH = 228;

  private static final int DEBUG_MARKS_COLOR  = 0xff202020;
  private static final int DEBUG_HBLANK_COLOR = 0xff444444;
  private static final int DEBUG_VBLANK_COLOR = 0xff2a2a2a;
 
  private static final int DEBUG_WSYNC_COLOR  = 0xff880088;
  private static final int DEBUG_HMOVE_COLOR  = 0xffffffff;

  private static final int DEBUG_P0_COLOR     = 0xff0000ff;
  private static final int DEBUG_P0_RES_COLOR = 0xff2222bb;
  private static final int DEBUG_P0_GR_COLOR  = 0xff111177;
  private static final int DEBUG_P1_COLOR     = 0xffff0000;
  private static final int DEBUG_P1_RES_COLOR = 0xffbb2222;
  private static final int DEBUG_P1_GR_COLOR  = 0xff771111;
  private static final int DEBUG_M0_COLOR     = 0xff6666ff;
  private static final int DEBUG_M1_COLOR     = 0xffff6666;

  private static final int DEBUG_PF_COLOR     = 0xff448844;
  private static final int DEBUG_PF_GR_COLOR  = 0xff33dd33;
  private static final int DEBUG_BK_COLOR     = 0xff334433;
  private static final int DEBUG_BL_COLOR     = 0xffffff00;

  @SuppressWarnings("unused")
  private static final int DEBUG_SP_COLOR    = 0xff00ffff;
  @SuppressWarnings("unused")
  private static final int DEBUG_SP_COLOR2   = 0xffff00ff;

  private static final int PLAYERS_DELAYED_SPRITE_GHANGES_MAX_COUNT = 50// Supports a maximum of player GR changes before any is drawn
 
  private static final double FORCED_CLOCK = Parameters.TIA_FORCED_CLOCK;  //  TIA Real Clock = NTSC clock = 3584160 or 3579545 Hz


  // Used to save/load states
  public static class TIAState implements Serializable {
    int[] linePixels;
    int lastObservableChangeClock;
    boolean observableChangeExtended;
    boolean repeatLastLine;
    boolean vSyncOn;
    boolean vBlankOn;
    boolean vBlankDecodeActive;
    boolean vBlankNewState;
    boolean[] playfieldPattern;
    boolean playfieldPatternInvalid;
    boolean playfieldCurrentPixel;
    int playfieldColor;
    int playfieldBackground;
    boolean playfieldReflected;
    boolean playfieldScoreMode;
    boolean playfieldPriority;
    int player0ActiveSprite;
    int player0DelayedSprite;
    int player0Color;
    boolean player0RecentReset;
    int player0Counter; 
    int player0ScanCounter; 
    int player0ScanSpeed;
    boolean player0VerticalDelay;
    boolean player0CloseCopy;
    boolean player0MediumCopy;
    boolean player0WideCopy;
    boolean player0Reflected;
    int player1ActiveSprite;
    int player1DelayedSprite;
    int player1Color;
    boolean player1RecentReset;
    int player1Counter;
    int player1ScanCounter;           
    int player1ScanSpeed;           
    boolean player1VerticalDelay;
    boolean player1CloseCopy;
    boolean player1MediumCopy;
    boolean player1WideCopy;
    boolean player1Reflected;
    boolean missile0Enabled;
    int missile0Color;
    boolean missile0RecentReset;
    int missile0Counter;
    int missile0ScanCounter;
    int missile0ScanSpeed;           
    boolean missile0ResetToPlayer;         
    boolean missile1Enabled;
    int missile1Color;
    boolean missile1RecentReset;
    int missile1Counter;
    int missile1ScanCounter;
    int missile1ScanSpeed;           
    boolean missile1ResetToPlayer;         
    boolean ballEnabled;
    boolean ballDelayedEnablement;
    int ballColor;
    int ballCounter;
    int ballScanCounter;
    int ballScanSpeed;           
    boolean ballVerticalDelay;
    int playfieldDelayedChangeClock;
    int playfieldDelayedChangePart;
    int playfieldDelayedChangePattern;
    int[][] playersDelayedSpriteChanges;
    int playersDelayedSpriteChangesCount;
    boolean hMoveHitBlank;
    int hMoveHitClock;
    int PF0;
    int PF1;
    int PF2;
    int AUDC0;
    int AUDC1;
    int AUDF0;   
    int AUDF1;
    int AUDV0;
    int AUDV1;
    int HMP0;
    int HMP1;   
    int HMM0;
    int HMM1;   
    int HMBL;
    int CXM0P;
    int CXM1P;
    int CXP0FB;
    int CXP1FB;
    int CXM0FB;
    int CXM1FB;
    int CXBLPF;
    int CXPPMM;

    public static final long serialVersionUID = 3L;
  }

}
TOP

Related Classes of org.javatari.atari.tia.TIA

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.