gfxmatrox: Introduce new programmer driver for Matrox Millenium cards

Supports a 64KiB parallel flash. ROM BAR configuration is tricky: The
register is only accessible when a special `biosen` bit is set. An OS
might not know this and is then unable to assign an address. We'll of-
fer to re-purpose the MMIO BAR address, but one has to be sure that no
graphics driver is running.

Datasheet used:
https://www.vintage3d.org/doc/matrox/2164spec.pdf

Change-Id: Ief6c05c8fbf752814d436ac3b47e87de0c977047
Signed-off-by: Nico Huber <nico.h@gmx.de>
Reviewed-on: https://review.sourcearcade.org/c/flashprog/+/532
Reviewed-by: Arthur Heymans <arthur@aheymans.xyz>
diff --git a/Makefile b/Makefile
index a9f2429..11eb07b 100644
--- a/Makefile
+++ b/Makefile
@@ -113,6 +113,7 @@
 DEPENDS_ON_RAW_MEM_ACCESS := \
 	CONFIG_ATAPROMISE \
 	CONFIG_DRKAISER \
+	CONFIG_GFXMATROX \
 	CONFIG_GFXNVIDIA \
 	CONFIG_INTERNAL \
 	CONFIG_IT8212 \
@@ -143,6 +144,7 @@
 	CONFIG_ATAPROMISE \
 	CONFIG_ATAVIA \
 	CONFIG_DRKAISER \
+	CONFIG_GFXMATROX \
 	CONFIG_GFXNVIDIA \
 	CONFIG_INTERNAL \
 	CONFIG_IT8212 \
@@ -440,6 +442,9 @@
 # Always enable AMD NICs for now.
 CONFIG_NICAMD ?= yes
 
+# Enable Matrox graphics cards.
+CONFIG_GFXMATROX ?= yes
+
 # Enable NVIDIA graphics cards. Note: write and erase do not work properly.
 CONFIG_GFXNVIDIA ?= yes
 
@@ -657,6 +662,11 @@
 PROGRAMMER_OBJS += nicamd.o
 endif
 
+ifeq ($(CONFIG_GFXMATROX), yes)
+FEATURE_FLAGS += -D'CONFIG_GFXMATROX=1'
+PROGRAMMER_OBJS += gfxmatrox.o
+endif
+
 ifeq ($(CONFIG_GFXNVIDIA), yes)
 FEATURE_FLAGS += -D'CONFIG_GFXNVIDIA=1'
 PROGRAMMER_OBJS += gfxnvidia.o
diff --git a/flashprog.8.tmpl b/flashprog.8.tmpl
index d771333..24d9aaf 100644
--- a/flashprog.8.tmpl
+++ b/flashprog.8.tmpl
@@ -310,6 +310,8 @@
 .sp
 .BR "* nicamd" " (for parallel flash ROMs on AMD PCnet network cards)
 .sp
+.BR "* gfxmatrox" " (for flash ROMs on Matrox graphics cards)"
+.sp
 .BR "* gfxnvidia" " (for flash ROMs on NVIDIA graphics cards)"
 .sp
 .BR "* drkaiser" " (for flash ROMs on Dr. Kaiser PC-Waechter PCI cards)"
@@ -763,9 +765,9 @@
 not here).
 .SS
 .BR "nic3com" , " nicrealtek" , " nicnatsemi" , " nicintel", " nicintel_eeprom"\
-, " nicintel_spi" , " nicamd" , " gfxnvidia" , " ogp_spi" , " drkaiser"\
-, " satasii" , " scsilsi", " satamv" , " atahpt", " atavia ", " atapromise "\
-and " it8212 " programmers
+, " nicintel_spi" , " nicamd" , " gfxmatrox" , " gfxnvidia" , " ogp_spi"\
+, " drkaiser", " satasii" , " scsilsi", " satamv" , " atahpt", " atavia "\
+, " atapromise " and " it8212 " programmers
 .IP
 These programmers have an option to specify the PCI address of the card
 your want to use, which must be specified if more than one card supported
@@ -823,6 +825,19 @@
 .sp
 .B "  flashprog \-p nicamd:max_decode_kb=128"
 .SS
+.BR "gfxmatrox " programmer
+.IP
+This programmer module supports parallel flash up to 64KiB, attached to Matrox graphics cards. The ROM BAR
+is used for reading
+.B and writing
+the flash. Because the whole BAR register is disabled by default, though, it's likely that we have to
+configure it ourselves. The ROM BAR decoding takes precedence, so we can borrow the MMIO BAR address,
+but have to make double sure that no graphics driver is running. Hence, this borrowing has to be explicitly
+enabled with the
+.BR "bar=no_gfx_driver_running " argument.
+.sp
+.B "  flashprog \-p gfxmatrox:bar=no_gfx_driver_running"
+.SS
 .BR "ft2232_spi " programmer
 .IP
 This module supports various programmers based on FTDI FT2232/FT4232H/FT4233H/FT232H chips including the DLP Design
@@ -1567,7 +1582,7 @@
 .B atahpt
 needs PCI configuration space access and raw I/O port access.
 .sp
-.BR gfxnvidia ", " drkaiser " and " it8212
+.BR gfxmatrox ", " gfxnvidia ", " drkaiser " and " it8212
 need PCI configuration space access and raw memory access.
 .sp
 .B rayer_spi
@@ -1596,8 +1611,8 @@
 needs no access permissions at all.
 .sp
 .BR internal ", " nicamd ", " nic3com ", " nicrealtek ", " nicnatsemi ", "
-.BR gfxnvidia ", " drkaiser ", " satamv ", " satasii ", " scsilsi ", "
-.BR atahpt ", " atavia " and " atapromise
+.BR gfxmatrox ", " gfxnvidia ", " drkaiser ", " satamv ", " satasii ", "
+.BR scsilsi ", " atahpt ", " atavia " and " atapromise
 have to be run as superuser/root, and need additional raw access permission.
 .sp
 .BR serprog ", " buspirate_spi ", " dediprog ", " usbblaster_spi ", " ft2232_spi ", " pickit2_spi ", " \
diff --git a/gfxmatrox.c b/gfxmatrox.c
new file mode 100644
index 0000000..647a908
--- /dev/null
+++ b/gfxmatrox.c
@@ -0,0 +1,97 @@
+/*
+ * This file is part of the flashprog project.
+ *
+ * Copyright (C) 2026 Nico Huber <nico.h@gmx.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "programmer.h"
+#include "platform/pci.h"
+#include "hwaccess_physmap.h"
+#include "programmer/physmap.h"
+
+#define PCI_VENDOR_ID_MATROX	0x102b
+
+static const struct dev_entry gfx_matrox[] = {
+	{PCI_VENDOR_ID_MATROX, 0x051b, OK, "Matrox Electronics Systems Ltd.", "MGA 2164W [Millennium II]" },
+
+	{0},
+};
+
+static const struct par_master par_master_gfxmatrox = {
+	.chip_readb	= mmio_chip_readb,
+	.chip_readw	= mmio_chip_readw,
+	.chip_readl	= mmio_chip_readl,
+	.chip_readn	= mmio_chip_readn,
+	.chip_writeb	= mmio_chip_writeb,
+	.chip_writew	= mmio_chip_writew,
+	.chip_writel	= mmio_chip_writel,
+	.chip_writen	= fallback_chip_writen,
+	.map_flash	= physmap,
+	.unmap_flash	= physunmap,
+};
+
+static int gfxmatrox_init(struct flashprog_programmer *const prog)
+{
+	bool borrow_mmio_bar = false;
+	char *const bar_param = extract_programmer_param("bar");
+	if (bar_param) {
+		if (strcmp(bar_param, "no_gfx_driver_running") != 0) {
+			msg_perr("Error: Unknown argument for `bar' parameter: \"%s\".\n", bar_param);
+			free(bar_param);
+			return 1;
+		}
+		borrow_mmio_bar = true;
+	}
+	free(bar_param);
+
+	struct pci_dev *const matrox = pcidev_init(gfx_matrox, PCI_BASE_ADDRESS_1);
+	if (!matrox)
+		return 1;
+
+	const uint16_t command = pci_read_word(matrox, PCI_COMMAND);
+	if (!(command & PCI_COMMAND_MEMORY)) {
+		msg_perr("Error: PCI memory access is disabled.\n");
+		return 1;
+	}
+
+	/* Enable flash + writes. */
+	const uint32_t option = pci_read_long(matrox, 0x40);
+	rpci_write_long(matrox, 0x40, option | (1 << 30) | (1 << 20));
+
+	uint32_t rom_base = pci_read_long(matrox, PCI_ROM_ADDRESS);
+	if (!rom_base) {
+		if (!borrow_mmio_bar) {
+			msg_perr("Error: PCI ROM base is not configured.\n");
+			msg_perr("We can try to work around this if no graphics driver is running. When no driver\n");
+			msg_perr("is running, confirm this by supplying `-p gfxmatrox:bar=no_gfx_driver_running'.\n");
+			return 1;
+		}
+		/* If the ROM BAR isn't enabled, borrow the MMIO BAR. */
+		rom_base = pcidev_readbar(matrox, PCI_BASE_ADDRESS_1);
+	}
+	rpci_write_long(matrox, PCI_ROM_ADDRESS, rom_base | 1);
+
+	return register_par_master(&par_master_gfxmatrox, BUS_PARALLEL, rom_base & ~1u, 64*KiB, NULL);
+}
+
+const struct programmer_entry programmer_gfxmatrox = {
+	.name			= "gfxmatrox",
+	.type			= PCI,
+	.devs.dev		= gfx_matrox,
+	.init			= gfxmatrox_init,
+};
diff --git a/include/programmer.h b/include/programmer.h
index 1aa57b4..88a7047 100644
--- a/include/programmer.h
+++ b/include/programmer.h
@@ -72,6 +72,7 @@
 extern const struct programmer_entry programmer_dummy;
 extern const struct programmer_entry programmer_ft2232_spi;
 extern const struct programmer_entry programmer_ft4222_spi;
+extern const struct programmer_entry programmer_gfxmatrox;
 extern const struct programmer_entry programmer_gfxnvidia;
 extern const struct programmer_entry programmer_internal;
 extern const struct programmer_entry programmer_it8212;
diff --git a/meson.build b/meson.build
index b94d951..9e996e9 100644
--- a/meson.build
+++ b/meson.build
@@ -249,6 +249,14 @@
     'srcs'    : files('ft4222_spi.c' ),
     'flags'   : [ '-DCONFIG_FT4222_SPI=1' ],
   },
+  'gfxmatrox' : {
+    'systems' : systems_hwaccess,
+    'cpu_families' : [ cpus_raw_mem ],
+    'deps'    : [ libpci ],
+    'groups'  : [ group_pci, group_internal ],
+    'srcs'    : files('gfxmatrox.c', 'pcidev.c'),
+    'flags'   : [ '-DCONFIG_GFXMATROX=1' ],
+  },
   'gfxnvidia' : {
     'systems' : systems_hwaccess,
     'cpu_families' : [ cpus_raw_mem ],
diff --git a/meson_options.txt b/meson_options.txt
index 85a8752..216ad92 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -11,7 +11,7 @@
         'group_ftdi', 'group_i2c', 'group_jlink', 'group_pci', 'group_serial', 'group_usb',
         'atahpt', 'atapromise', 'atavia', 'buspirate_spi', 'ch341a_spi', 'ch347_spi', 'dediprog', 'developerbox_spi',
         'digilent_spi', 'dirtyjtag_spi', 'drkaiser', 'dummy', 'ft2232_spi', 'ft4222_spi',
-        'gfxnvidia', 'internal', 'it8212',
+        'gfxmatrox', 'gfxnvidia', 'internal', 'it8212',
         'jlink_spi', 'linux_gpio_spi', 'linux_mtd', 'linux_spi', 'mediatek_i2c_spi', 'mstarddc_spi',
         'nic3com', 'nicamd', 'nicintel', 'nicintel_eeprom', 'nicintel_spi', 'nicnatsemi', 'nicrealtek',
         'ogp_spi', 'parade_lspcon',
diff --git a/programmer_table.c b/programmer_table.c
index b2ab826..192851e 100644
--- a/programmer_table.c
+++ b/programmer_table.c
@@ -40,6 +40,10 @@
     &programmer_nicnatsemi,
 #endif
 
+#if CONFIG_GFXMATROX == 1
+    &programmer_gfxmatrox,
+#endif
+
 #if CONFIG_GFXNVIDIA == 1
     &programmer_gfxnvidia,
 #endif