diff --git a/src/include/86box/vid_quadcolor.h b/src/include/86box/vid_quadcolor.h
new file mode 100644
index 000000000..41b39dfb0
--- /dev/null
+++ b/src/include/86box/vid_quadcolor.h
@@ -0,0 +1,93 @@
+/*
+ * 86Box A hypervisor and IBM PC system emulator that specializes in
+ * running old operating systems and software designed for IBM
+ * PC systems and compatibles from 1981 through fairly recent
+ * system designs based on the PCI bus.
+ *
+ * This file is part of the 86Box distribution.
+ *
+ * Quadram Quadcolor I / I+II emulation
+ *
+ * Authors: Benedikt Freisen,
+ * Jasmine Iwanek,
+ *
+ * Copyright 2024 Benedikt Freisen.
+ Copyright 2025 Jasmine Iwanek.
+ */
+
+#ifndef _VID_QUADCOLOR_H_
+#define _VID_QUADCOLOR_H_
+
+typedef struct quadcolor_t {
+ mem_mapping_t mapping;
+ mem_mapping_t mapping_2;
+
+ int crtcreg;
+#if 0
+ uint8_t crtc[CGA_NUM_CRTC_REGS];
+#else
+ uint8_t crtc[32];
+#endif
+ uint8_t cgastat;
+
+ uint8_t cgamode;
+ uint8_t cgacol;
+
+ uint8_t lp_strobe;
+
+ uint8_t quadcolor_ctrl;
+ uint8_t quadcolor_2_oe;
+ uint16_t page_offset;
+
+ int fontbase;
+ int linepos;
+ int displine;
+ int scanline;
+ int vc;
+ int cgadispon;
+ int cursorvisible; // Determines if the cursor is visible FOR THE CURRENT SCANLINE.
+ int cursoron;
+ int cgablink;
+ int vsynctime;
+ int vadj;
+ uint16_t memaddr;
+ uint16_t memaddr_backup;
+ int oddeven;
+
+ int qc2idx;
+ uint8_t qc2mask;
+
+ uint64_t dispontime;
+ uint64_t dispofftime;
+ pc_timer_t timer;
+
+ int firstline;
+ int lastline;
+
+ int drawcursor;
+
+ int fullchange;
+
+ uint8_t *vram;
+ uint8_t *vram_2;
+
+ uint8_t charbuffer[256];
+
+ int revision;
+ int composite;
+ int rgb_type;
+ int double_type;
+
+ int has_2nd_charset;
+ int has_quadcolor_2;
+} quadcolor_t;
+
+void quadcolor_init(quadcolor_t *quadcolor);
+void quadcolor_out(uint16_t addr, uint8_t val, void *priv);
+uint8_t quadcolor_in(uint16_t addr, void *priv);
+void quadcolor_write(uint32_t addr, uint8_t val, void *priv);
+uint8_t quadcolor_read(uint32_t addr, void *priv);
+void quadcolor_recalctimings(quadcolor_t *quadcolor);
+void quadcolor_poll(void *priv);
+
+#endif /* _VID_QUADCOLOR_H_ */
diff --git a/src/include/86box/video.h b/src/include/86box/video.h
index 986529d6f..70d031a4f 100644
--- a/src/include/86box/video.h
+++ b/src/include/86box/video.h
@@ -491,6 +491,9 @@ extern const device_t paradise_wd90c11_megapc_device;
extern const device_t paradise_wd90c11_device;
extern const device_t paradise_wd90c30_device;
+/* Quadram Quadcolor I / I + II */
+extern const device_t quadcolor_device;
+
/* Realtek (S)VGA */
extern const device_t realtek_rtg3105_device;
extern const device_t realtek_rtg3106_device;
diff --git a/src/video/CMakeLists.txt b/src/video/CMakeLists.txt
index 4cac8b76d..1159c93e2 100644
--- a/src/video/CMakeLists.txt
+++ b/src/video/CMakeLists.txt
@@ -55,8 +55,9 @@ add_library(vid OBJECT
vid_cga_colorplus.c
vid_cga_ncr.c
vid_cga_olivetti.c
- vid_cga_toshiba_t1000.c
- vid_cga_toshiba_t3100e.c
+ vid_cga_quadcolor.c
+ vid_cga_toshiba_t1000.c
+ vid_cga_toshiba_t3100e.c
# PCJr/Tandy
vid_pcjr.c
diff --git a/src/video/vid_cga_quadcolor.c b/src/video/vid_cga_quadcolor.c
new file mode 100644
index 000000000..5974682c8
--- /dev/null
+++ b/src/video/vid_cga_quadcolor.c
@@ -0,0 +1,981 @@
+/*
+ * 86Box A hypervisor and IBM PC system emulator that specializes in
+ * running old operating systems and software designed for IBM
+ * PC systems and compatibles from 1981 through fairly recent
+ * system designs based on the PCI bus.
+ *
+ * This file is part of the 86Box distribution.
+ *
+ * Quadram Quadcolor I / I+II emulation
+ *
+ * Authors: Sarah Walker,
+ * Miran Grca,
+ * W. M. Martinez,
+ * Benedikt Freisen,
+ * Jasmine Iwanek,
+ *
+ * Copyright 2008-2019 Sarah Walker.
+ * Copyright 2016-2019 Miran Grca.
+ * Copyright 2023 W. M. Martinez
+ * Copyright 2024 Benedikt Freisen.
+ * Copyright 2025 Jasmine Iwanek.
+ */
+
+/* This has been derived from CGA emulation */
+/* omissions: simulated snow (Quadcolor has dual-ported RAM), single and dual 8x16 font configuration */
+/* additions: ports 0x3dd and 0x3de, 2nd char set, 2nd VRAM bank, hi-res bg color, Quadcolor II memory and mode */
+/* assumptions: MA line 12 XORed with Bank Select, hi-res bg is also border color, QC2 mode has simple address counter */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include <86box/86box.h>
+#include "cpu.h"
+#include <86box/io.h>
+#include <86box/timer.h>
+#include <86box/pit.h>
+#include <86box/mem.h>
+#include <86box/rom.h>
+#include <86box/device.h>
+#include <86box/video.h>
+#include <86box/vid_cga.h>
+#include <86box/vid_quadcolor.h>
+#include <86box/vid_cga_comp.h>
+#include <86box/plat_unused.h>
+
+#define CGA_RGB 0
+#define CGA_COMPOSITE 1
+
+#define COMPOSITE_OLD 0
+#define COMPOSITE_NEW 1
+
+#define DOUBLE_NONE 0
+#define DOUBLE_SIMPLE 1
+#define DOUBLE_INTERPOLATE_SRGB 2
+#define DOUBLE_INTERPOLATE_LINEAR 3
+
+#define DEVICE_VRAM 0x8000
+#define DEVICE_VRAM_MASK 0x7fff
+
+typedef union {
+ uint32_t color;
+ struct {
+ uint8_t b;
+ uint8_t g;
+ uint8_t r;
+ uint8_t a;
+ };
+} color_t;
+
+static uint8_t crtcmask[32] = {
+ 0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x7f, 0x7f, 0xf3, 0x1f, 0x7f, 0x1f, 0x3f, 0xff, 0x3f, 0xff,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+static uint8_t interp_lut[2][256][256];
+
+static video_timings_t timing_quadcolor = { .type = VIDEO_ISA, .write_b = 8, .write_w = 16, .write_l = 32, .read_b = 8, .read_w = 16, .read_l = 32 };
+
+void quadcolor_recalctimings(quadcolor_t *quadcolor);
+
+static void
+quadcolor_update_latch(quadcolor_t *quadcolor)
+{
+ uint32_t lp_latch = quadcolor->displine * quadcolor->crtc[CGA_CRTC_HDISP];
+
+ quadcolor->crtc[CGA_CRTC_LIGHT_PEN_ADDR_HIGH] = (lp_latch >> 8) & 0x3f;
+ quadcolor->crtc[CGA_CRTC_LIGHT_PEN_ADDR_LOW] = lp_latch & 0xff;
+}
+
+void
+quadcolor_out(uint16_t addr, uint8_t val, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+ uint8_t old;
+
+ if ((addr >= 0x3d0) && (addr <= 0x3d7))
+ addr = (addr & 0xff9) | 0x004;
+
+ switch (addr) {
+ case CGA_REGISTER_CRTC_INDEX:
+ quadcolor->crtcreg = val & 31;
+ return;
+ case CGA_REGISTER_CRTC_DATA:
+ old = quadcolor->crtc[quadcolor->crtcreg];
+ quadcolor->crtc[quadcolor->crtcreg] = val & crtcmask[quadcolor->crtcreg];
+ if (old != val) {
+ // Recalc the timings if we are writing any invalid CRTC register or a valid CRTC register
+ // except the CURSOR and LIGHT PEN registers
+ if ((quadcolor->crtcreg < 0xe) || (quadcolor->crtcreg > 0x11)) {
+ quadcolor->fullchange = changeframecount;
+ quadcolor_recalctimings(quadcolor);
+ }
+ }
+ return;
+ case CGA_REGISTER_MODE_CONTROL:
+ old = quadcolor->cgamode;
+ quadcolor->cgamode = val;
+
+ if (old ^ val) {
+ if ((old ^ val) & 0x07)
+ update_cga16_color(val);
+
+ quadcolor_recalctimings(quadcolor);
+ }
+ return;
+ case CGA_REGISTER_COLOR_SELECT:
+ old = quadcolor->cgacol;
+ quadcolor->cgacol = val;
+ if (old ^ val)
+ quadcolor_recalctimings(quadcolor);
+ return;
+
+ case CGA_REGISTER_CLEAR_LIGHT_PEN_LATCH:
+ if (quadcolor->lp_strobe == 1)
+ quadcolor->lp_strobe = 0;
+ return;
+ case CGA_REGISTER_SET_LIGHT_PEN_LATCH:
+ if (quadcolor->lp_strobe == 0) {
+ quadcolor->lp_strobe = 1;
+ quadcolor_update_latch(quadcolor);
+ }
+ return;
+
+ case 0x3dd:
+ quadcolor->quadcolor_ctrl = val & 0x3f;
+ /* helper variable that can be XORed onto the VRAM address to select the page to display */
+ quadcolor->page_offset = (val & 0x10) << 8;
+ /* in dual 8x8 font configuration, use fontbase 256 if "Character Set Select" bit is set */
+ if (quadcolor->has_2nd_charset)
+ quadcolor->fontbase = (val & 0x20) << 3;
+ return;
+ case 0x3de:
+ /* NOTE: the polarity of this register is the opposite of what the manual says */
+ if (quadcolor->has_quadcolor_2)
+ quadcolor->quadcolor_2_oe = !(val & 0x10);
+ return;
+
+ default:
+ break;
+ }
+}
+
+uint8_t
+quadcolor_in(uint16_t addr, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+ uint8_t ret = 0xff;
+
+ if ((addr >= 0x3d0) && (addr <= 0x3d7))
+ addr = (addr & 0xff9) | 0x004;
+
+ switch (addr) {
+ case CGA_REGISTER_CRTC_INDEX:
+ ret = quadcolor->crtcreg;
+ break;
+ case CGA_REGISTER_CRTC_DATA:
+ ret = quadcolor->crtc[quadcolor->crtcreg];
+ break;
+ case CGA_REGISTER_STATUS:
+ ret = quadcolor->cgastat;
+ break;
+ case CGA_REGISTER_CLEAR_LIGHT_PEN_LATCH:
+ if (quadcolor->lp_strobe == 1)
+ quadcolor->lp_strobe = 0;
+ break;
+ case CGA_REGISTER_SET_LIGHT_PEN_LATCH:
+ if (quadcolor->lp_strobe == 0) {
+ quadcolor->lp_strobe = 1;
+ quadcolor_update_latch(quadcolor);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+void
+quadcolor_waitstates(UNUSED(void *priv))
+{
+ int ws_array[16] = { 3, 4, 5, 6, 7, 8, 4, 5, 6, 7, 8, 4, 5, 6, 7, 8 };
+ int ws;
+
+ ws = ws_array[cycles & 0xf];
+ cycles -= ws;
+}
+
+void
+quadcolor_write(uint32_t addr, uint8_t val, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+
+ quadcolor->vram[addr & DEVICE_VRAM_MASK] = val;
+ quadcolor_waitstates(quadcolor);
+}
+
+uint8_t
+quadcolor_read(uint32_t addr, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+
+ quadcolor_waitstates(quadcolor);
+ return quadcolor->vram[addr & DEVICE_VRAM_MASK];
+}
+
+void
+quadcolor_2_write(uint32_t addr, uint8_t val, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+ quadcolor->vram_2[addr & 0xffff] = val;
+}
+
+uint8_t
+quadcolor_2_read(uint32_t addr, void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+ return quadcolor->vram_2[addr & 0xffff];
+}
+
+void
+quadcolor_recalctimings(quadcolor_t *quadcolor)
+{
+ double disptime;
+ double _dispontime;
+ double _dispofftime;
+
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES) {
+ disptime = (double) (quadcolor->crtc[CGA_CRTC_HTOTAL] + 1);
+ _dispontime = (double) quadcolor->crtc[CGA_CRTC_HDISP];
+ } else {
+ disptime = (double) ((quadcolor->crtc[CGA_CRTC_HTOTAL] + 1) << 1);
+ _dispontime = (double) (quadcolor->crtc[CGA_CRTC_HDISP] << 1);
+ }
+ _dispofftime = disptime - _dispontime;
+ _dispontime = _dispontime * CGACONST;
+ _dispofftime = _dispofftime * CGACONST;
+ quadcolor->dispontime = (uint64_t) (_dispontime);
+ quadcolor->dispofftime = (uint64_t) (_dispofftime);
+}
+
+static inline uint8_t
+get_next_qc2_pixel(quadcolor_t *quadcolor)
+{
+ uint8_t mask = quadcolor->qc2mask;
+ quadcolor->qc2mask = ~quadcolor->qc2mask;
+ uint8_t pixel = (quadcolor->vram_2[quadcolor->qc2idx] & mask) >> (quadcolor->qc2mask & 4);
+
+ quadcolor->qc2idx += quadcolor->qc2mask >> 7;
+
+ return quadcolor->quadcolor_2_oe ? pixel : 0;
+}
+
+static void
+quadcolor_render(quadcolor_t *quadcolor, int line)
+{
+ uint16_t cursoraddr = (quadcolor->crtc[CGA_CRTC_CURSOR_ADDR_LOW] | (quadcolor->crtc[CGA_CRTC_CURSOR_ADDR_HIGH] << 8)) & DEVICE_VRAM_MASK;
+ int drawcursor;
+ int x;
+ int column;
+ uint8_t chr;
+ uint8_t attr;
+ uint16_t dat;
+ int cols[4];
+ int col;
+
+ int32_t highres_graphics_flag = (CGA_MODE_FLAG_HIGHRES_GRAPHICS | CGA_MODE_FLAG_GRAPHICS);
+
+ if (((quadcolor->cgamode & highres_graphics_flag) == highres_graphics_flag)) {
+ for (column = 0; column < 8; ++column) {
+ buffer32->line[line][column] = 0;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)
+ buffer32->line[line][column + (quadcolor->crtc[CGA_CRTC_HDISP] << 3) + 8] = (quadcolor->quadcolor_ctrl & 15); /* TODO: Is Quadcolor bg color actually relevant, here? */
+ else
+ buffer32->line[line][column + (quadcolor->crtc[CGA_CRTC_HDISP] << 4) + 8] = (quadcolor->quadcolor_ctrl & 15); /* TODO: Is Quadcolor bg color actually relevant, here? */
+ }
+ } else {
+ for (column = 0; column < 8; ++column) {
+ buffer32->line[line][column] = (quadcolor->cgacol & 15) + 16;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)
+ buffer32->line[line][column + (quadcolor->crtc[CGA_CRTC_HDISP] << 3) + 8] = (quadcolor->cgacol & 15) + 16;
+ else
+ buffer32->line[line][column + (quadcolor->crtc[CGA_CRTC_HDISP] << 4) + 8] = (quadcolor->cgacol & 15) + 16;
+ }
+ }
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES) { /* 80-column text */
+ for (x = 0; x < quadcolor->crtc[CGA_CRTC_HDISP]; x++) {
+ if (quadcolor->cgamode & CGA_MODE_FLAG_VIDEO_ENABLE) {
+ chr = quadcolor->charbuffer[x << 1];
+ attr = quadcolor->charbuffer[(x << 1) + 1];
+ } else
+ chr = attr = 0;
+ drawcursor = ((quadcolor->memaddr == cursoraddr) && quadcolor->cursorvisible && quadcolor->cursoron);
+ cols[1] = (attr & 15) + 16;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_BLINK) {
+ cols[0] = ((attr >> 4) & 7) + 16;
+ if ((quadcolor->cgablink & 8) && (attr & 0x80) && !quadcolor->drawcursor)
+ cols[1] = cols[0];
+ } else
+ cols[0] = (attr >> 4) + 16;
+ if (drawcursor) {
+ for (column = 0; column < 8; column++) {
+ buffer32->line[line][(x << 3) + column + 8]
+ = (cols[(fontdat[chr + quadcolor->fontbase][quadcolor->scanline & 7] & (1 << (column ^ 7))) ? 1 : 0] ^ 15) | get_next_qc2_pixel(quadcolor);
+ }
+ } else {
+ for (column = 0; column < 8; column++) {
+ buffer32->line[line][(x << 3) + column + 8]
+ = cols[(fontdat[chr + quadcolor->fontbase][quadcolor->scanline & 7] & (1 << (column ^ 7))) ? 1 : 0] | get_next_qc2_pixel(quadcolor);
+ }
+ }
+ quadcolor->memaddr++;
+ }
+ } else if (!(quadcolor->cgamode & CGA_MODE_FLAG_GRAPHICS)) { /* not graphics (nor 80-column text) => 40-column text */
+ for (x = 0; x < quadcolor->crtc[CGA_CRTC_HDISP]; x++) {
+ if (quadcolor->cgamode & CGA_MODE_FLAG_VIDEO_ENABLE) {
+ chr = quadcolor->vram[(quadcolor->page_offset ^ (quadcolor->memaddr << 1)) & DEVICE_VRAM_MASK];
+ attr = quadcolor->vram[(quadcolor->page_offset ^ ((quadcolor->memaddr << 1) + 1)) & DEVICE_VRAM_MASK];
+ } else
+ chr = attr = 0;
+ drawcursor = ((quadcolor->memaddr == cursoraddr) && quadcolor->cursorvisible && quadcolor->cursoron);
+ cols[1] = (attr & 15) + 16;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_BLINK) {
+ cols[0] = ((attr >> 4) & 7) + 16;
+ if ((quadcolor->cgablink & 8) && (attr & 0x80))
+ cols[1] = cols[0];
+ } else
+ cols[0] = (attr >> 4) + 16;
+ quadcolor->memaddr++;
+ if (drawcursor) {
+ for (column = 0; column < 8; column++) {
+ dat = (cols[(fontdat[chr + quadcolor->fontbase][quadcolor->scanline & 7] & (1 << (column ^ 7))) ? 1 : 0] ^ 15);
+ buffer32->line[line][(x << 4) + (column << 1) + 8] = dat | get_next_qc2_pixel(quadcolor);
+ buffer32->line[line][(x << 4) + (column << 1) + 9] = dat | get_next_qc2_pixel(quadcolor);
+
+ }
+ } else {
+ for (column = 0; column < 8; column++) {
+ dat = cols[(fontdat[chr + quadcolor->fontbase][quadcolor->scanline & 7] & (1 << (column ^ 7))) ? 1 : 0];
+ buffer32->line[line][(x << 4) + (column << 1) + 8] = dat | get_next_qc2_pixel(quadcolor);
+ buffer32->line[line][(x << 4) + (column << 1) + 9] = dat | get_next_qc2_pixel(quadcolor);
+
+ }
+ }
+ }
+ } else if (!(quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES_GRAPHICS)) { /* not hi-res (but graphics) => 4-color mode */
+ cols[0] = (quadcolor->cgacol & 15) | 16;
+ col = (quadcolor->cgacol & 16) ? 24 : 16;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_BW) {
+ cols[1] = col | 3; /* Cyan */
+ cols[2] = col | 4; /* Red */
+ cols[3] = col | 7; /* White */
+ } else if (quadcolor->cgacol & 32) {
+ cols[1] = col | 3; /* Cyan */
+ cols[2] = col | 5; /* Magenta */
+ cols[3] = col | 7; /* White */
+ } else {
+ cols[1] = col | 2; /* Green */
+ cols[2] = col | 4; /* Red */
+ cols[3] = col | 6; /* Yellow */
+ }
+ for (x = 0; x < quadcolor->crtc[CGA_CRTC_HDISP]; x++) {
+ if (quadcolor->cgamode & CGA_MODE_FLAG_VIDEO_ENABLE)
+ dat = (quadcolor->vram[quadcolor->page_offset ^ (((quadcolor->memaddr << 1) & 0x1fff) + ((quadcolor->scanline & 1) * 0x2000))] << 8) |
+ quadcolor->vram[quadcolor->page_offset ^ (((quadcolor->memaddr << 1) & 0x1fff) + ((quadcolor->scanline & 1) * 0x2000) + 1)];
+ else
+ dat = 0;
+ quadcolor->memaddr++;
+ for (column = 0; column < 8; column++) {
+ buffer32->line[line][(x << 4) + (column << 1) + 8] = cols[dat >> 14] | get_next_qc2_pixel(quadcolor);
+ buffer32->line[line][(x << 4) + (column << 1) + 9] = cols[dat >> 14] | get_next_qc2_pixel(quadcolor);
+ dat <<= 2;
+ }
+ }
+ } else { /* 2-color hi-res graphics mode */
+ cols[0] = quadcolor->quadcolor_ctrl & 15; /* background color (Quadcolor-specific) */;
+ cols[1] = (quadcolor->cgacol & 15) + 16;
+ for (x = 0; x < quadcolor->crtc[CGA_CRTC_HDISP]; x++) {
+ if (quadcolor->cgamode & CGA_MODE_FLAG_VIDEO_ENABLE) /* video enabled */
+ dat = (quadcolor->vram[quadcolor->page_offset ^ (((quadcolor->memaddr << 1) & 0x1fff) + ((quadcolor->scanline & 1) * 0x2000))] << 8) |
+ quadcolor->vram[quadcolor->page_offset ^ (((quadcolor->memaddr << 1) & 0x1fff) + ((quadcolor->scanline & 1) * 0x2000) + 1)];
+ else
+ dat = quadcolor->quadcolor_ctrl & 15; /* TODO: Is Quadcolor bg color actually relevant, here? Probably. See QC2 manual p.46 1. */;
+ quadcolor->memaddr++;
+ for (column = 0; column < 16; column++) {
+ buffer32->line[line][(x << 4) + column + 8] = cols[dat >> 15] | get_next_qc2_pixel(quadcolor);
+ dat <<= 1;
+ }
+ }
+ }
+}
+
+static void
+quadcolor_render_blank(quadcolor_t *quadcolor, int line)
+{
+ int32_t highres_graphics_flag = (CGA_MODE_FLAG_HIGHRES_GRAPHICS | CGA_MODE_FLAG_GRAPHICS);
+
+ /* `+ 16` isn't in PCem's version */
+ int col = ((quadcolor->cgamode & highres_graphics_flag) == highres_graphics_flag) ? (quadcolor->quadcolor_ctrl & 15) + 16 : (quadcolor->cgacol & 15) + 16; /* TODO: Is Quadcolor bg color actually relevant, here? */
+
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)
+ hline(buffer32, 0, line, (quadcolor->crtc[CGA_CRTC_HDISP] << 3) + 16, col);
+ else
+ hline(buffer32, 0, line, (quadcolor->crtc[CGA_CRTC_HDISP] << 4) + 16, col);
+}
+
+static void
+quadcolor_render_process(quadcolor_t *quadcolor, int line)
+{
+ int x;
+ uint8_t border;
+ int32_t highres_graphics_flag = (CGA_MODE_FLAG_HIGHRES_GRAPHICS | CGA_MODE_FLAG_GRAPHICS);
+
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)
+ x = (quadcolor->crtc[CGA_CRTC_HDISP] << 3) + 16;
+ else
+ x = (quadcolor->crtc[CGA_CRTC_HDISP] << 4) + 16;
+
+ if (quadcolor->composite) {
+ border = ((quadcolor->cgamode & highres_graphics_flag) == highres_graphics_flag) ? 0 : (quadcolor->cgacol & 15);
+
+ Composite_Process(quadcolor->cgamode, border, x >> 2, buffer32->line[line]);
+ } else
+ video_process_8(x, line);
+}
+
+static uint8_t
+quadcolor_interpolate_srgb(uint8_t co1, uint8_t co2, double fraction)
+{
+ uint8_t ret = ((co2 - co1) * fraction + co1);
+
+ return ret;
+}
+
+static uint8_t
+quadcolor_interpolate_linear(uint8_t co1, uint8_t co2, double fraction)
+{
+ double c1, c2;
+ double r1, r2;
+ uint8_t ret;
+
+ c1 = ((double) co1) / 255.0;
+ c1 = pow((co1 >= 0) ? c1 : -c1, 2.19921875);
+ if (co1 <= 0)
+ c1 = -c1;
+ c2 = ((double) co2) / 255.0;
+ c2 = pow((co2 >= 0) ? c2 : -c2, 2.19921875);
+ if (co2 <= 0)
+ c2 = -c2;
+ r1 = ((c2 - c1) * fraction + c1);
+ r2 = pow((r1 >= 0.0) ? r1 : -r1, 1.0 / 2.19921875);
+ if (r1 <= 0.0)
+ r2 = -r2;
+ ret = (uint8_t) round(r2 * 255.0);
+
+ return ret;
+}
+
+static color_t
+quadcolor_interpolate_lookup(quadcolor_t *quadcolor, color_t color1, color_t color2, UNUSED(double fraction))
+{
+ color_t ret;
+ uint8_t dt = quadcolor->double_type - DOUBLE_INTERPOLATE_SRGB;
+
+ ret.a = 0x00;
+ ret.r = interp_lut[dt][color1.r][color2.r];
+ ret.g = interp_lut[dt][color1.g][color2.g];
+ ret.b = interp_lut[dt][color1.b][color2.b];
+
+ return ret;
+}
+
+static void
+quadcolor_interpolate(quadcolor_t *quadcolor, int x, int y, int w, int h)
+{
+ double quotient = 0.5;
+
+ for (int i = y; i < (y + h); i++) {
+ if (i & 1) for (int j = x; j < (x + w); j++) {
+ int prev = i - 1;
+ int next = i + 1;
+ color_t prev_color, next_color;
+ color_t black;
+ color_t interim_1, interim_2;
+ color_t final;
+
+ if (i < 0)
+ continue;
+
+ black.color = 0x00000000;
+
+ if ((prev >= 0) && (prev < (y + h)))
+ prev_color.color = buffer32->line[prev][j];
+ else
+ prev_color.color = 0x00000000;
+
+ if ((next >= 0) && (next < (y + h)))
+ next_color.color = buffer32->line[next][j];
+ else
+ next_color.color = 0x00000000;
+
+ interim_1 = quadcolor_interpolate_lookup(quadcolor, prev_color, black, quotient);
+ interim_2 = quadcolor_interpolate_lookup(quadcolor, black, next_color, quotient);
+ final = quadcolor_interpolate_lookup(quadcolor, interim_1, interim_2, quotient);
+
+ buffer32->line[i][j] = final.color;
+ }
+ }
+}
+
+static void
+quadcolor_blit_memtoscreen(quadcolor_t *quadcolor, int x, int y, int w, int h)
+{
+ if (quadcolor->double_type > DOUBLE_SIMPLE)
+ quadcolor_interpolate(quadcolor, x, y, w, h);
+
+ video_blit_memtoscreen(x, y, w, h);
+}
+
+void
+quadcolor_poll(void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+ int x;
+ int scanline_old;
+ int oldvc;
+ int xs_temp;
+ int ys_temp;
+ int old_ma;
+
+ if (!quadcolor->linepos) {
+ timer_advance_u64(&quadcolor->timer, quadcolor->dispofftime);
+ quadcolor->cgastat |= 1;
+ quadcolor->linepos = 1;
+ scanline_old = quadcolor->scanline;
+ if ((quadcolor->crtc[CGA_CRTC_INTERLACE] & 3) == 3)
+ quadcolor->scanline = ((quadcolor->scanline << 1) + quadcolor->oddeven) & 7;
+ if (quadcolor->cgadispon) {
+ if (quadcolor->displine < quadcolor->firstline) {
+ quadcolor->firstline = quadcolor->displine;
+ video_wait_for_buffer();
+ }
+ quadcolor->lastline = quadcolor->displine;
+ switch (quadcolor->double_type) {
+ default:
+ quadcolor_render(quadcolor, quadcolor->displine << 1);
+ quadcolor_render_blank(quadcolor, (quadcolor->displine << 1) + 1);
+ break;
+ case DOUBLE_NONE:
+ quadcolor_render(quadcolor, quadcolor->displine);
+ break;
+ case DOUBLE_SIMPLE:
+ old_ma = quadcolor->memaddr;
+ quadcolor_render(quadcolor, quadcolor->displine << 1);
+ quadcolor->memaddr = old_ma;
+ quadcolor_render(quadcolor, (quadcolor->displine << 1) + 1);
+ break;
+ }
+ } else {
+ switch (quadcolor->double_type) {
+ default:
+ quadcolor_render_blank(quadcolor, quadcolor->displine << 1);
+ break;
+ case DOUBLE_NONE:
+ quadcolor_render_blank(quadcolor, quadcolor->displine);
+ break;
+ case DOUBLE_SIMPLE:
+ quadcolor_render_blank(quadcolor, quadcolor->displine << 1);
+ quadcolor_render_blank(quadcolor, (quadcolor->displine << 1) + 1);
+ break;
+ }
+ }
+
+ switch (quadcolor->double_type) {
+ default:
+ quadcolor_render_process(quadcolor, quadcolor->displine << 1);
+ quadcolor_render_process(quadcolor, (quadcolor->displine << 1) + 1);
+ break;
+ case DOUBLE_NONE:
+ quadcolor_render_process(quadcolor, quadcolor->displine);
+ break;
+ }
+
+ quadcolor->scanline = scanline_old;
+ if (quadcolor->vc == quadcolor->crtc[CGA_CRTC_VSYNC] && !quadcolor->scanline)
+ quadcolor->cgastat |= 8;
+ quadcolor->displine++;
+ if (quadcolor->displine >= 360)
+ quadcolor->displine = 0;
+ } else {
+ timer_advance_u64(&quadcolor->timer, quadcolor->dispontime);
+ quadcolor->linepos = 0;
+ if (quadcolor->vsynctime) {
+ quadcolor->vsynctime--;
+ if (!quadcolor->vsynctime)
+ quadcolor->cgastat &= ~8;
+ quadcolor->qc2idx = 0;
+ quadcolor->qc2mask = 0xf0;
+ }
+ if (quadcolor->scanline == (quadcolor->crtc[CGA_CRTC_CURSOR_END] & 31) || ((quadcolor->crtc[CGA_CRTC_INTERLACE] & 3) == 3 &&
+ quadcolor->scanline == ((quadcolor->crtc[CGA_CRTC_CURSOR_END] & 31) >> 1))) {
+ quadcolor->cursorvisible = 0;
+ }
+ if ((quadcolor->crtc[CGA_CRTC_INTERLACE] & 3) == 3 && quadcolor->scanline == (quadcolor->crtc[CGA_CRTC_MAX_SCANLINE_ADDR] >> 1))
+ quadcolor->memaddr_backup = quadcolor->memaddr;
+ if (quadcolor->vadj) {
+ quadcolor->scanline++;
+ quadcolor->scanline &= 31;
+ quadcolor->memaddr = quadcolor->memaddr_backup;
+ quadcolor->vadj--;
+ if (!quadcolor->vadj) {
+ quadcolor->cgadispon = 1;
+ quadcolor->memaddr = quadcolor->memaddr_backup = (quadcolor->crtc[CGA_CRTC_START_ADDR_LOW] | (quadcolor->crtc[CGA_CRTC_START_ADDR_HIGH] << 8)) & DEVICE_VRAM_MASK;
+ quadcolor->scanline = 0;
+ }
+ } else if (quadcolor->scanline == quadcolor->crtc[CGA_CRTC_MAX_SCANLINE_ADDR]) {
+ quadcolor->memaddr_backup = quadcolor->memaddr;
+ quadcolor->scanline = 0;
+ oldvc = quadcolor->vc;
+ quadcolor->vc++;
+ quadcolor->vc &= 127;
+
+ if (quadcolor->vc == quadcolor->crtc[CGA_CRTC_VDISP])
+ quadcolor->cgadispon = 0;
+
+ if (oldvc == quadcolor->crtc[CGA_CRTC_VTOTAL]) {
+ quadcolor->vc = 0;
+ quadcolor->vadj = quadcolor->crtc[CGA_CRTC_VTOTAL_ADJUST];
+ if (!quadcolor->vadj) {
+ quadcolor->cgadispon = 1;
+ quadcolor->memaddr = quadcolor->memaddr_backup = (quadcolor->crtc[CGA_CRTC_START_ADDR_LOW] | (quadcolor->crtc[CGA_CRTC_START_ADDR_HIGH] << 8)) & DEVICE_VRAM_MASK;
+ }
+
+ switch (quadcolor->crtc[CGA_CRTC_CURSOR_START] & 0x60) {
+ case 0x20:
+ quadcolor->cursoron = 0;
+ break;
+ case 0x60:
+ quadcolor->cursoron = quadcolor->cgablink & 0x10;
+ break;
+ default:
+ quadcolor->cursoron = quadcolor->cgablink & 0x08;
+ break;
+ }
+ }
+
+ if (quadcolor->vc == quadcolor->crtc[CGA_CRTC_VSYNC]) {
+ quadcolor->cgadispon = 0;
+ quadcolor->displine = 0;
+ quadcolor->vsynctime = 16;
+ if (quadcolor->crtc[CGA_CRTC_VSYNC]) {
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)
+ x = (quadcolor->crtc[CGA_CRTC_HDISP] << 3) + 16;
+ else
+ x = (quadcolor->crtc[CGA_CRTC_HDISP] << 4) + 16;
+ quadcolor->lastline++;
+
+ xs_temp = x;
+ ys_temp = quadcolor->lastline - quadcolor->firstline;
+ if (quadcolor->double_type > DOUBLE_NONE)
+ ys_temp <<= 1;
+
+ if ((xs_temp > 0) && (ys_temp > 0)) {
+ if (xs_temp < 64)
+ xs_temp = 656;
+ if (ys_temp < 32)
+ ys_temp = 200;
+ if (!enable_overscan)
+ xs_temp -= 16;
+
+ if ((quadcolor->cgamode & CGA_MODE_FLAG_VIDEO_ENABLE) && ((xs_temp != xsize) ||
+ (ys_temp != ysize) || video_force_resize_get())) {
+ xsize = xs_temp;
+ ysize = ys_temp;
+ if (quadcolor->double_type > DOUBLE_NONE)
+ set_screen_size(xsize, ysize + (enable_overscan ? 16 : 0));
+ else
+ set_screen_size(xsize, ysize + (enable_overscan ? 8 : 0));
+
+ if (video_force_resize_get())
+ video_force_resize_set(0);
+ }
+
+ if (quadcolor->double_type > DOUBLE_NONE) {
+ if (enable_overscan)
+ quadcolor_blit_memtoscreen(quadcolor, 0, (quadcolor->firstline - 4) << 1,
+ xsize, ((quadcolor->lastline - quadcolor->firstline) << 1) + 16);
+ else
+ quadcolor_blit_memtoscreen(quadcolor, 8, quadcolor->firstline << 1,
+ xsize, (quadcolor->lastline - quadcolor->firstline) << 1);
+ } else {
+ if (enable_overscan)
+ video_blit_memtoscreen(0, quadcolor->firstline - 4,
+ xsize, (quadcolor->lastline - quadcolor->firstline) + 8);
+ else
+ video_blit_memtoscreen(8, quadcolor->firstline,
+ xsize, quadcolor->lastline - quadcolor->firstline);
+ }
+ }
+
+ frames++;
+
+ video_res_x = xsize;
+ video_res_y = ysize;
+ if (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES) {
+ video_res_x /= 8;
+ video_res_y /= quadcolor->crtc[CGA_CRTC_MAX_SCANLINE_ADDR] + 1;
+ video_bpp = 0;
+ } else if (!(quadcolor->cgamode & CGA_MODE_FLAG_GRAPHICS)) {
+ video_res_x /= 16;
+ video_res_y /= quadcolor->crtc[CGA_CRTC_MAX_SCANLINE_ADDR] + 1;
+ video_bpp = 0;
+ } else if (!(quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES_GRAPHICS)) {
+ video_res_x /= 2;
+ video_bpp = 2;
+ } else
+ video_bpp = 1;
+ }
+ quadcolor->firstline = 1000;
+ quadcolor->lastline = 0;
+ quadcolor->cgablink++;
+ quadcolor->oddeven ^= 1;
+ }
+ } else {
+ quadcolor->scanline++;
+ quadcolor->scanline &= 31;
+ quadcolor->memaddr = quadcolor->memaddr_backup;
+ }
+ if (quadcolor->cgadispon)
+ quadcolor->cgastat &= ~1;
+ if (quadcolor->scanline == (quadcolor->crtc[CGA_CRTC_CURSOR_START] & 31) || ((quadcolor->crtc[CGA_CRTC_INTERLACE] & 3) == 3 &&
+ quadcolor->scanline == ((quadcolor->crtc[CGA_CRTC_CURSOR_START] & 31) >> 1)))
+ quadcolor->cursorvisible = 1;
+ if (quadcolor->cgadispon && (quadcolor->cgamode & CGA_MODE_FLAG_HIGHRES)) {
+ for (x = 0; x < (quadcolor->crtc[CGA_CRTC_HDISP] << 1); x++)
+ quadcolor->charbuffer[x] = quadcolor->vram[(quadcolor->page_offset ^ ((quadcolor->memaddr << 1) + x)) & DEVICE_VRAM_MASK];
+ }
+ }
+}
+
+void
+quadcolor_init(quadcolor_t *quadcolor)
+{
+ timer_add(&quadcolor->timer, quadcolor_poll, quadcolor, 1);
+ quadcolor->composite = 0;
+}
+
+void *
+quadcolor_standalone_init(UNUSED(const device_t *info))
+{
+ int display_type;
+ quadcolor_t *quadcolor = calloc(1, sizeof(quadcolor_t));
+
+ video_inform(VIDEO_FLAG_TYPE_CGA, &timing_quadcolor);
+
+ display_type = device_get_config_int("display_type");
+ quadcolor->composite = (display_type != CGA_RGB);
+ quadcolor->revision = device_get_config_int("composite_type");
+ quadcolor->has_2nd_charset = device_get_config_int("has_2nd_charset");
+ quadcolor->has_quadcolor_2 = device_get_config_int("has_quadcolor_2");
+
+ quadcolor->vram = malloc(DEVICE_VRAM);
+ quadcolor->vram_2 = malloc(0x10000);
+
+ cga_comp_init(quadcolor->revision);
+ timer_add(&quadcolor->timer, quadcolor_poll, quadcolor, 1);
+ mem_mapping_add(&quadcolor->mapping, 0xb8000, 0x08000, quadcolor_read, NULL, NULL, quadcolor_write, NULL, NULL, NULL /*quadcolor->vram*/, MEM_MAPPING_EXTERNAL, quadcolor);
+ /* add mapping for vram_2 at 0xd0000, mirrored at 0xe0000 */
+ if (quadcolor->has_quadcolor_2)
+ mem_mapping_add(&quadcolor->mapping_2, 0xd0000, 0x20000, quadcolor_2_read, NULL, NULL, quadcolor_2_write, NULL, NULL, NULL, MEM_MAPPING_EXTERNAL,
+ quadcolor);
+
+ io_sethandler(0x03d0, 0x0010, quadcolor_in, NULL, NULL, quadcolor_out, NULL, NULL, quadcolor);
+
+ overscan_x = overscan_y = 16;
+
+ quadcolor->rgb_type = device_get_config_int("rgb_type");
+ cga_palette = (quadcolor->rgb_type << 1);
+ cgapal_rebuild();
+ update_cga16_color(quadcolor->cgamode);
+
+ quadcolor->double_type = device_get_config_int("double_type");
+
+ for (uint16_t i = 0; i < 256; i++) {
+ for (uint16_t j = 0; j < 256; j++) {
+ interp_lut[0][i][j] = quadcolor_interpolate_srgb(i, j, 0.5);
+ interp_lut[1][i][j] = quadcolor_interpolate_linear(i, j, 0.5);
+ }
+ }
+
+ switch(device_get_config_int("font")) {
+ case 0:
+ loadfont(FONT_IBM_MDA_437_PATH, 0);
+ break;
+ case 1:
+ loadfont(FONT_IBM_MDA_437_NORDIC_PATH, 0);
+ break;
+ case 4:
+ loadfont(FONT_TULIP_DGA_PATH, 0);
+ break;
+ }
+
+ return quadcolor;
+}
+
+void
+quadcolor_close(void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+
+ free(quadcolor->vram);
+ free(quadcolor->vram_2);
+ free(quadcolor);
+}
+
+void
+quadcolor_speed_changed(void *priv)
+{
+ quadcolor_t *quadcolor = (quadcolor_t *) priv;
+
+ quadcolor_recalctimings(quadcolor);
+}
+
+// clang-format off
+const device_config_t quadcolor_config[] = {
+ {
+ .name = "display_type",
+ .description = "Display type",
+ .type = CONFIG_SELECTION,
+ .default_string = NULL,
+ .default_int = CGA_RGB,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = {
+ { .description = "RGB", .value = CGA_RGB },
+ { .description = "Composite", .value = CGA_COMPOSITE },
+ { .description = "" }
+ },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "composite_type",
+ .description = "Composite type",
+ .type = CONFIG_SELECTION,
+ .default_string = NULL,
+ .default_int = COMPOSITE_OLD,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = {
+ { .description = "Old", .value = COMPOSITE_OLD },
+ { .description = "New", .value = COMPOSITE_NEW },
+ { .description = "" }
+ },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "rgb_type",
+ .description = "RGB type",
+ .type = CONFIG_SELECTION,
+ .default_string = NULL,
+ .default_int = 5,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = {
+ { .description = "Color (generic)", .value = 0 },
+ { .description = "Green Monochrome", .value = 1 },
+ { .description = "Amber Monochrome", .value = 2 },
+ { .description = "Gray Monochrome", .value = 3 },
+ { .description = "Color (no brown)", .value = 4 },
+ { .description = "Color (IBM 5153)", .value = 5 },
+ { .description = "" }
+ },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "double_type",
+ .description = "Line doubling type",
+ .type = CONFIG_SELECTION,
+ .default_string = NULL,
+ .default_int = DOUBLE_NONE,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = {
+ { .description = "None", .value = DOUBLE_NONE },
+ { .description = "Simple doubling", .value = DOUBLE_SIMPLE },
+ { .description = "sRGB interpolation", .value = DOUBLE_INTERPOLATE_SRGB },
+ { .description = "Linear interpolation", .value = DOUBLE_INTERPOLATE_LINEAR },
+ { .description = "" }
+ },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "font",
+ .description = "Font",
+ .type = CONFIG_SELECTION,
+ .default_string = NULL,
+ .default_int = 0,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = {
+ { .description = "US (CP 437)", .value = 0 },
+ { .description = "IBM Nordic (CP 437-Nordic)", .value = 1 },
+ { .description = "Tulip DGA", .value = 4 },
+ { .description = "" }
+ },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "has_2nd_charset",
+ .description = "Has secondary 8x8 character set",
+ .type = CONFIG_BINARY,
+ .default_string = NULL,
+ .default_int = 0,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = { { 0 } },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "has_quadcolor_2",
+ .description = "Has Quadcolor II daughter board",
+ .type = CONFIG_BINARY,
+ .default_string = NULL,
+ .default_int = 1,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = { { 0 } },
+ .bios = { { 0 } }
+ },
+ {
+ .name = "contrast",
+ .description = "Alternate monochrome contrast",
+ .type = CONFIG_BINARY,
+ .default_string = NULL,
+ .default_int = 0,
+ .file_filter = NULL,
+ .spinner = { 0 },
+ .selection = { { 0 } },
+ .bios = { { 0 } }
+ },
+ { .name = "", .description = "", .type = CONFIG_END }
+};
+// clang-format on
+
+const device_t quadcolor_device = {
+ .name = "Quadram Quadcolor I / I+II",
+ .internal_name = "quadcolor",
+ .flags = DEVICE_ISA,
+ .local = 0,
+ .init = quadcolor_standalone_init,
+ .close = quadcolor_close,
+ .reset = NULL,
+ .available = NULL,
+ .speed_changed = quadcolor_speed_changed,
+ .force_redraw = NULL,
+ .config = quadcolor_config
+};
diff --git a/src/video/vid_table.c b/src/video/vid_table.c
index b97defe8c..2e42e5c2c 100644
--- a/src/video/vid_table.c
+++ b/src/video/vid_table.c
@@ -67,6 +67,7 @@ video_cards[] = {
{ .device = &ati18800_device, .flags = VIDEO_FLAG_TYPE_NONE },
{ .device = &ati18800_wonder_device, .flags = VIDEO_FLAG_TYPE_NONE },
{ .device = &cga_device, .flags = VIDEO_FLAG_TYPE_NONE },
+ { .device = &quadcolor_device, .flags = VIDEO_FLAG_TYPE_NONE },
{ .device = &sega_device, .flags = VIDEO_FLAG_TYPE_NONE },
{ .device = &jega_device, .flags = VIDEO_FLAG_TYPE_NONE },
{ .device = &gd5401_isa_device, .flags = VIDEO_FLAG_TYPE_NONE },