Implement registers used for loading and reading microcode for the CCE engine. Loading the microcode is the first step for any driver implementing CCE. Reading, while not used by drivers is very helpful for any reverse engineering and testing work. The microcode is currently stored but not used. This lays the groundwork for future RE work on the microcode.
There's some quirky behavior around microcode reads that isn't documented elsewhere. There appear to be two internal pointers, one for reading and one for writing that can get out of sync. Comments in the code expand on this. Tested and validated against a Rage 128 Pro Ultra (PCI 1002:5446). Signed-off-by: Chad Jablonski <[email protected]> --- hw/display/ati.c | 47 ++++++++++++++++++++++++++++++++++++++++++++ hw/display/ati_cce.h | 26 ++++++++++++++++++++++++ hw/display/ati_int.h | 2 ++ 3 files changed, 75 insertions(+) create mode 100644 hw/display/ati_cce.h diff --git a/hw/display/ati.c b/hw/display/ati.c index f9be5b302c..bac0ceaa79 100644 --- a/hw/display/ati.c +++ b/hw/display/ati.c @@ -535,6 +535,31 @@ static uint64_t ati_mm_read(void *opaque, hwaddr addr, unsigned int size) qemu_log_mask(LOG_GUEST_ERROR, "Read from write-only register 0x%x\n", (unsigned)addr); break; + case PM4_MICROCODE_ADDR: + val = s->cce.microcode.addr; + break; + case PM4_MICROCODE_RADDR: + val = 0; + break; + case PM4_MICROCODE_DATAH: + val = (s->cce.microcode.microcode[s->cce.microcode.raddr] >> 32) & + 0xffffffff; + break; + case PM4_MICROCODE_DATAL: + val = s->cce.microcode.microcode[s->cce.microcode.raddr] & 0xffffffff; + s->cce.microcode.addr += 1; + /* + * The write address (addr) is always copied into the + * read address (raddr) after a DATAL read. This leads + * to surprising behavior when the PM4_MICROCODE_ADDR + * instead of the PM4_MICROCODE_RADDR register is set to + * a value just before a read. The first read after this + * will reflect the previous raddr before incrementing and + * re-syncing with addr. This is expected and observed on + * the hardware. + */ + s->cce.microcode.raddr = s->cce.microcode.addr; + break; default: break; } @@ -1052,6 +1077,28 @@ void ati_reg_write(ATIVGAState *s, hwaddr addr, ati_flush_host_data(s); ati_host_data_reset(&s->host_data); break; + case PM4_MICROCODE_ADDR: + s->cce.microcode.addr = data; + break; + case PM4_MICROCODE_RADDR: + s->cce.microcode.raddr = data; + s->cce.microcode.addr = data; + break; + case PM4_MICROCODE_DATAH: { + uint64_t curr = s->cce.microcode.microcode[s->cce.microcode.addr]; + uint64_t low = curr & 0xffffffff; + uint64_t high = (data & 0x1f) << 32; + s->cce.microcode.microcode[s->cce.microcode.addr] = high | low; + break; + } + case PM4_MICROCODE_DATAL: { + uint64_t curr = s->cce.microcode.microcode[s->cce.microcode.addr]; + uint64_t low = data & 0xffffffff; + uint64_t high = curr & (0xffffffffull << 32); + s->cce.microcode.microcode[s->cce.microcode.addr] = high | low; + s->cce.microcode.addr += 1; + break; + } default: break; } diff --git a/hw/display/ati_cce.h b/hw/display/ati_cce.h new file mode 100644 index 0000000000..f2ef1345de --- /dev/null +++ b/hw/display/ati_cce.h @@ -0,0 +1,26 @@ +/* + * QEMU ATI SVGA emulation + * CCE engine functions + * + * Copyright (c) 2025 Chad Jablonski + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef ATI_CCE_H +#define ATI_CCE_H + +#include "qemu/osdep.h" +#include "qemu/log.h" + +typedef struct ATIPM4MicrocodeState { + uint8_t addr; + uint8_t raddr; + uint64_t microcode[256]; +} ATIPM4MicrocodeState; + +typedef struct ATICCEState { + ATIPM4MicrocodeState microcode; +} ATICCEState; + +#endif /* ATI_CCE_H */ diff --git a/hw/display/ati_int.h b/hw/display/ati_int.h index 3ee891c888..416275ae0d 100644 --- a/hw/display/ati_int.h +++ b/hw/display/ati_int.h @@ -16,6 +16,7 @@ #include "vga_int.h" #include "qom/object.h" #include "qemu/units.h" +#include "ati_cce.h" /*#define DEBUG_ATI*/ @@ -123,6 +124,7 @@ struct ATIVGAState { MemoryRegion mm; ATIVGARegs regs; ATIHostDataState host_data; + ATICCEState cce; }; const char *ati_reg_name(int num); -- 2.51.2
