/* * 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.apple2e; import jace.config.ConfigurableField; import jace.core.Computer; import jace.core.RAMEvent; import jace.core.RAMListener; import jace.core.Video; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.util.HashSet; import java.util.Set; /** * Provides a clean color monitor simulation, complete with text-friendly * palette and mixed color/bw (mode 7) rendering. This class extends the * VideoDHGR class to provide all necessary video writers and other rendering * mechanics, and then overrides the actual output routines (showBW, showDhgr) with more suitable * (and much prettier) alternatives. Rather than draw to the video buffer every * cycle, rendered screen info is pushed into a buffer with mask bits (to * indicate B&W vs color) And the actual conversion happens at the end of the * scanline during the HBLANK period. This video rendering was inspired by * Blargg but was ultimately rewritten from scratch once the color palette was * implemented. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class VideoNTSC extends VideoDHGR { @ConfigurableField(name = "Text palette", shortName = "textPalette", defaultValue = "false", description = "Use text-friendly color palette") public static boolean useTextPalette = false; static int activePalette[][]; @ConfigurableField(name = "Video 7", shortName = "video7", defaultValue = "true", description = "Enable Video 7 RGB rendering support") public static boolean enableVideo7 = false; // Scanline represents 560 bits, divided up into 28-bit words int[] scanline = new int[20]; static int[] divBy28 = new int[560]; static { for (int i = 0; i < 560; i++) { divBy28[i] = i / 28; } } int pos = 0; int lastKnownY = -1; boolean colorActive = false; int rowStart = 0; @Override protected void showBW(BufferedImage screen, int xOffset, int y, int dhgrWord) { if (lastKnownY != y) { lastKnownY = y; pos = rowStart = divBy28[xOffset]; colorActive = false; } else { if (pos > 20) pos-=20; } doDisplay(screen, xOffset, y, dhgrWord); } @Override protected void showDhgr(BufferedImage screen, int xOffset, int y, int dhgrWord) { if (lastKnownY != y) { lastKnownY = y; pos = rowStart = divBy28[xOffset]; colorActive = true; } doDisplay(screen, xOffset, y, dhgrWord); } @Override protected void displayLores(BufferedImage screen, int xOffset, int y, int rowAddress) { // Skip odd columns since this does two at once if ((xOffset & 0x01) == 1) { return; } if (lastKnownY != y) { lastKnownY = y; pos = rowStart = divBy28[xOffset]; colorActive = true; } int c1 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF; if ((y & 7) < 4) { c1 &= 15; } else { c1 >>= 4; } int c2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1) & 0x0FF; if ((y & 7) < 4) { c2 &= 15; } else { c2 >>= 4; } int pat = c1 | c1 << 4 | c1 << 8 | (c1 & 3) << 12; pat |= (c2 & 12) << 12 | c2 << 16 | c2 << 20 | c2 << 24; scanline[pos++] = pat; } private void doDisplay(BufferedImage screen, int xOffset, int y, int dhgrWord) { if (pos >= 20) pos -= 20; scanline[pos] = dhgrWord; pos++; } @Override public void hblankStart(BufferedImage screen, int y, boolean isDirty) { if (isDirty) { renderScanline(screen, y); } lastKnownY = -1; } // Offset is based on location in graphics buffer that corresponds with the row and // a number (0-20) that represents how much of the scanline was rendered // This is based off the xyOffset but is different because of P static int pyOffset[][]; static { pyOffset = new int[192][21]; for (int y = 0; y < 192; y++) { for (int p = 0; p < 21; p++) { pyOffset[y][p] = (y * 560) + (p * 28); } } } private void renderScanline(BufferedImage screen, int y) { DataBuffer b = screen.getRaster().getDataBuffer(); try { // This is equivilant to y*560 but is 5% faster //int yOffset = ((y << 4) + (y << 5) + (y << 9))+xOffset; // For some reason this jumps up to 40 in the wayout title screen (?) int p = pyOffset[y][rowStart]; if (rowStart > 0) { getCurrentWriter().markDirty(y); } // Reset scanline position if (colorActive && (!dhgrMode || !enableVideo7 || graphicsMode.isColor())) { int byteCounter = 0; for (int s = rowStart; s < 20; s++) { int add = 0; int bits; if (hiresMode) { bits = scanline[s] << 2; if (s > 0) { bits |= (scanline[s - 1] >> 26) & 3; } } else { bits = scanline[s] << 3; if (s > 0) { bits |= (scanline[s - 1] >> 25) & 7; } } if (s < 19) { add = (scanline[s + 1] & 7); } boolean isBW = false; if (enableVideo7 && dhgrMode && graphicsMode == rgbMode.mix) { for (int i = 0; i < 28; i++) { if (i % 7 == 0) { isBW = !hiresMode && !useColor[byteCounter]; byteCounter++; } if (isBW) { b.setElem(p++, ((bits & 0x8) == 0) ? BLACK : WHITE); } else { b.setElem(p++, activePalette[i % 4][bits & 0x07f]); } bits >>= 1; if (i == 20) { bits |= add << (hiresMode ? 9 : 10); } } } else { for (int i = 0; i < 28; i++) { b.setElem(p++, activePalette[i % 4][bits & 0x07f]); bits >>= 1; if (i == 20) { bits |= add << (hiresMode ? 9 : 10); } } } } } else { for (int s = rowStart; s < 20; s++) { int bits = scanline[s]; for (int i = 0; i < 28; i++) { b.setElem(p++, ((bits & 1) == 0) ? BLACK : WHITE); bits >>= 1; } } } } catch (ArrayIndexOutOfBoundsException ex) { // Flag this scanline to be written again, something screwed up! // This only happens during a race condition when the video // mode changes at just the wrong time. getCurrentWriter().markDirty(y); } } // y Range [0,1] public static final double MIN_Y = 0; public static final double MAX_Y = 1; // i Range [-0.5957, 0.5957] public static final double MAX_I = 0.5957; // q Range [-0.5226, 0.5226] public static final double MAX_Q = 0.5226; static final int solidPalette[][] = new int[4][128]; static final int textPalette[][] = new int[4][128]; static final double[][] yiq = { {0.0, 0.0, 0.0}, //0000 0 {0.25, 0.5, 0.5}, //0001 1 {0.25, -0.5, 0.5}, //0010 2 {0.5, 0.0, 1.0}, //0011 3 +Q {0.25, -0.5, -0.5}, //0100 4 {0.5, 0.0, 0.0}, //0101 5 {0.5, -1.0, 0.0}, //0110 6 +I {0.75, -0.5, 0.5}, //0111 7 {0.25, 0.5, -0.5}, //1000 8 {0.5, 1.0, 0.0}, //1001 9 -I {0.5, 0.0, 0.0}, //1010 a {0.75, 0.5, 0.5}, //1011 b {0.5, 0.0, -1.0}, //1100 c -Q {0.75, 0.5, -0.5}, //1101 d {0.75, -0.5, -0.5}, //1110 e {1.0, 0.0, 0.0}, //1111 f }; static { int maxLevel = 10; for (int offset = 0; offset < 4; offset++) { for (int pattern = 0; pattern < 128; pattern++) { int level = (pattern & 1) + ((pattern >> 1) & 1) * 1 + ((pattern >> 2) & 1) * 2 + ((pattern >> 3) & 1) * 4 + ((pattern >> 4) & 1) * 2 + ((pattern >> 5) & 1) * 1; int col = (pattern >> 2) & 0x0f; for (int rot = 0; rot < offset; rot++) { col = ((col & 8) >> 3) | ((col << 1) & 0x0f); } double y1 = yiq[col][0]; double y2 = ((double) level / (double) maxLevel); solidPalette[offset][pattern] = (255 << 24) | yiqToRgb(y1, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q); textPalette[offset][pattern] = (255 << 24) | yiqToRgb(y2, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q); } } // Avoid NPE just in case. activePalette = solidPalette; } static public int yiqToRgb(double y, double i, double q) { int r = (int) (normalize((y + 0.956 * i + 0.621 * q), 0, 1) * 255); int g = (int) (normalize((y - 0.272 * i - 0.647 * q), 0, 1) * 255); int b = (int) (normalize((y - 1.105 * i + 1.702 * q), 0, 1) * 255); return (r << 16) | (g << 8) | b; } public static double normalize(double x, double minX, double maxX) { if (x < minX) { return minX; } if (x > maxX) { return maxX; } return x; } @Override public void reconfigure() { detach(); activePalette = useTextPalette ? textPalette : solidPalette; super.reconfigure(); attach(); } // The following section captures changes to the RGB mode // The details of this are in Brodener's patent application #4631692 // http://www.freepatentsonline.com/4631692.pdf // as well as the AppleColor adapter card manual // http://apple2.info/download/Ext80ColumnAppleColorCardHR.pdf rgbMode graphicsMode = rgbMode.color; public static enum rgbMode { color(true), mix(true), bw(false), _160col(false); boolean colorMode = false; rgbMode(boolean c) { this.colorMode = c; } public boolean isColor() { return colorMode; } } public static enum ModeStateChanges { SET_AN3, CLEAR_AN3, SET_80, CLEAR_80; } boolean f1 = true; boolean f2 = true; boolean an3 = true; public void rgbStateChange(ModeStateChanges state) { switch (state) { case CLEAR_80: break; case CLEAR_AN3: an3 = false; break; case SET_80: break; case SET_AN3: if (!an3) { f2 = f1; f1 = SoftSwitches._80COL.getState(); } an3 = true; break; } // This is the more technically correct implementation except for two issues: // 1) 160-column mode isn't implemented so it's not worth bothering to capture that state // 2) A lot of programs are clueless about RGB modes so it's good to default to normal color mode // graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode._160col : rgbMode.bw); graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode.color : rgbMode.bw); // System.out.println(state + ": "+ graphicsMode); } // These catch changes to the RGB mode to toggle between color, BW and mixed static Set rgbStateListeners = new HashSet<>(); static { rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { @Override protected void doConfig() { setScopeStart(0x0c05e); } @Override protected void doEvent(RAMEvent e) { Video v = Computer.getComputer().getVideo(); if (v instanceof VideoNTSC) { ((VideoNTSC) v).rgbStateChange(ModeStateChanges.CLEAR_AN3); } } }); rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { @Override protected void doConfig() { setScopeStart(0x0c05f); } @Override protected void doEvent(RAMEvent e) { Video v = Computer.getComputer().getVideo(); if (v instanceof VideoNTSC) { ((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_AN3); } } }); rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { @Override protected void doConfig() { setScopeStart(0x0fa62); } @Override protected void doEvent(RAMEvent e) { Video v = Computer.getComputer().getVideo(); if (v instanceof VideoNTSC) { // When reset hook is called, reset the graphics mode // This is useful in case a program is running that // is totally clueless how to set the RGB state correctly. ((VideoNTSC) v).f1 = true; ((VideoNTSC) v).f2 = true; ((VideoNTSC) v).an3 = false; ((VideoNTSC) v).graphicsMode = rgbMode.color; } } }); rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { @Override protected void doConfig() { setScopeStart(0x0c00d); } @Override protected void doEvent(RAMEvent e) { Video v = Computer.getComputer().getVideo(); if (v instanceof VideoNTSC) { ((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_80); } } }); } @Override public void detach() { super.detach(); rgbStateListeners.stream().forEach((l) -> { Computer.getComputer().getMemory().removeListener(l); }); } @Override public void attach() { super.attach(); rgbStateListeners.stream().forEach((l) -> { Computer.getComputer().getMemory().addListener(l); }); } }