mirror of https://github.com/buserror/mii_emu.git
1266 lines
36 KiB
C
1266 lines
36 KiB
C
/*
|
|
* mii_video.c
|
|
*
|
|
* Copyright (C) 2023 Michel Pollet <buserror@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
|
|
#include "mii.h"
|
|
#include "mii_bank.h"
|
|
#include "mii_rom_iiee_video.h"
|
|
#include "mii_sw.h"
|
|
#include "minipt.h"
|
|
|
|
|
|
#if defined(__AVX2__)
|
|
#include <immintrin.h>
|
|
typedef uint32_t u32_v __attribute__((vector_size(32)));
|
|
#define VEC_ALIGN 31
|
|
#define VEC_ECOUNT 8
|
|
#else
|
|
#include <emmintrin.h>
|
|
typedef uint32_t u32_v __attribute__((vector_size(16)));
|
|
#define VEC_ALIGN 15
|
|
#define VEC_ECOUNT 4
|
|
#endif
|
|
|
|
enum {
|
|
// https://rich12345.tripod.com/aiivideo/vbl.html
|
|
MII_VBL_DOWN_CYCLES = 12480,
|
|
MII_VBL_UP_CYCLES = 4550,
|
|
MII_VIDEO_H_CYCLES = 40,
|
|
MII_VIDEO_HB_CYCLES = 25,
|
|
};
|
|
|
|
#define MII_VIDEO_MIXED_LINE (192 - (4 * 8))
|
|
// frequency of the blinking text, in frames. When that bit changes, we flash
|
|
#define MII_VIDEO_FLASH_FRAME_MASK 0x10
|
|
|
|
// this is the bank video memory is read from. This differs from the AUX
|
|
// bank as it doesn't change when the Ramworks card bank is changed
|
|
#define MII_VIDEO_BANK MII_BANK_AUX_BASE
|
|
|
|
/*
|
|
* Colors were lifted from
|
|
* https://comp.sys.apple2.narkive.com/lTSrj2ZI/apple-ii-colour-rgb
|
|
* and
|
|
* https://www.mrob.com/pub/xapple2/colors.html
|
|
*/
|
|
#define HI_LUMA(r,g,b) \
|
|
((uint8_t)(0.2126 * (r) + 0.7152 * (g) + 0.0722 * (b)))
|
|
|
|
/*
|
|
* You migth have to tweak this for performance reasons. At least on nVidia
|
|
* cards, GL_BGRA is faster than GL_RGBA.
|
|
*/
|
|
#define HI_RGB(_r,_g,_b) (0xff000000 | ((_b) << 16) | ((_g) << 8) | (_r))
|
|
#define HI_C_RGB(_r,_g,_b) (mii_color_t) \
|
|
(0xff000000 | ((_b) << 16) | ((_g) << 8) | (_r))
|
|
|
|
#define HI_GET_RGB(_rgb, _r, _g, _b) { \
|
|
(_r) = (_rgb) & 0xff; \
|
|
(_g) = ((_rgb) >> 8) & 0xff; \
|
|
(_b) = ((_rgb) >> 16) & 0xff; \
|
|
}
|
|
|
|
/* this 'dims' the colors for every second line of pixels
|
|
* This is a very very cheap filter but it works really well!
|
|
*/
|
|
#define C_SCANLINE_MASK 0xffc0c0c0
|
|
|
|
// these are more or less arbitrary orders really
|
|
enum mii_video_color_mode_e {
|
|
CI_BLACK = 0,
|
|
CI_PURPLE, CI_GREEN, CI_BLUE, CI_ORANGE, CI_WHITE, CI_MAGENTA,
|
|
CI_DARKBLUE,CI_DARKGREEN,CI_GRAY1,CI_GRAY2,CI_LIGHTBLUE,
|
|
CI_BROWN,CI_PINK,CI_YELLOW,CI_AQUA,
|
|
};
|
|
typedef struct mii_palette_t {
|
|
const char * name;
|
|
uint32_t mono_color;
|
|
mii_color_t color[16];
|
|
} mii_palette_t;
|
|
|
|
static const mii_palette_t palettes[] = {
|
|
[0] = {
|
|
.name = "Color NTSC",
|
|
.color = {
|
|
[CI_BLACK] = HI_C_RGB(0x00,0x00,0x00),
|
|
[CI_PURPLE] = HI_C_RGB(0xff,0x44,0xfd),
|
|
[CI_GREEN] = HI_C_RGB(0x14,0xf5,0x3c),
|
|
[CI_BLUE] = HI_C_RGB(0x14,0xcf,0xfd),
|
|
[CI_ORANGE] = HI_C_RGB(0xff,0x6a,0x3c),
|
|
[CI_WHITE] = HI_C_RGB(0xff,0xff,0xff),
|
|
[CI_MAGENTA] = HI_C_RGB(0xe3,0x1e,0x60),
|
|
[CI_DARKBLUE] = HI_C_RGB(0x60,0x4e,0xbd),
|
|
[CI_DARKGREEN] = HI_C_RGB(0x00,0xa3,0x60),
|
|
[CI_GRAY1] = HI_C_RGB(0x9c,0x9c,0x9c),
|
|
[CI_GRAY2] = HI_C_RGB(0x9c,0x9c,0x9c),
|
|
[CI_LIGHTBLUE] = HI_C_RGB(0xd0,0xc3,0xff),
|
|
[CI_BROWN] = HI_C_RGB(0x60,0x72,0x03),
|
|
[CI_PINK] = HI_C_RGB(0xff,0xa0,0xd0),
|
|
[CI_YELLOW] = HI_C_RGB(0xd0,0xdd,0x8d),
|
|
[CI_AQUA] = HI_C_RGB(0x72,0xff,0xd0),
|
|
},
|
|
},
|
|
[1] = {
|
|
.name = "NTSC 2",
|
|
.color = {
|
|
[CI_BLACK] = HI_C_RGB(0x00,0x00,0x00),
|
|
[CI_MAGENTA] = HI_C_RGB(0x9F,0x1B,0x48),
|
|
[CI_DARKBLUE] = HI_C_RGB(0x48,0x32,0xEB),
|
|
[CI_PURPLE] = HI_C_RGB(0xD6,0x43,0xFF),
|
|
[CI_DARKGREEN] = HI_C_RGB(0x19,0x75,0x44),
|
|
[CI_GRAY1] = HI_C_RGB(0x81,0x81,0x81),
|
|
[CI_BLUE] = HI_C_RGB(0x36,0x92,0xFF),
|
|
[CI_LIGHTBLUE] = HI_C_RGB(0xB8,0x9E,0xFF),
|
|
[CI_BROWN] = HI_C_RGB(0x49,0x65,0x00),
|
|
[CI_ORANGE] = HI_C_RGB(0xD8,0x73,0x00),
|
|
[CI_GRAY2] = HI_C_RGB(0x81,0x81,0x81),
|
|
[CI_PINK] = HI_C_RGB(0xFB,0x8F,0xBC),
|
|
[CI_GREEN] = HI_C_RGB(0x3C,0xCC,0x00),
|
|
[CI_YELLOW] = HI_C_RGB(0xBC,0xD6,0x00),
|
|
[CI_AQUA] = HI_C_RGB(0x6C,0xE6,0xB8),
|
|
[CI_WHITE] = HI_C_RGB(0xF1,0xF1,0xF1),
|
|
},
|
|
},
|
|
[2] = {
|
|
.name = "Color Mega2",
|
|
.color = {
|
|
[CI_BLACK ] = HI_C_RGB(0x00,0x00,0x00),
|
|
[CI_MAGENTA ] = HI_C_RGB(0xDB,0x1F,0x42),
|
|
[CI_DARKBLUE] = HI_C_RGB(0x0C,0x11,0xA4),
|
|
[CI_PURPLE ] = HI_C_RGB(0xDC,0x43,0xE1),
|
|
[CI_DARKGREEN]= HI_C_RGB(0x1C,0x82,0x31),
|
|
[CI_GRAY1 ] = HI_C_RGB(0x63,0x63,0x63),
|
|
[CI_BLUE ] = HI_C_RGB(0x39,0x3D,0xFF),
|
|
[CI_LIGHTBLUE]= HI_C_RGB(0x7A,0xB3,0xFF),
|
|
[CI_BROWN ] = HI_C_RGB(0x91,0x64,0x00),
|
|
[CI_ORANGE ] = HI_C_RGB(0xFA,0x77,0x00),
|
|
[CI_GRAY2 ] = HI_C_RGB(0xB3,0xB3,0xB3),
|
|
[CI_PINK ] = HI_C_RGB(0xFB,0xA5,0x93),
|
|
[CI_GREEN ] = HI_C_RGB(0x40,0xDE,0x00),
|
|
[CI_YELLOW ] = HI_C_RGB(0xFE,0xFE,0x00),
|
|
[CI_AQUA ] = HI_C_RGB(0x67,0xFC,0xA3),
|
|
[CI_WHITE ] = HI_C_RGB(0xFF,0xFF,0xFF),
|
|
},
|
|
},
|
|
[3] = {
|
|
.name = "Green",
|
|
.mono_color = HI_C_RGB(0x14, 0xf5, 0x3c)
|
|
},
|
|
[4] = {
|
|
.name = "Amber",
|
|
.mono_color = HI_C_RGB(0xfd, 0xcf, 0x14),
|
|
},
|
|
};
|
|
|
|
/*
|
|
* All video mode colors. Note that this is not REALLY a color palette in this
|
|
* state, instead, it is a color index in the palette that has been chosen by
|
|
* the user... The set_video_mode function will synthetize the actual colors,
|
|
* as well as the 'dim' variant use for artifacts.
|
|
*/
|
|
static const mii_video_clut_t mii_base_clut = {
|
|
.lores = {{
|
|
[0x0] = CI_BLACK, [0x1] = CI_MAGENTA, [0x2] = CI_DARKBLUE,[0x3] = CI_PURPLE,
|
|
[0x4] = CI_DARKGREEN,[0x5] = CI_GRAY1, [0x6] = CI_BLUE, [0x7] = CI_LIGHTBLUE,
|
|
[0x8] = CI_BROWN, [0x9] = CI_ORANGE, [0xa] = CI_GRAY2, [0xb] = CI_PINK,
|
|
[0xc] = CI_GREEN, [0xd] = CI_YELLOW, [0xe] = CI_AQUA, [0xf] = CI_WHITE,
|
|
},{
|
|
[0x0] = CI_BLACK, [0x1] = CI_DARKBLUE,[0x2] = CI_DARKGREEN,[0x3] = CI_BLUE,
|
|
[0x4] = CI_BROWN, [0x5] = CI_GRAY2, [0x6] = CI_GREEN, [0x7] = CI_AQUA,
|
|
[0x8] = CI_MAGENTA, [0x9] = CI_PURPLE, [0xa] = CI_GRAY1, [0xb] = CI_LIGHTBLUE,
|
|
[0xc] = CI_ORANGE, [0xd] = CI_PINK, [0xe] = CI_YELLOW, [0xf] = CI_WHITE,
|
|
} },
|
|
.dhires = {
|
|
[0x0] = CI_BLACK, [0x1] = CI_MAGENTA, [0x2] = CI_BROWN, [0x3] = CI_ORANGE,
|
|
[0x4] = CI_DARKGREEN,[0x5] = CI_GRAY1, [0x6] = CI_GREEN, [0x7] = CI_YELLOW,
|
|
[0x8] = CI_DARKBLUE,[0x9] = CI_PURPLE, [0xa] = CI_GRAY2, [0xb] = CI_PINK,
|
|
[0xc] = CI_BLUE, [0xd] = CI_LIGHTBLUE,[0xe] = CI_AQUA, [0xf] = CI_WHITE,
|
|
},
|
|
.hires = {
|
|
CI_BLACK, CI_PURPLE, CI_GREEN, CI_GREEN, CI_PURPLE,
|
|
CI_BLUE, CI_ORANGE, CI_ORANGE, CI_BLUE, CI_WHITE,
|
|
},
|
|
#if 0
|
|
.hires2 = {
|
|
CI_BLACK, CI_GREEN, CI_PURPLE, CI_WHITE,
|
|
CI_BLACK, CI_ORANGE, CI_BLUE, CI_WHITE,
|
|
},
|
|
#endif
|
|
.mono = { CI_BLACK, CI_WHITE },
|
|
};
|
|
|
|
|
|
// Used for DHRES decoding
|
|
static inline uint8_t reverse4(uint8_t b) {
|
|
b = (b & 0b0001) << 3 | (b & 0b0010) << 1 |
|
|
(b & 0b0100) >> 1 | (b & 0b1000) >> 3;
|
|
return b;
|
|
}
|
|
static inline uint8_t reverse8(uint8_t b) {
|
|
b = reverse4(b) << 4 | reverse4(b >> 4);
|
|
return b;
|
|
}
|
|
static inline uint16_t
|
|
_mii_line_to_video_addr(
|
|
uint16_t addr,
|
|
uint8_t line)
|
|
{
|
|
addr += ((line & 0x07) << 10) |
|
|
(((line >> 3) & 7) << 7) |
|
|
((line >> 6) << 5) | ((line >> 6) << 3);
|
|
return addr;
|
|
}
|
|
|
|
|
|
static inline int
|
|
_mii_addr_to_line_text_lores(
|
|
uint16_t a) // ZERO based, not 0x400 based
|
|
{
|
|
int hole = (a & 0x7f) > 0x77;
|
|
if (hole)
|
|
return -1;
|
|
int group = ((a >> 7) & 0x7);
|
|
int gline = (a & 0x7f) / 40;
|
|
int line = (group + (gline * 8)) * 8;
|
|
return line;
|
|
}
|
|
/*
|
|
* check if addr is in the current text page, including page2 switch --
|
|
* this also works for lowres (and mixed mode)
|
|
*/
|
|
static void
|
|
_mii_line_check_text_lores(
|
|
struct mii_video_t *video,
|
|
uint32_t sw,
|
|
uint16_t addr)
|
|
{
|
|
bool store80 = SWW_GETSTATE(sw, SW80STORE);
|
|
bool page2 = store80 ? 0 : SWW_GETSTATE(sw, SWPAGE2);
|
|
uint16_t a = 0x400 + (0x400 * page2);
|
|
|
|
// if addr is in the text/lores page, convert the addr into a line number
|
|
// and mark that line as dirty. In this particular, case, we need to mark
|
|
// 7 video lines as dirty obviously
|
|
if (addr >= a && addr < a + 0x400) {
|
|
a = addr - a;
|
|
int line = _mii_addr_to_line_text_lores(a);
|
|
if (line < 0)
|
|
return;
|
|
for (int i = line; i < line + 8; i++) {
|
|
video->lines_dirty[i / 64] |= 1ULL << (i & 63);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* check if addr is in the current graphic page, including page2 switch
|
|
* We don't have to care about dhires etc, as either bank would be same addresses
|
|
*
|
|
* This means in the unlikely case where code writes to the aux memory at the
|
|
* same address as the main memory for that graphic mode, we will dirty lines
|
|
* that don't need to be dirty, but it is a small price to pay for this
|
|
* unusual case. Also, this would just have a slight impact on performance anyway.
|
|
*
|
|
* Also handle mixed mode, when mixed is on, check the bottom lines of text
|
|
* for any changes
|
|
*/
|
|
static void
|
|
_mii_line_check_hires_dires(
|
|
struct mii_video_t *video,
|
|
uint32_t sw,
|
|
uint16_t addr)
|
|
{
|
|
bool store80 = SWW_GETSTATE(sw, SW80STORE);
|
|
bool page2 = store80 ? 0 : SWW_GETSTATE(sw, SWPAGE2);
|
|
bool mixed = SWW_GETSTATE(sw, SWMIXED);
|
|
uint16_t a = (0x2000 + (0x2000 * page2));
|
|
|
|
if (addr >= a && addr < a + 0x2000) {
|
|
a = addr - a;
|
|
int hole = (a & 0x78) == 0x78;// (a & 0x7f) > 0x77;
|
|
if (hole)
|
|
return;
|
|
int g = ((a >> 7) & 0x7);
|
|
int g2 = (a >> 10) & 0x7;
|
|
int gline = (a & 0x7f) / 40;
|
|
int line = (gline * 64) + (g * 8) + g2;
|
|
|
|
if (!mixed || line < MII_VIDEO_MIXED_LINE)
|
|
video->lines_dirty[line / 64] |= 1ULL << (line & 63);
|
|
}
|
|
if (mixed) {
|
|
a = 0x400 + (0x400 * page2);
|
|
if (addr >= a && addr < a + 0x400) {
|
|
a = addr - a;
|
|
int line = _mii_addr_to_line_text_lores(a);
|
|
if (line < MII_VIDEO_MIXED_LINE)
|
|
return;
|
|
for (int i = line; i < line + 8; i++) {
|
|
video->lines_dirty[i / 64] |= 1ULL << (i & 63);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
_mii_line_render_dhires_mono(
|
|
mii_t *mii)
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
mii_bank_t * aux = &mii->bank[MII_VIDEO_BANK];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
// uint8_t mode = mii->video.color_mode;
|
|
uint16_t a = (0x2000 + (0x2000 * page2));
|
|
mii->video.base_addr = a;
|
|
a = _mii_line_to_video_addr(a, mii->video.line);
|
|
mii->video.line_addr = a;
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
|
|
const uint32_t clut[2] = {
|
|
mii->video.clut.mono[0],
|
|
mii->video.clut.mono[1] };
|
|
for (int x = 0; x < 40; x++) {
|
|
uint32_t ext = (mii_bank_peek(aux, a + x) & 0x7f) |
|
|
((mii_bank_peek(main, a + x) & 0x7f) << 7);
|
|
for (int bi = 0; bi < 14; bi++) {
|
|
uint8_t pixel = (ext >> bi) & 1;
|
|
uint32_t col = clut[pixel];
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* get exactly 1 bits from position bit from the buffer */
|
|
static inline uint8_t
|
|
_mii_get_1bits(
|
|
uint8_t * buffer,
|
|
int bit)
|
|
{
|
|
int in_byte = (bit) / 8;
|
|
int in_bit = 7 - ((bit) % 8);
|
|
uint8_t b = (buffer[in_byte] >> in_bit) & 1;
|
|
return b;
|
|
}
|
|
|
|
|
|
static void
|
|
_mii_line_render_dhires_color(
|
|
mii_t *mii)
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
mii_bank_t * aux = &mii->bank[MII_VIDEO_BANK];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
|
|
uint16_t a = (0x2000 + (0x2000 * page2));
|
|
mii->video.base_addr = a;
|
|
a = _mii_line_to_video_addr(a, mii->video.line);
|
|
mii->video.line_addr = a;
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
|
|
uint8_t bits[71] = { 0 };
|
|
|
|
for (int x = 0; x < 80; x++) {
|
|
uint8_t b = mii_bank_peek(x & 1 ? main : aux, a + (x / 2));
|
|
// this reverse the 7 bits of each bytes into the bit buffer
|
|
for (int i = 0; i < 7; i++) {
|
|
int out_index = 2 + (x * 7) + i;
|
|
int out_byte = out_index / 8;
|
|
int out_bit = 7 - (out_index % 8);
|
|
int bit = (b >> i) & 1;
|
|
bits[out_byte] |= bit << out_bit;
|
|
}
|
|
}
|
|
// destination pixels are offset by 2 pixels, so the image is centered
|
|
// with an 'artifact' on the left and right side, seems to match pictures I've
|
|
// seen on TFT screens.
|
|
for (int i = 0, d = 2; i < 560; i++, d++) {
|
|
const uint8_t pixel =
|
|
(_mii_get_1bits(bits, i + 3) << (3 - ((d + 3) % 4))) +
|
|
(_mii_get_1bits(bits, i + 2) << (3 - ((d + 2) % 4))) +
|
|
(_mii_get_1bits(bits, i + 1) << (3 - ((d + 1) % 4))) +
|
|
(_mii_get_1bits(bits, i) << (3 - (d % 4)));
|
|
uint32_t col = mii->video.clut.dhires[pixel];
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static int _trace = 0;
|
|
#define TRACE
|
|
#ifdef TRACE
|
|
#define T(_w) if (_trace) { _w; }
|
|
#else
|
|
#define T(_w)
|
|
#endif
|
|
|
|
static void
|
|
_mii_line_render_hires(
|
|
mii_t *mii)
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
uint8_t mode = mii->video.color_mode;
|
|
uint16_t a = (0x2000 + (0x2000 * page2));
|
|
mii->video.base_addr = a;
|
|
a = _mii_line_to_video_addr(a, mii->video.line);
|
|
mii->video.line_addr = a;
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
uint8_t bits[280] = { 0 };
|
|
uint8_t *src = main->mem + a;
|
|
uint bits_dx = 0;
|
|
uint8_t last_run = 0;
|
|
for (int x = 0; x < 40; x += 2) {
|
|
/* prepare fourteen pixels */
|
|
uint8_t b0 = src[x];
|
|
uint8_t b1 = src[x + 1];
|
|
uint8_t t[2] = { (b0 >> 7) << 2, (b1 >> 7) << 2 }; // prepare high bits
|
|
// just want the pixels as a nice clean 14 bits bitfield here
|
|
uint16_t run = ((reverse8(b0) >> 1) << 7) | (reverse8(b1) >> 1);
|
|
// uint16_t run = ((b1 & 0x7f) << 9) | ((b0 & 0x7f) << 2) | last_run;
|
|
uint8_t px;
|
|
for (int dx = 0 ; dx < 14; dx++) {
|
|
// take 2 bits, add the corresponding 'high bit' to it
|
|
px = run & 3;
|
|
px |= t[dx > 7];
|
|
// now we have 2 pixels, we can set them in the bit buffer
|
|
bits[bits_dx++] = px;
|
|
}
|
|
last_run = run >> 14;
|
|
}
|
|
// now we have a 'nice' table of actual colors, without artifacts in bits
|
|
// we can plot this
|
|
for (int i = 0; i < 280; i++) {
|
|
uint8_t pixel = bits[i];
|
|
uint32_t col = mii->video.clut.hires2[pixel];
|
|
*screen++ = col;
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
#else
|
|
static void
|
|
_mii_line_render_hires(
|
|
mii_t *mii)
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
// uint8_t mode = mii->video.color_mode;
|
|
uint16_t a = (0x2000 + (0x2000 * page2));
|
|
mii->video.base_addr = a;
|
|
a = _mii_line_to_video_addr(a, mii->video.line);
|
|
mii->video.line_addr = a;
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
uint8_t *src = main->mem;
|
|
|
|
uint8_t b0 = 0;
|
|
uint8_t b1 = src[a + 0];
|
|
uint32_t lastcol = 0;
|
|
for (int x = 0; x < 40; x++) {
|
|
uint8_t b2 = src[a + x + 1];
|
|
// last 2 pixels, current 7 pixels, next 2 pixels
|
|
uint16_t run = ((b0 & 0x60) >> ( 5 )) |
|
|
((b1 & 0x7f) << ( 2 )) |
|
|
((b2 & 0x03) << ( 9 ));
|
|
int odd = (x & 1) << 1;
|
|
int offset = (b1 & 0x80) >> 5;
|
|
if (!mii->video.monochrome) {
|
|
for (int i = 0; i < 7; i++) {
|
|
uint8_t left = (run >> (1 + i)) & 1;
|
|
uint8_t pixel = (run >> (2 + i)) & 1;
|
|
uint8_t right = (run >> (3 + i)) & 1;
|
|
|
|
int idx = 0; // black
|
|
if (pixel) {
|
|
if (left || right) {
|
|
idx = 9; // white
|
|
} else {
|
|
idx = offset + odd + (i & 1) + 1;
|
|
}
|
|
} else {
|
|
if (left && right) {
|
|
idx = offset + odd + 1 - (i & 1) + 1;
|
|
}
|
|
}
|
|
uint32_t col = mii->video.clut.hires[idx];
|
|
if (col != lastcol) {
|
|
uint32_t nc = mii->video.clut_low.hires[idx];
|
|
*screen++ = nc; //col & C_SCANLINE_MASK;
|
|
*screen++ = nc; //col & C_SCANLINE_MASK;
|
|
lastcol = col;
|
|
} else {
|
|
*screen++ = col;
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
} else {
|
|
for (int i = 0; i < 7; i++) {
|
|
uint8_t pixel = (run >> (2 + i)) & 1;
|
|
uint32_t col = mii->video.clut.mono[pixel];
|
|
if (col != lastcol) {
|
|
*screen++ = col & C_SCANLINE_MASK;
|
|
lastcol = col;
|
|
} else
|
|
*screen++ = col;
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
b0 = b1;
|
|
b1 = b2;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
_mii_line_render_text(
|
|
mii_t *mii)
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
mii_bank_t * aux = &mii->bank[MII_VIDEO_BANK];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
// uint8_t mode = mii->video.color_mode;
|
|
|
|
uint16_t a = (0x400 + (0x400 * page2));
|
|
mii->video.base_addr = a;
|
|
int i = mii->video.line >> 3;
|
|
a += ((i & 0x07) << 7) | ((i >> 3) << 5) | ((i >> 3) << 3);
|
|
mii->video.line_addr = a;
|
|
|
|
bool col80 = SW_GETSTATE(mii, SW80COL);
|
|
bool altset = SW_GETSTATE(mii, SWALTCHARSET);
|
|
int flash = mii->video.frame_count & MII_VIDEO_FLASH_FRAME_MASK ?
|
|
-0x40 : 0x40;
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
|
|
for (int x = 0; x < 40 + (40 * col80); x++) {
|
|
uint8_t c = 0;
|
|
if (col80)
|
|
c = mii_bank_peek(x & 1 ? main : aux, a + (x >> 1));
|
|
else
|
|
c = mii_bank_peek(main, a + x);
|
|
if (!altset) {
|
|
if (c >= 0x40 && c <= 0x7f)
|
|
c = (int)c + flash;
|
|
}
|
|
const uint8_t * rom = iie_enhanced_video + (c << 3);
|
|
uint8_t bits = rom[mii->video.line & 0x07];
|
|
for (int pi = 0; pi < 7; pi++) {
|
|
uint8_t pixel = (bits >> pi) & 1;
|
|
uint32_t col = mii->video.clut.mono[!pixel];
|
|
*screen++ = col;
|
|
if (!col80)
|
|
*screen++ = col;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
_mii_line_render_lores(
|
|
mii_t *mii )
|
|
{
|
|
mii_bank_t * main = &mii->bank[MII_BANK_MAIN];
|
|
mii_bank_t * aux = &mii->bank[MII_VIDEO_BANK];
|
|
bool page2 = SW_GETSTATE(mii, SW80STORE) ? 0 : SW_GETSTATE(mii, SWPAGE2);
|
|
// uint8_t mode = mii->video.color_mode;
|
|
|
|
uint16_t a = (0x400 + (0x400 * page2));
|
|
mii->video.base_addr = a;
|
|
int i = mii->video.line >> 3;
|
|
a += ((i & 0x07) << 7) | ((i >> 3) << 5) | ((i >> 3) << 3);
|
|
mii->video.line_addr = a;
|
|
|
|
bool col80 = SW_GETSTATE(mii, SW80COL);
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
mii_video_clut_t * clut = &mii->video.clut;
|
|
mii_video_clut_t * clut_low = &mii->video.clut_low;
|
|
uint32_t lastcolor = 0;
|
|
for (int x = 0; x < 40 + (40 * col80); x++) {
|
|
uint16_t c = 0;
|
|
if (col80)
|
|
c = mii_bank_peek(x & 1 ? main : aux, a + (x >> 1));
|
|
else
|
|
c = mii_bank_peek(main, a + x);
|
|
|
|
int lo_line = mii->video.line / 4;
|
|
c = (c >> ((lo_line & 1) * 4)) & 0xf;
|
|
// c |= (c << 4);
|
|
uint32_t color = clut->lores[(x & col80) ^ col80][c & 0x0f];
|
|
uint32_t dim = clut_low->lores[(x & col80) ^ col80][c & 0x0f];
|
|
if (!mii->video.monochrome) {
|
|
for (int pi = 0; pi < 7; pi++) {
|
|
uint32_t pixel = color;
|
|
if (pixel != lastcolor) {
|
|
pixel = dim;
|
|
lastcolor = color;
|
|
}
|
|
*screen++ = pixel;
|
|
if (!col80)
|
|
*screen++ = pixel;
|
|
}
|
|
} else {
|
|
c = reverse4(c);
|
|
c |= c << 4;
|
|
c |= c << 8;
|
|
if (col80) {
|
|
for (int pi = 0; pi < 7; pi++) {
|
|
uint8_t b = (c >> pi) & 1;
|
|
uint32_t pixel = b ? color : dim;
|
|
*screen++ = pixel;
|
|
}
|
|
} else {
|
|
if (x & 1) c >>= 2;
|
|
for (int pi = 0; pi < 14; pi++) {
|
|
uint8_t b = (c >> pi) & 1;
|
|
uint32_t pixel = b ? color : dim;
|
|
*screen++ = pixel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This is called when writes are made from outside the 6502 emulation, for
|
|
* example the DMA froms smartport. Otherwise you could BLOAD an image in video
|
|
* ram and there would be now way of knowing if the addresses *were* in the
|
|
* video ram. So this call is used by anything doing DMA (curretnly just smartport)
|
|
*/
|
|
void
|
|
mii_video_OOB_write_check(
|
|
mii_t *mii,
|
|
uint16_t addr,
|
|
uint16_t size)
|
|
{
|
|
for (int i = 0; i < size; i += 40)
|
|
mii->video.line_cb.check(&mii->video, mii->sw_state, addr + i);
|
|
}
|
|
|
|
/*
|
|
* This return the correct line drawing function callback for the mode
|
|
* and softswitches
|
|
*/
|
|
static mii_video_cb_t
|
|
_mii_video_get_line_render_cb(
|
|
mii_t *mii,
|
|
uint32_t sw_state)
|
|
{
|
|
mii_video_cb_t res = { 0 };
|
|
|
|
bool text = SWW_GETSTATE(sw_state, SWTEXT);
|
|
bool col80 = SWW_GETSTATE(sw_state, SW80COL);
|
|
bool hires = SWW_GETSTATE(sw_state, SWHIRES);
|
|
bool dhires = SWW_GETSTATE(sw_state, SWDHIRES);
|
|
|
|
if (hires && !text && col80 && dhires) {
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
uint8_t reg = mii_bank_peek(sw, SWAN3_REGISTER);
|
|
if (reg != 0 && !mii->video.monochrome)
|
|
res.render = _mii_line_render_dhires_color;
|
|
else
|
|
res.render = _mii_line_render_dhires_mono;
|
|
res.check = _mii_line_check_hires_dires;
|
|
} else if (hires && !text) {
|
|
res.render = _mii_line_render_hires;
|
|
res.check = _mii_line_check_hires_dires;
|
|
} else if (text) {
|
|
res.render = _mii_line_render_text;
|
|
res.check = _mii_line_check_text_lores;
|
|
} else {
|
|
res.render = _mii_line_render_lores;
|
|
res.check = _mii_line_check_text_lores;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static void
|
|
_mii_video_mark_dirty(
|
|
mii_video_t *video)
|
|
{
|
|
video->frame_dirty = 1;
|
|
video->lines_dirty[0] =
|
|
video->lines_dirty[1] =
|
|
video->lines_dirty[2] = -1LL;
|
|
}
|
|
|
|
/*
|
|
* This is called when the video mode changes, and we need to update the
|
|
* line drawing callback
|
|
*/
|
|
static void
|
|
_mii_video_mode_changed(
|
|
mii_t *mii)
|
|
{
|
|
uint32_t sw_state = mii->sw_state;
|
|
|
|
mii_video_cb_t res = _mii_video_get_line_render_cb(mii, sw_state);
|
|
if (res.render != mii->video.line_cb.render) {
|
|
mii->video.line_cb = res;
|
|
_mii_video_mark_dirty(&mii->video);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This is the state of the video output
|
|
* All timings lifted from https://rich12345.tripod.com/aiivideo/vbl.html
|
|
*
|
|
* This is a 'protothread' basically cooperative scheduling using an
|
|
* old compiler trick. It's not a real thread, but it's a way to
|
|
* write code that looks like a thread, and is easy to read.
|
|
* The 'pt_start' macro starts the thread, and pt_yield() yields
|
|
* the thread to the main loop.
|
|
* The pt_end() macro ends the thread.
|
|
* Remeber you cannot have locals in the thread, they must be
|
|
* static or global.
|
|
* *everything* before the pt_start call is ran every time, so you can use
|
|
* that to reload some sort of state, as here, were we reload all the
|
|
* video mode softswitches.
|
|
*
|
|
* This function is also a 'cycle timer' it returns the number of 6502
|
|
* cycles to wait until being called again, so it mostly returns the
|
|
* number of cycles until the next horizontal blanking between each lines,
|
|
* but also the number of cycles until the next vertical blanking once
|
|
* the last line is drawn.
|
|
*/
|
|
static uint64_t
|
|
mii_video_timer_cb(
|
|
mii_t *mii,
|
|
void *param)
|
|
{
|
|
uint64_t res = MII_VIDEO_H_CYCLES * mii->speed;
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
uint32_t sw_state = mii->sw_state;
|
|
|
|
pt_start(mii->video.state);
|
|
/*
|
|
We cheat and draw a whole line at a time, then 'wait' until
|
|
horizontal blanking, then wait until vertical blanking.
|
|
*/
|
|
do {
|
|
// 'clear' VBL flag. Flag is 0 during retrace
|
|
mii_bank_poke(sw, SWVBL, 0x80);
|
|
|
|
mii_video_line_drawing_cb line_drawing = mii->video.line_cb.render;
|
|
/* If we are in mixed mode past line 160, check if we need to
|
|
* switch from the 'main mode' callback to the text callback */
|
|
if (mii->video.line >= MII_VIDEO_MIXED_LINE) {
|
|
bool mixed = SWW_GETSTATE(sw_state, SWMIXED);
|
|
if (mixed) {
|
|
uint32_t sw = sw_state;
|
|
SWW_SETSTATE(sw, SWTEXT, 1);
|
|
if (sw != sw_state)
|
|
line_drawing = _mii_video_get_line_render_cb(mii, sw).render;
|
|
}
|
|
}
|
|
if (mii->video.lines_dirty[mii->video.line / 64] &
|
|
(1ULL << (mii->video.line & 63))) {
|
|
line_drawing(mii);
|
|
|
|
uint32_t * screen = mii->video.pixels +
|
|
(mii->video.line * MII_VRAM_WIDTH * 2);
|
|
uint32_t * l2 = screen + MII_VRAM_WIDTH;
|
|
|
|
#if defined(__AVX2__)
|
|
const __m256i mask = _mm256_set1_epi32(C_SCANLINE_MASK);
|
|
// Process scanline using AVX GCC intrinsic
|
|
for (int i = 0; i < MII_VIDEO_WIDTH; i += 8) {
|
|
__m256i src = _mm256_loadu_si256((__m256i *)(screen + i));
|
|
__m256i result = _mm256_and_si256(src, mask);
|
|
_mm256_storeu_si256((__m256i *)(l2 + i), result);
|
|
}
|
|
#elif defined(__SSE2__)
|
|
const __m128i mask = _mm_set1_epi32(C_SCANLINE_MASK);
|
|
// Process scanline using SSE GCC intrinsic
|
|
for (int i = 0; i < MII_VIDEO_WIDTH; i += 4) {
|
|
__m128i src = _mm_loadu_si128((__m128i *)(screen + i));
|
|
__m128i result = _mm_and_si128(src, mask);
|
|
_mm_storeu_si128((__m128i *)(l2 + i), result);
|
|
}
|
|
#else
|
|
#if 1 // generic vector code -- NEON and wasm?
|
|
const u32_v mask = C_SCANLINE_MASK - (u32_v){}; // broadcast
|
|
for (int i = 0; i < MII_VIDEO_WIDTH; i += VEC_ECOUNT,
|
|
screen += VEC_ECOUNT, l2 += VEC_ECOUNT) {
|
|
u32_v s = *(u32_v *)screen;
|
|
s &= mask;
|
|
*(u32_v *)l2 = s;
|
|
}
|
|
#else
|
|
for (int i = 0; i < MII_VIDEO_WIDTH; i++)
|
|
*l2++ = *screen++ & C_SCANLINE_MASK;
|
|
#endif
|
|
#endif
|
|
|
|
mii->video.lines_dirty[mii->video.line / 64] &=
|
|
~(1ULL << (mii->video.line & 63));
|
|
mii->video.frame_dirty = 1;
|
|
#if MII_VIDEO_DEBUG_HEAPMAP
|
|
mii->video.video_hmap[mii->video.line] = 0xff;
|
|
#endif
|
|
}
|
|
mii->video.line++;
|
|
if (mii->video.line == 192) {
|
|
mii->video.line = 0;
|
|
mii->video.line_addr = mii->video.base_addr;
|
|
mii->video.timer_max = MII_VIDEO_H_CYCLES;
|
|
res = mii->video.timer_max * mii->speed;
|
|
pt_yield(mii->video.state);
|
|
mii_bank_poke(sw, SWVBL, 0x00);
|
|
mii->video.timer_max = MII_VBL_UP_CYCLES;
|
|
res = mii->video.timer_max * mii->speed;
|
|
/*
|
|
* This is to handle the corner case where text has some blinking
|
|
* text, and we need to redraw the screen.
|
|
* We only check every 16 frames, so we don't waste time
|
|
* redrawing the screen every frame. Also, the alt char set needs
|
|
* to be off, as the blinking text is only in the main charset.
|
|
*/
|
|
uint32_t new_frame = mii->video.frame_count + 1;
|
|
if ((new_frame & MII_VIDEO_FLASH_FRAME_MASK) !=
|
|
(mii->video.frame_count & MII_VIDEO_FLASH_FRAME_MASK)) {
|
|
if (!SW_GETSTATE(mii, SWALTCHARSET))
|
|
_mii_video_mark_dirty(&mii->video);
|
|
}
|
|
mii->video.frame_count = new_frame;
|
|
pt_yield(mii->video.state);
|
|
// check if we need to switch the video mode, in case the UI switches
|
|
// Color/mono palette etc
|
|
mii->cpu.instruction_run = 0; // stop current instruction run!
|
|
if (mii->video.frame_dirty)
|
|
mii->video.frame_seed++;
|
|
mii->video.frame_dirty = 0;
|
|
} else {
|
|
mii->video.timer_max = MII_VIDEO_H_CYCLES + MII_VIDEO_HB_CYCLES;
|
|
res = mii->video.timer_max * mii->speed;
|
|
pt_yield(mii->video.state);
|
|
}
|
|
} while (1);
|
|
pt_end(mii->video.state);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* TODO: this doesn't work yet. Don't get overexcited about this.
|
|
* Or, get overexcited about this and fix it! :-)
|
|
*/
|
|
uint8_t
|
|
mii_video_get_vapor(
|
|
mii_t *mii)
|
|
{
|
|
uint8_t res = 0;
|
|
int64_t timer = mii_timer_get(mii, mii->video.timer_id);
|
|
timer = timer / mii->speed;
|
|
uint16_t addr = mii->video.line_addr;
|
|
int64_t current = mii->video.timer_max - timer;
|
|
addr += current - 25;
|
|
res = mii_bank_peek(&mii->bank[MII_BANK_MAIN], addr);
|
|
// printf("VAPOR %5ld/%5ld %04x->%04x %02x\n",
|
|
// current, mii->video.timer_max, mii->video.line_addr, addr, res);
|
|
return res;
|
|
}
|
|
|
|
bool
|
|
mii_access_video(
|
|
mii_t *mii,
|
|
uint16_t addr,
|
|
uint8_t * byte,
|
|
bool write)
|
|
{
|
|
bool res = false;
|
|
if (write)
|
|
mii->video.line_cb.check(&mii->video, mii->sw_state, addr);
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
switch (addr) {
|
|
case SWALTCHARSETOFF:
|
|
case SWALTCHARSETON:
|
|
if (!write) break;
|
|
res = true;
|
|
SW_SETSTATE(mii, SWALTCHARSET, addr & 1);
|
|
mii_bank_poke(sw, SWALTCHARSET, (addr & 1) << 7);
|
|
// in case there is some blinking text, we need to redraw
|
|
_mii_video_mark_dirty(&mii->video);
|
|
break;
|
|
case SWVBL:
|
|
case SW80COL:
|
|
case SWTEXT:
|
|
case SWMIXED:
|
|
case SWPAGE2:
|
|
case SWHIRES:
|
|
case SWALTCHARSET:
|
|
case SWRDDHIRES:
|
|
res = true;
|
|
if (!write)
|
|
*byte = mii_bank_peek(sw, addr);
|
|
break;
|
|
case SWHIRESOFF:
|
|
case SWHIRESON:
|
|
// res = true; // we return false here, so generic SW code is called
|
|
SW_SETSTATE(mii, SWHIRES, addr & 1);
|
|
mii_bank_poke(sw, SWHIRES, (addr & 1) << 7);
|
|
_mii_video_mode_changed(mii);
|
|
break;
|
|
case SWPAGE2OFF:
|
|
case SWPAGE2ON:
|
|
// res = true; // we return false here, so generic SW code is called
|
|
SW_SETSTATE(mii, SWPAGE2, addr & 1);
|
|
mii_bank_poke(sw, SWPAGE2, (addr & 1) << 7);
|
|
if (!write)
|
|
*byte = mii_bank_peek(sw, SWPAGE2);
|
|
// 80STORE completely changes the meaning of PAGE2
|
|
if (!SW_GETSTATE(mii, SW80STORE)) {
|
|
_mii_video_mode_changed(mii);
|
|
_mii_video_mark_dirty(&mii->video);
|
|
}
|
|
break;
|
|
case SW80COLOFF:
|
|
case SW80COLON:
|
|
if (!write) break;
|
|
res = true;
|
|
SW_SETSTATE(mii, SW80COL, addr & 1);
|
|
mii_bank_poke(sw, SW80COL, (addr & 1) << 7);
|
|
_mii_video_mode_changed(mii);
|
|
break;
|
|
case SWDHIRESOFF: // 0xc05f,
|
|
case SWDHIRESON: { // = 0xc05e,
|
|
res = true;
|
|
uint8_t an3 = !!mii_bank_peek(sw, SWAN3);
|
|
bool an3_on = !!(addr & 1); // 5f is ON, 5e is OFF
|
|
uint8_t reg = mii_bank_peek(sw, SWAN3_REGISTER);
|
|
if (an3_on && !an3) {
|
|
uint8_t bit = SW_GETSTATE(mii, SW80COL);
|
|
reg = ((reg << 1) | bit) & 3;
|
|
// printf("VIDEO 80:%d REG now %x\n", bit, reg);
|
|
mii_bank_poke(sw, SWAN3_REGISTER, reg);
|
|
}
|
|
mii_bank_poke(sw, SWAN3, an3_on ? 0x80 : 0);
|
|
// printf("DHRES IS %s mode:%d\n", (addr & 1) ? "OFF" : "ON ", reg);
|
|
SW_SETSTATE(mii, SWDHIRES, !(addr & 1));
|
|
mii_bank_poke(sw, SWRDDHIRES, (!(addr & 1)) << 7);
|
|
_mii_video_mark_dirty(&mii->video);
|
|
_mii_video_mode_changed(mii);
|
|
} break;
|
|
case SWTEXTOFF:
|
|
case SWTEXTON:
|
|
res = true;
|
|
SW_SETSTATE(mii, SWTEXT, addr & 1);
|
|
mii_bank_poke(sw, SWTEXT, (addr & 1) << 7);
|
|
_mii_video_mode_changed(mii);
|
|
if (!write)
|
|
*byte = mii_video_get_vapor(mii);
|
|
break;
|
|
case SWMIXEDOFF:
|
|
case SWMIXEDON:
|
|
res = true;
|
|
SW_SETSTATE(mii, SWMIXED, addr & 1);
|
|
mii_bank_poke(sw, SWMIXED, (addr & 1) << 7);
|
|
_mii_video_mode_changed(mii);
|
|
break;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void
|
|
mii_video_full_refresh(
|
|
mii_t *mii)
|
|
{
|
|
_mii_video_mark_dirty(&mii->video);
|
|
|
|
if (mii->state == MII_RUNNING)
|
|
return;
|
|
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
_mii_video_mark_dirty(&mii->video);
|
|
do {
|
|
mii_video_timer_cb(mii, NULL);
|
|
} while (mii_bank_peek(sw, SWVBL));
|
|
do {
|
|
mii_video_timer_cb(mii, NULL);
|
|
} while (!mii_bank_peek(sw, SWVBL));
|
|
}
|
|
|
|
void
|
|
mii_video_init(
|
|
mii_t *mii)
|
|
{
|
|
mii->video.timer_id = mii_timer_register(mii,
|
|
mii_video_timer_cb, NULL, MII_VIDEO_H_CYCLES, __func__);
|
|
// start the DHRES in color
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
mii_bank_poke(sw, SWAN3_REGISTER, 1);
|
|
_mii_video_mode_changed(mii);
|
|
mii_video_set_mode(mii, 0);
|
|
}
|
|
|
|
typedef struct {
|
|
double r,g,b; // a fraction between 0 and 1
|
|
} frgb_t;
|
|
|
|
typedef struct {
|
|
double h; // angle in degrees
|
|
double s,v; // a fraction between 0 and 1
|
|
} fhsv_t;
|
|
|
|
static fhsv_t
|
|
rgb2hsv(frgb_t in)
|
|
{
|
|
fhsv_t out;
|
|
double min, max, delta;
|
|
|
|
min = in.r < in.g ? in.r : in.g;
|
|
min = min < in.b ? min : in.b;
|
|
max = in.r > in.g ? in.r : in.g;
|
|
max = max > in.b ? max : in.b;
|
|
|
|
out.v = max; // v
|
|
delta = max - min;
|
|
if (delta < 0.00001) {
|
|
out.s = 0;
|
|
out.h = 0; // undefined, maybe nan?
|
|
return out;
|
|
}
|
|
if (max > 0.0) { // NOTE: if Max is == 0, this divide would cause a crash
|
|
out.s = (delta / max); // s
|
|
} else {
|
|
// if max is 0, then r = g = b = 0
|
|
// s = 0, h is undefined
|
|
out.s = 0.0;
|
|
out.h = NAN; // its now undefined
|
|
return out;
|
|
}
|
|
if (in.r >= max) // > is bogus, just keeps compilor happy
|
|
out.h = (in.g - in.b) / delta; // between yellow & magenta
|
|
else if (in.g >= max)
|
|
out.h = 2.0 + (in.b - in.r) / delta; // between cyan & yellow
|
|
else
|
|
out.h = 4.0 + (in.r - in.g) / delta; // between magenta & cyan
|
|
out.h *= 60.0; // degrees
|
|
if (out.h < 0.0)
|
|
out.h += 360.0;
|
|
return out;
|
|
}
|
|
|
|
static frgb_t
|
|
hsv2rgb(fhsv_t in)
|
|
{
|
|
double hh, p, q, t, ff;
|
|
long i;
|
|
frgb_t out;
|
|
|
|
if (in.s <= 0.0) {
|
|
out.r = out.g = out.b = in.v;
|
|
return out;
|
|
}
|
|
hh = in.h;
|
|
if (hh >= 360.0) {
|
|
hh = 0.0;
|
|
}
|
|
hh /= 60.0;
|
|
i = (long)hh;
|
|
ff = hh - i;
|
|
p = in.v * (1.0 - in.s);
|
|
q = in.v * (1.0 - (in.s * ff));
|
|
t = in.v * (1.0 - (in.s * (1.0 - ff)));
|
|
|
|
switch (i) {
|
|
case 0: out.r = in.v; out.g = t; out.b = p; break;
|
|
case 1: out.r = q; out.g = in.v; out.b = p; break;
|
|
case 2: out.r = p; out.g = in.v; out.b = t; break;
|
|
case 3: out.r = p; out.g = q; out.b = in.v; break;
|
|
case 4: out.r = t; out.g = p; out.b = in.v; break;
|
|
case 5:
|
|
default: out.r = in.v; out.g = p; out.b = q; break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
* Takes a RGB color, and a base color, and returns a color that is
|
|
* the same luminance as the RGB color, but with the hue of the base color
|
|
* This is not an exact formula, and there are some chroma drifts, but it
|
|
* will do for now.
|
|
*/
|
|
static inline uint32_t
|
|
_mii_rgb_to_lumed_color(
|
|
uint32_t rgb,
|
|
uint32_t base)
|
|
{
|
|
#if 0
|
|
uint8_t r, g, b;
|
|
HI_GET_RGB(rgb, r, g, b);
|
|
uint8_t br, bg, bb;
|
|
HI_GET_RGB(base, br, bg, bb);
|
|
frgb_t in = { r / 255.0, g / 255.0, b / 255.0 };
|
|
frgb_t base_in = { br / 255.0, bg / 255.0, bb / 255.0 };
|
|
fhsv_t hsv = rgb2hsv(in);
|
|
fhsv_t base_hsv = rgb2hsv(base_in);
|
|
fhsv_t n = base_hsv;
|
|
n.v *= hsv.v;
|
|
frgb_t out = hsv2rgb(n);
|
|
r = out.r * 255;
|
|
g = out.g * 255;
|
|
b = out.b * 255;
|
|
return HI_RGB(r, g, b);
|
|
#else
|
|
uint8_t r, g, b;
|
|
HI_GET_RGB(rgb, r, g, b);
|
|
uint8_t l = HI_LUMA(r, g, b);
|
|
if (l == 0)
|
|
return HI_RGB(0,0,0);
|
|
uint8_t br, bg, bb;
|
|
HI_GET_RGB(base, br, bg, bb);
|
|
// uint8_t bl = HI_LUMA(br, bg, bb);
|
|
r = (br * l) / 255;
|
|
g = (bg * l) / 255;
|
|
b = (bb * l) / 255;
|
|
rgb = HI_RGB(r, g, b);
|
|
return rgb;
|
|
#endif
|
|
}
|
|
|
|
|
|
void
|
|
mii_video_set_mode(
|
|
mii_t *mii,
|
|
uint8_t mode)
|
|
{
|
|
// used to implement cycling through palettes
|
|
if (mode >= (sizeof(palettes) / sizeof(palettes[0])))
|
|
mode = 0;
|
|
// printf("%s mode %d\n", __func__, mode);
|
|
mii->video.color_mode = mode;
|
|
mii_video_clut_t * clut = &mii->video.clut;
|
|
|
|
uint32_t base = palettes[mode].mono_color;
|
|
mii->video.monochrome = base != 0;
|
|
if (mii->video.monochrome) {
|
|
// convert one set of RGB colors to monochrome. arbitrarily 0
|
|
const mii_palette_t * pal = &palettes[0];
|
|
// base CLUT is using color *indexes* in the palette we picked
|
|
for (uint i = 0; i < sizeof(clut->colors) / sizeof(clut->colors[0]); i++)
|
|
clut->colors[i] = pal->color[mii_base_clut.colors[i]];
|
|
for (uint i = 0; i < sizeof(clut->colors) / sizeof(clut->colors[0]); i++) {
|
|
clut->colors[i] = _mii_rgb_to_lumed_color(
|
|
pal->color[mii_base_clut.colors[i]], base);
|
|
}
|
|
// now calculate a new lores color table, with dimmer colors
|
|
uint8_t br, bg, bb;
|
|
HI_GET_RGB(base, br, bg, bb);
|
|
frgb_t base_in = { br / 255.0, bg / 255.0, bb / 255.0 };
|
|
fhsv_t base_hsv = rgb2hsv(base_in);
|
|
base_hsv.v /= 2.0;
|
|
frgb_t out = hsv2rgb(base_hsv);
|
|
br = out.r * 255;
|
|
bg = out.g * 255;
|
|
bb = out.b * 255;
|
|
base = HI_RGB(br, bg, bb);
|
|
|
|
clut = &mii->video.clut_low;
|
|
*clut = mii->video.clut;
|
|
|
|
for (uint i = 0; i < sizeof(clut->colors) / sizeof(clut->colors[0]); i++) {
|
|
clut->colors[i] = _mii_rgb_to_lumed_color(
|
|
clut->colors[i], base);
|
|
}
|
|
} else {
|
|
const mii_palette_t * pal = &palettes[mode];
|
|
// base CLUT is using color *indexes* in the palette we picked
|
|
for (uint i = 0; i < sizeof(clut->colors) / sizeof(clut->colors[0]); i++)
|
|
clut->colors[i] = pal->color[mii_base_clut.colors[i]];
|
|
clut = &mii->video.clut_low;
|
|
*clut = mii->video.clut;
|
|
|
|
for (uint i = 0; i < sizeof(clut->colors) / sizeof(clut->colors[0]); i++) {
|
|
uint8_t br, bg, bb;
|
|
HI_GET_RGB(clut->colors[i], br, bg, bb);
|
|
frgb_t base_in = { br / 255.0, bg / 255.0, bb / 255.0 };
|
|
fhsv_t base_hsv = rgb2hsv(base_in);
|
|
base_hsv.s *= 0.75;
|
|
base_hsv.v *= 0.75;
|
|
frgb_t out = hsv2rgb(base_hsv);
|
|
br = out.r * 255;
|
|
bg = out.g * 255;
|
|
bb = out.b * 255;
|
|
clut->colors[i] = HI_RGB(br, bg, bb);
|
|
}
|
|
}
|
|
mii_video_full_refresh(mii);
|
|
}
|
|
|
|
|
|
static void
|
|
_mii_mish_video(
|
|
void * param,
|
|
int argc,
|
|
const char * argv[])
|
|
{
|
|
mii_t * mii = param;
|
|
|
|
if (!argv[1] || !strcmp(argv[1], "list")) {
|
|
for (int i = 0; i < 16; i++) {
|
|
printf("%01x: %08x %08x %08x\n", i,
|
|
mii->video.clut.lores[0][i],
|
|
mii->video.clut.lores[1][i],
|
|
mii->video.clut.dhires[i]);
|
|
}
|
|
return;
|
|
}
|
|
if (!strcmp(argv[1], "color")) {
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
uint8_t reg = mii_bank_peek(sw, SWAN3_REGISTER);
|
|
printf("AN3 REG %d -> %d\n", reg, 1);
|
|
mii_bank_poke(sw, SWAN3_REGISTER, 1);
|
|
_mii_video_mode_changed(mii);
|
|
mii_video_full_refresh(mii);
|
|
return;
|
|
}
|
|
if (!strcmp(argv[1], "mono")) {
|
|
mii_bank_t * sw = &mii->bank[MII_BANK_SW];
|
|
uint8_t reg = mii_bank_peek(sw, SWAN3_REGISTER);
|
|
printf("AN3 REG %d -> %d\n", reg, 0);
|
|
mii_bank_poke(sw, SWAN3_REGISTER, 0);
|
|
_mii_video_mode_changed(mii);
|
|
mii_video_full_refresh(mii);
|
|
return;
|
|
}
|
|
if (!strcmp(argv[1], "dirty")) {
|
|
_mii_video_mode_changed(mii);
|
|
mii_video_full_refresh(mii);
|
|
return;
|
|
}
|
|
#ifdef TRACE
|
|
if (!strcmp(argv[1], "trace")) {
|
|
_trace = 1;
|
|
mii_video_full_refresh(mii);
|
|
_trace = 0;
|
|
return;
|
|
}
|
|
#endif
|
|
fprintf(stderr, "Unknown video command %s\n", argv[1]);
|
|
}
|
|
|
|
#include "mish.h"
|
|
|
|
MISH_CMD_NAMES(video, "video");
|
|
MISH_CMD_HELP(video,
|
|
"video: test patterns generator",
|
|
" <default>: dump color tables",
|
|
" list: dump color tables",
|
|
" color: set color mode",
|
|
" mono: set mono mode",
|
|
" dirty: force full refresh"
|
|
);
|
|
MII_MISH(video, _mii_mish_video);
|