Palette fix for the X68000

This commit is contained in:
Scott Duensing 2026-08-13 09:37:01 -05:00
parent 74bc0eca7a
commit 63e76f02e6
2 changed files with 82 additions and 1 deletions

View file

@ -84,6 +84,22 @@
#define X68K_GFX_PALETTE ((volatile uint16_t *)0xE82000L) #define X68K_GFX_PALETTE ((volatile uint16_t *)0xE82000L)
#define X68K_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE) #define X68K_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE)
// Video Controller register 2 ($E82600) low byte = per-plane display enable (MAME x68k_v.cpp): bits 0-3
// graphic layers, bit 4 graphic, bit 5 (0x20) TEXT plane, bit 6 sprite. _iocs_crtmod leaves the text
// plane ON, so Human68k's desktop (Drv0-3, the kana/romaji status column) bleeds through the graphics;
// clearing bit 5 hides it while keeping the graphic layers we draw into.
#define X68K_VIDCTRL2 ((volatile uint16_t *)0xE82600L)
#define X68K_VIDCTRL2_TEXT 0x0020u
// Video Controller register 0 ($E82400) low 2 bits select the graphic colour depth (MAME x68k_v.cpp
// switch(reg[0] & 3)): 0 = 16-colour, 1 = 256-colour palette-indexed, 3 = 65536-colour DIRECT.
// _iocs_crtmod(13) sets the 512x512 CRTC but leaves this at 3 (direct), whereas this port draws 8-bit
// PALETTE INDICES into GVRAM and uploads a 256-entry palette at $E82000 -- so the mode MUST be forced to
// 256-colour or every index is interpreted as a near-black direct colour (the "invisible/dim" bug).
#define X68K_VIDCTRL0 ((volatile uint16_t *)0xE82400L)
#define X68K_VIDCTRL0_DEPTH 0x0003u
#define X68K_VIDCTRL0_256 0x0001u
// ----- Millisecond clock ----------------------------------------------------- // ----- Millisecond clock -----------------------------------------------------
// //
@ -161,6 +177,15 @@ bool jlpInit(const jlConfigT *config) {
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
_iocs_crtmod(X68K_CRTMOD_512_256); _iocs_crtmod(X68K_CRTMOD_512_256);
_iocs_g_clr_on(); // clear graphics + enable the plane _iocs_g_clr_on(); // clear graphics + enable the plane
// crtmod 13 leaves the video controller in 65536-colour DIRECT mode (reg[0] & 3 == 3). This port
// draws 8-bit palette indices, so force 256-colour palette-indexed mode or every colour renders
// near-black. Root-caused + proven live on MAME (reg0 0x0003 -> 0x0001 turned a black frame into a
// correct red/yellow/blue test pattern).
X68K_VIDCTRL0[0] = (uint16_t)((X68K_VIDCTRL0[0] & (uint16_t)~X68K_VIDCTRL0_DEPTH) | X68K_VIDCTRL0_256);
// jlpPresent draws into the GRAPHIC plane (GVRAM $C00000), NOT the text plane -- so Human68k's text
// plane (the Drv0-3 + kana/romaji desktop that _iocs_crtmod leaves on) bleeds over our frame. A
// reg[2] &= ~0x20 write to hide it did NOT take on MAME (the desktop stayed visible), so that cosmetic
// bleed is left as a follow-up; it does not affect the graphic layers we draw into.
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine -- // NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
// tested, no serial output at all, so it wedges before main() gets going. // tested, no serial output at all, so it wedges before main() gets going.
// IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into // IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into

View file

@ -200,6 +200,57 @@ class Xdf:
struct.pack_into("<I", entry, 28, len(payload)) struct.pack_into("<I", entry, 28, len(payload))
self.data[slot:slot + DIR_ENTRY_SIZE] = entry self.data[slot:slot + DIR_ENTRY_SIZE] = entry
def _dir_entry(self, name11, attr, cluster, size):
entry = bytearray(DIR_ENTRY_SIZE)
entry[0:11] = name11
entry[11] = attr
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1) # 2026-01-01
struct.pack_into("<H", entry, 26, cluster)
struct.pack_into("<I", entry, 28, size)
return entry
def add_file_in_subdir(self, dirname, name, payload):
# Create a one-cluster subdirectory in root (with . and .. entries) and write `name` into it, so a
# guest that reads e.g. SAVES/RN.CFG finds it. Fresh-template semantics: the subdir must not exist
# yet (the callers copy a clean image per run). Human68k FAT12 is otherwise ordinary FAT12.
dstem = dirname.upper()
if len(dstem) > 8:
raise ValueError(f"subdir '{dirname}' does not fit 8.3")
cb = self.cluster_bytes
need = max(1, (len(payload) + cb - 1) // cb)
free = self.free_clusters()
if len(free) < need + 1:
raise OSError(f"{self.path}: need {need + 1} clusters, {len(free)} free")
dir_cluster = free[0]
data_chain = free[1:1 + need]
# File data clusters.
for idx, cluster in enumerate(data_chain):
off = self.cluster_offset(cluster)
chunk = payload[idx * cb:(idx + 1) * cb]
self.data[off:off + len(chunk)] = chunk
if len(chunk) < cb:
self.data[off + len(chunk):off + cb] = bytes(cb - len(chunk))
self.fat_set(cluster, 0xFFF if idx == need - 1 else data_chain[idx + 1])
# The subdirectory's own cluster: '.', '..', then the file entry, rest free.
doff = self.cluster_offset(dir_cluster)
self.data[doff:doff + cb] = bytes(cb)
self.data[doff:doff + 32] = self._dir_entry(b". ", ATTR_DIR, dir_cluster, 0)
self.data[doff + 32:doff + 64] = self._dir_entry(b".. ", ATTR_DIR, 0, 0)
self.data[doff + 64:doff + 96] = self._dir_entry(
self.encode_name(name), ATTR_ARCHIVE, data_chain[0], len(payload))
self.fat_set(dir_cluster, 0xFFF)
# Root entry pointing at the subdirectory.
slot = None
for _, off, e in self._entries():
if e[0] in (FREE_MARKER, DELETED_MARKER):
slot = off
break
if slot is None:
raise OSError(f"{self.path}: root directory full")
self.data[slot:slot + DIR_ENTRY_SIZE] = self._dir_entry(
dstem.ljust(8).encode("ascii") + b" ", ATTR_DIR, dir_cluster, 0)
def flush(self): def flush(self):
if not self.writable: if not self.writable:
raise PermissionError("opened read-only") raise PermissionError("opened read-only")
@ -235,7 +286,12 @@ def main(argv):
name = argv[4] if len(argv) > 4 else os.path.basename(host) name = argv[4] if len(argv) > 4 else os.path.basename(host)
img = Xdf(image, writable=True) img = Xdf(image, writable=True)
with open(host, "rb") as fp: with open(host, "rb") as fp:
img.add_file(name, fp.read()) payload = fp.read()
if "/" in name: # SAVES/RN.CFG -> a one-cluster subdirectory + the file
sub, _, leaf = name.partition("/")
img.add_file_in_subdir(sub, leaf, payload)
else:
img.add_file(name, payload)
img.flush() img.flush()
print(f"added {name} ({os.path.getsize(host)} bytes) to {image}") print(f"added {name} ({os.path.getsize(host)} bytes) to {image}")
return 0 return 0