/*
* HalfNES by Andrew Hoffman
* Licensed under the GNU GPL Version 3. See LICENSE file
*/
package com.grapeshot.halfnes.video;
import java.awt.image.*;
import java.util.*;
import java.util.zip.CRC32;
/**
*
* @author Andrew
*/
public class NTSCRenderer extends Renderer {
private final static List<Integer> lines;
static {
lines = new ArrayList<>();
for (int line = 0; line < 240; ++line) {
lines.add(line);
}
}
//hm, if I downsampled these perfectly to 4Fsc i could get rid of matrix decode
//and the sine tables altogether...
private final static int[][] colorphases = { //int for alignment reasons
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},//0x00
{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1},//0x01
{0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0},//0x02
{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0},//0x03
{0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},//0x04
{0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0},//0x05
{0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0},//0x06
{1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0},//0x07
{1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1},//0x08
{1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1},//0x09
{1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1},//0x0A
{1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1},//0x0B
{1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},//0x0C
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},//0x0D
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},//0x0E
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};//0x0F
//i would like to replace these tables with logic but it's a tricky shape
//for a Karnaugh map
private final static float[][][] lumas = genlumas();
private final static int[][] coloremph = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1},//X
{0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0},//Y
{1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1},//XY
{1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1},//Z
{1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1},//XZ
{1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1},//YZ
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}};//XYZ
//private final static float sync = -0.359f;
private int frames = 0;
private final float[] i_filter = new float[12], q_filter = new float[12];
private final static int[] colortbl = genColorCorrectTbl();
public NTSCRenderer() {
frame_width = 704 * 3;
init_images();
int hue = -512;
double col_adjust = 1.2 / .707;
for (int j = 0; j < 12; ++j) {
float angle = (float) (Math.PI * ((hue + (j << 8)) / (12 * 128.0) - 33.0 / 180));
i_filter[j] = (float) (-col_adjust * Math.cos(angle));
q_filter[j] = (float) (col_adjust * Math.sin(angle));
}
}
public static int[] genColorCorrectTbl() {
int[] corr = new int[256];
//float gamma = 1.2;
float brightness = 20;
float contrast = 1;
for (int i = 0; i < 256; ++i) {
float br = (i * contrast - (128 * contrast) + 128 + brightness) / 255.f;
corr[i] = clamp((int) (255 * Math.pow(br, 1.3)));
//convert tv gamma image (~2.2-2.5) to computer gamma (~1.8)
}
return corr;
}
public static float[][][] genlumas() {
float[][] lumas = {
{-0.117f, 0.000f, 0.308f, 0.715f}, //low phase
//0x00 0x10 0x20 0x30
{0.397f, 0.681f, 1.0f, 1.0f} //high phase
};
float[][][] premultlumas = new float[lumas.length][lumas[0].length][2];
for (int i = 0; i < lumas.length; ++i) {
for (int j = 0; j < lumas[i].length; ++j) {
premultlumas[i][j][0] = lumas[i][j];
premultlumas[i][j][1] = lumas[i][j] * 0.735f;
}
}
return premultlumas;
}
public final float[] ntsc_encode(final int[] nescolors, final int offset, final int scanline, final int bgcolor) {
//part one of the process. creates a 2728 pxl array of floats representing
//ntsc version of scanline passed to it. Meant to be called 240x a frame
//todo:
//-make this encode an entire frame at a time
//-reduce # of array lookups (precalc. what is necessary)
int i, col = bgcolor & 0xf, lum = (bgcolor >> 4) & 3, emphasis = (bgcolor >> 6);
//luminance portion of nes color is bits 4-6, chrominance part is bits 1-3
//they are both used as the index into various tables
//the chroma generator chops between 2 different voltages from luma table
//at a constant rate but shifted phase.
//sync and front porch are not actually used by decoder so not implemented here
//dot 0-200:sync
//dot 200-232:black
//dot 232-352:colorburst
//dot 352-400:black
//dot 520-2568:picture
//dot 400-520 and 2568-2656: background color
//dot 2656-2720:black
//but then i'm going to chop off before dot 240 and after 2656 b/c it's not used
//so after this comment, add 240 to any num. in this for dot #
final float[] sample = new float[2728 - 240];
for (i = 400 - 240; i < 520 - 240; ++i) { //bg color at beginning
final int phase = (i + offset) % 12;
final int hue = colorphases[col][phase];
sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
}
for (i = 2568 - 240; i < 2656 - 240; ++i) { //bg color at end of line
final int phase = (i + offset) % 12;
final int hue = colorphases[col][phase];
sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
}
for (i = 520 - 240; i < 2568 - 240; ++i) { //picture
if ((i & 7) == 0) {
col = nescolors[(((i - (520 - 240)) >> 3))];
if ((col & 0xf) > 0xd) {
col = 0x0f;
}
lum = (col >> 4) & 3;
emphasis = (col >> 6);
col &= 0xf;
}
final int phase = (i + offset) % 12;
final int hue = colorphases[col][phase];
sample[i] = lumas[hue][lum][coloremph[emphasis][phase]];
}
sample[2728 - 241] = offset; //hack to not have to deal with a tuple
return sample;
}
public final static float chroma_filterfreq = 3579000.f, pixel_rate = 42950000.f;
private final static int[] cbstphase = {240 - 240, 0, 250 - 240, 0, 248 - 240, 0, 246 - 240, 0, 244 - 240, 0, 242 - 240, 0};
//starting point for color burst (depends on offset of previous line, even values not used in a progressive signal)
public final int[] ntsc_decode(final float[] ntsc, final int offset) {
final float[] chroma = new float[2656 - 240];
final float[] luma = new float[2656 - 240];
final float[] eye = new float[2656 - 240];
final float[] queue = new float[2656 - 240];
final int[] line = new int[frame_w];
//decodes one scan line of ntsc video and outputs as rgb packed in int
//uses the cheap TV method, which is filtering the chroma from the luma w/o
//combing or buffering previous lines
box_filter(ntsc, luma, chroma, 12);
for (int cbst = cbstphase[offset], j = 0; cbst < 2656 - 240 - 50; ++cbst, ++j, j %= 12) {
//matrix decode the color difference signals;
eye[cbst] = i_filter[j] * chroma[cbst + 12];
queue[cbst] = q_filter[j] * chroma[cbst + 12]; //comment out for teal and orange filter
}
lowpass_filter(eye, 0.06f);
lowpass_filter(queue, 0.05f);
for (int i = 0, x = 492 - 240; i < frame_w; ++i, ++x) {
line[i] = compose_col(
((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[0][0] * luma[x] + iqm[0][1] * eye[x] + iqm[0][2] * queue[x]))]),
((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[1][0] * luma[x] + iqm[1][1] * eye[x] + iqm[1][2] * queue[x]))]),
((luma[x] <= 0) ? 0 : colortbl[clamp((int) (iqm[2][0] * luma[x] + iqm[2][1] * eye[x] + iqm[2][2] * queue[x]))]));
}
return line;
}
public static void box_filter(final float[] in, final float[] lpout, final float[] hpout, final int order) {
float accum = 0;
for (int i = 12; i < 2656 - 240; ++i) {
accum += in[i] - in[i - order];
lpout[i] = accum / order;
hpout[i] = in[i] - lpout[i];
}
}
public static void lowpass_filter(final float[] arr, final float order) {
float b = 0;
for (int i = 0; i < 2656 - 240; ++i) {
arr[i] -= b;
b += arr[i] * order;
arr[i] = b;
}
}
private static int compose_col(int r, int g, int b) {
return (r << 16) | (g << 8) | (b) | 0xff000000;
}
private final static int[][] iqm = {{255, -244, 158}, {255, 69, -165}, {255, 282, 434}};
public static int clamp(final int a) {
return (a != (a & 0xff)) ? ((a < 0) ? 0 : 255) : a;
}
public final static int frame_w = 704 * 3;
int[] frame = new int[frame_w * 240];
// Kernel kernel = new Kernel(3, 3,
// new float[]{-.0625f, .125f, -.0625f,
// .125f, .75f, .125f,
// -.0625f, .125f, -.0625f});
// BufferedImageOp op = new ConvolveOp(kernel);
@Override
public BufferedImage render(final int[] nespixels, final int[] bgcolors, final boolean dotcrawl) {
// multithreaded filter
lines.parallelStream().forEach(line -> cacheRender(nespixels, line, bgcolors, dotcrawl));
BufferedImage i = getBufferedImage(frame);
++frames;
//i = op.filter(i, null); //sharpen
return i;
}
//ConcurrentHashMap cache = new ConcurrentHashMap<Long, int[]>(600);
Map<Long, int[]> cache = Collections.synchronizedMap(new WeakHashMap<Long, int[]>(600));
//weak hash map allows things in it to be garbage collected
private void cacheRender(final int[] nespixels, final int line, final int[] bgcolors, final boolean dotcrawl) {
//first of all, increment scanline numbers and get the offset for this line.
int offset = ((frames & 1) == 0 && dotcrawl) ? 0 : 6;
offset = (4 * line + offset) % 12; //3 line dot crawl
final int[] inpixels = new int[256];
System.arraycopy(nespixels, line << 8, inpixels, 0, 256);
final long crc = crc32(inpixels, offset, bgcolors[line]);
// //you'd think crc32 would have too many collisions but i haven't seen a one
int[] outpixels;
outpixels = (int[]) cache.get(crc);
if (outpixels == null) { //not in cache
//could do with hints from the PPU here: if the entire screen is
//scrolling horizontally, the cache will be useless.
outpixels = ntsc_decode(ntsc_encode(inpixels, offset, line, bgcolors[line]), offset);
cache.put(crc, outpixels);
}
System.arraycopy(outpixels, 0, frame, line * frame_w, frame_w);
}
public static long crc32(int[] array, int offset, int bgcolor) {
CRC32 c = new CRC32();
for (int i : array) {
c.update(i);
}
//it's not immediately obvious, but this ONLY sets the CRC based on the
//blue channel of the output. Still works well though.
//You get some interesting compressiony effects if you only take the CRC
//of every 20th pixel to see if your line is the same.
//especially in sidescrolling things.
c.update(offset);
c.update(bgcolor);
return c.getValue();
}
}