diff --git a/cerf/cpu/emulated_memory.cpp b/cerf/cpu/emulated_memory.cpp index 040b26d7..3b03c474 100644 --- a/cerf/cpu/emulated_memory.cpp +++ b/cerf/cpu/emulated_memory.cpp @@ -22,11 +22,11 @@ void EmulatedMemory::AddRegion(uint32_t base, uint32_t size, std::lock_guard lk(add_mutex_); const uint32_t span = decode_span ? decode_span : size; - if (size == 0 || span < size || span % size != 0 || + if (size == 0 || span < size || uint64_t(base) + span > (uint64_t{1} << 32) || + span % size != 0 || (span != size && (size & (size - 1u)) != 0)) { - LOG(Caution, "EmulatedMemory::AddRegion bad decode span: base=0x%08X " - "size=0x%X span=0x%X (span must be a multiple of a " - "power-of-two size)\n", base, size, span); + LOG(Caution, "EmulatedMemory::AddRegion invalid region: base=0x%08X " + "size=0x%X span=0x%X\n", base, size, span); CerfFatalExit(CERF_FATAL_RUNTIME_ERROR); } const uint32_t wrap_mask = (span == size) ? 0xFFFFFFFFu : (size - 1u); @@ -38,10 +38,12 @@ void EmulatedMemory::AddRegion(uint32_t base, uint32_t size, for (size_t i = 0; i < n; ++i) { const Region& r = regions_[i]; - if (base < r.base + r.span && r.base < base + span) { + if (uint64_t(base) < uint64_t(r.base) + r.span && + uint64_t(r.base) < uint64_t(base) + span) { LOG(Caution, "EmulatedMemory::AddRegion overlap: new " - "[0x%08X..0x%08X) vs existing [0x%08X..0x%08X)\n", - base, base + span, r.base, r.base + r.span); + "[0x%08X..0x%llX) vs existing [0x%08X..0x%llX)\n", + base, static_cast(base) + span, + r.base, static_cast(r.base) + r.span); CerfFatalExit(CERF_FATAL_RUNTIME_ERROR); } } @@ -70,7 +72,7 @@ EmulatedMemory::Region* EmulatedMemory::FindRegion(uint32_t vaddr) { const size_t n = count_.load(std::memory_order_acquire); for (size_t i = 0; i < n; ++i) { Region& r = regions_[i]; - if (vaddr >= r.base && vaddr < r.base + r.span) { + if (vaddr >= r.base && vaddr - r.base < r.span) { return &r; } } @@ -139,6 +141,20 @@ uint8_t* EmulatedMemory::TryTranslateWrite(uint32_t paddr) { return EnsureBacked(r) + ((paddr - r->base) & r->wrap_mask); } +uint8_t* EmulatedMemory::TryTranslateRange(uint64_t paddr, uint64_t size, bool write) { + if (size == 0 || paddr > UINT32_MAX || size > (uint64_t{1} << 32) - paddr) + return nullptr; + Region* r = FindRegion(static_cast(paddr)); + if (!r || (write && (r->page_protect == PAGE_READONLY || + r->page_protect == PAGE_EXECUTE_READ))) + return nullptr; + const uint64_t offset = paddr - r->base; + const uint64_t backed_offset = offset & r->wrap_mask; + if (size > r->span - offset || size > r->size - backed_offset) + return nullptr; + return EnsureBacked(r) + static_cast(backed_offset); +} + bool EmulatedMemory::IsSlotRangeUniform(uint32_t span_bytes, uint32_t pa) { if (span_bytes <= 0x1000u) return true; const uint32_t base = pa & ~(span_bytes - 1u); diff --git a/cerf/cpu/emulated_memory.h b/cerf/cpu/emulated_memory.h index 91bfac24..b6bb1fdc 100644 --- a/cerf/cpu/emulated_memory.h +++ b/cerf/cpu/emulated_memory.h @@ -36,6 +36,8 @@ class EmulatedMemory : public Service { PAGE_EXECUTE_READ regions so writes dispatch as MMIO. */ uint8_t* TryTranslateWrite(uint32_t paddr); + uint8_t* TryTranslateRange(uint64_t paddr, uint64_t size, bool write = false); + bool IsSlotRangeUniform(uint32_t span_bytes, uint32_t pa); uint8_t ReadByte(uint32_t vaddr); diff --git a/cerf/socs/imx51/imx51_gpu3d.cpp b/cerf/socs/imx51/imx51_gpu3d.cpp index 7722621b..aef6b380 100644 --- a/cerf/socs/imx51/imx51_gpu3d.cpp +++ b/cerf/socs/imx51/imx51_gpu3d.cpp @@ -3,10 +3,14 @@ #include "../../core/cerf_emulator.h" #include "../../boards/board_context.h" #include "../../peripherals/peripheral_dispatcher.h" -#include "../../cpu/emulated_memory.h" #include "../../state/state_stream.h" -#include "imx51_pixel_pack.h" +#include "imx51_gpu3d_blit.h" +#include "imx51_gpu3d_draw.h" +#include "imx51_gpu3d_raster.h" +#include "imx51_gpu3d_memory.h" #include "imx51_gpu3d_regs.h" +#include "imx51_gpu3d_packet.h" +#include "imx51_gpu3d_context.h" #include #include @@ -25,6 +29,7 @@ class Imx51Gpu3d : public Peripheral { return bd && bd->GetSoc() == SocFamily::iMX51; } void OnReady() override { + emu_.Get(); emu_.Get().Register(this); } @@ -50,6 +55,10 @@ class Imx51Gpu3d : public Peripheral { HaltUnsupportedAccess("ReadWord", a, 0); } void WriteWord(uint32_t a, uint32_t v) override { + const uint32_t idx = (a - kBase) >> 2; + if ((idx >= kIdxScratchReg0 && idx <= kIdxScratchReg7) || idx == kIdxScratchAddr || idx == kIdxScratchUmsk) { + WriteRegister(idx, v); return; + } switch ((a - kBase) >> 2) { case kIdxPmOverride1: pm_override1_ = v; return; case kIdxPmOverride2: pm_override2_ = v; return; @@ -60,17 +69,19 @@ class Imx51Gpu3d : public Peripheral { case kIdxRbWptrBase: return; case kIdxRbWptrDelay: return; case kIdxMhArbiterConfig: return; - case kIdxSqVsProgram: return; - case kIdxSqPsProgram: return; - case kIdxMhMmuConfig: return; + case kIdxSqVsProgram: + case kIdxSqPsProgram: WriteRegister(idx, v); return; + /* NXP linux-imx a1638da9, gsl_mmu.c:506-526; Mesa e97ad748 a2xx.xml:1042, BEH_NEVR. */ + case kIdxMhMmuConfig: + reg_file_[kIdxMhMmuConfig] = v; + if ((v & 1u) && v != 1u) HaltUnsupportedAccess("GPU MMU configuration", a, v); + return; case kIdxMhInterruptMask: return; case kIdxMhMmuMpuBase: return; case kIdxMhMmuMpuEnd: return; case kIdxRbCntl: rb_cntl_ = v; return; - /* CP/render config, ring-scan-inert. */ - case kIdxRbEdramInfo: - case kIdxScratchAddr: - case kIdxScratchUmsk: + /* NXP a1638da9 gsl_yamato.c:36-58, kgsl_yamato_gmeminit. */ + case kIdxRbEdramInfo: reg_file_[idx] = v; return; case kIdxCpIntAck: case kIdxCpDebug: case kIdxMeCntl: @@ -98,6 +109,9 @@ class Imx51Gpu3d : public Peripheral { w.Write(wptr_); w.Write(static_cast(reg_file_.size())); for (const auto& [idx, val] : reg_file_) { w.Write(idx); w.Write(val); } + emu_.Get().SaveState(w); + emu_.Get().SaveState(w); + emu_.Get().SaveState(w); } void RestoreState(StateReader& r) override { r.Read(pm_override1_); @@ -111,31 +125,100 @@ class Imx51Gpu3d : public Peripheral { r.Read(n); reg_file_.clear(); for (uint32_t k = 0; k < n; ++k) { uint32_t idx = 0, val = 0; r.Read(idx); r.Read(val); reg_file_[idx] = val; } + emu_.Get().RestoreState(r); + emu_.Get().RestoreState(r); + emu_.Get().RestoreState(r); } private: - uint32_t ReadPa32(uint32_t pa) { - const uint8_t* hp = emu_.Get().TryTranslate(pa); - if (!hp) - HaltUnsupportedAccess("CP ring/IB read unbacked", pa, 0); - return *reinterpret_cast(hp); + uint32_t MmuConfig() const { + const auto config = reg_file_.find(kIdxMhMmuConfig); + return config == reg_file_.end() ? 0u : config->second; + } + uint8_t* ReadSpan(uint64_t pa, uint64_t size) { + return emu_.Get().ReadSpan(pa, size, MmuConfig()); + } + uint8_t* WriteSpan(uint64_t pa, uint64_t size) { + return emu_.Get().WriteSpan(pa, size, MmuConfig()); + } + uint32_t ReadPa32(uint64_t pa) { + return emu_.Get().ReadPa32(pa, MmuConfig()); + } + void WritePa32(uint64_t pa, uint32_t value) { + emu_.Get().WritePa32(pa, value, MmuConfig()); + } + + Imx51Gpu3dPacket DecodePacket(uint64_t address, uint32_t available, + Imx51Gpu3dPacketSource source) { + if (address > UINT32_MAX || (address & 3u) != 0 || available == 0) + HaltUnsupportedAccess("PM4 malformed header address/extent", static_cast(address), available); + const uint32_t header = ReadPa32(address); + Imx51Gpu3dPacket packet; + if (const char* error = Imx51Gpu3dPacket::Decode(header, address, available, source, packet)) + HaltUnsupportedAccess(error, static_cast(address), header); + return packet; } - /* TYPE0 register write (kgsl_pm4types.h:157): cnt data dwords -> consecutive regs - regindx..regindx+cnt-1, or all to regindx when bit15 (same-register) is set. - Held in the register file so the draw-context REG_TO_MEM save reads them back. */ - void StoreType0(uint32_t hdr, uint32_t pa, uint32_t cnt) { + uint32_t ReadOperand(const Imx51Gpu3dPacket& packet, uint32_t index) { + uint64_t address = 0; + if (!packet.OperandAddress(index, address)) + HaltUnsupportedAccess("PM4 malformed operand index", static_cast(packet.address), index); + return ReadPa32(address); + } + + /* NXP linux-imx a1638da9, yamato_offset.h:450-465; gsl_ringbuffer.c:1038-1054. */ + void WriteRegister(uint32_t idx, uint32_t value) { + switch (idx) { + /* NXP linux-imx a1638da9, gsl_yamato.c:353-354; yamato_registers.h: SQ_VS_PROGRAM/SQ_PS_PROGRAM. */ + case kIdxSqVsProgram: case kIdxSqPsProgram: + if (value) HaltUnsupportedAccess("unsupported shader program selector", kBase + idx * 4u, value); + break; + /* NXP linux-imx a1638da9, yamato_registers.h: SCRATCH_ADDR; gsl_ringbuffer.h:196-201. */ + case kIdxScratchAddr: + if (value & 31u) HaltUnsupportedAccess("SCRATCH_ADDR unsupported alignment", kBase + idx * 4u, value); + break; + case kIdxScratchUmsk: + if (value > 1u) HaltUnsupportedAccess("SCRATCH_UMSK unsupported mask/swap", kBase + idx * 4u, value); + break; + case kIdxPmOverride1: case kIdxPmOverride2: WriteWord(kBase + idx * 4u, value); return; + case kIdxSoftReset: case kIdxRbbmCntl: case kIdxRbbmIntCntl: case kIdxCpIntCntl: + case kIdxRbWptrBase: case kIdxRbWptrDelay: case kIdxMhArbiterConfig: + case kIdxMhMmuConfig: case kIdxMhInterruptMask: case kIdxMhMmuMpuBase: case kIdxMhMmuMpuEnd: + case kIdxRbCntl: case kIdxRbBase: case kIdxRbRptrAddr: case kIdxRbWptr: + case kIdxCpIntAck: case kIdxCpDebug: + case kIdxMeCntl: case kIdxMeRamWaddr: case kIdxMeRamData: case kIdxPfpUcodeAddr: + case kIdxPfpUcodeData: case kIdxQueueThresh: case kIdxRbbmStatus: + case kIdxMasterIntSignal: case kIdxPeriphId1: case kIdxPeriphId2: case kIdxPatchRelease: + HaltUnsupportedAccess("PM4 unsupported control register write", kBase + idx * 4u, value); + default: break; + } + /* NXP linux-imx a1638da9, gsl_ringbuffer.c:527-531,833-838; gsl_cmdstream.h: GSL_CMDSTREAM_GET_SOP_TIMESTAMP. */ + if (idx == kIdxScratchReg0) { + const auto mask = reg_file_.find(kIdxScratchUmsk); + if (mask != reg_file_.end() && mask->second != 0u) { + const auto address = reg_file_.find(kIdxScratchAddr); + if (mask->second != 1u || address == reg_file_.end() || (address->second & 31u) != 0) + HaltUnsupportedAccess("scratch timestamp unsupported configuration", kBase + idx * 4u, value); + WritePa32(address->second, value); + } + } + emu_.Get().ShadowWrite(idx, value, MmuConfig()); + reg_file_[idx] = value; + } + + /* NXP linux-imx gsl_pm4types.h: pm4_type0 and pm4_type0_packet_for_sameregister. */ + void StoreType0(const Imx51Gpu3dPacket& packet) { + const uint32_t hdr = packet.header, cnt = packet.payload_count; const uint32_t regindx = hdr & 0x7FFFu; - const bool same = (hdr & 0x8000u) != 0u; + const bool same = packet.same_register; for (uint32_t k = 0; k < cnt; ++k) - reg_file_[same ? regindx : regindx + k] = ReadPa32(pa + 4u + k * 4u); + WriteRegister(same ? regindx : regindx + k, ReadOperand(packet, k)); } - /* SET_CONSTANT (kgsl_drawctxt.c:360 PM4_REG / gsl_yamato_imx.c:286): first payload - dword = (type<<16)|offset; the cnt-1 values load into that type's bank, which the - draw-context save reads back as registers (reg_to_mem). Held in the register file. */ - void StoreSetConstant(uint32_t pa, uint32_t cnt) { - const uint32_t tgt = ReadPa32(pa + 4u); + /* NXP linux-imx gsl_drawctxt.c: reg_to_mem, build_reg_to_mem_range and PM4_REG. */ + void StoreSetConstant(const Imx51Gpu3dPacket& packet) { + const uint32_t pa = static_cast(packet.address), cnt = packet.payload_count; + const uint32_t tgt = ReadOperand(packet, 0u); const uint32_t offset = tgt & 0xFFFFu; uint32_t base = 0u; switch ((tgt >> 16) & 0x7u) { @@ -144,240 +227,186 @@ class Imx51Gpu3d : public Peripheral { case 2u: base = kScBaseBool; break; case 3u: base = kScBaseLoop; break; case 4u: base = kScBaseReg; break; - default: HaltUnsupportedAccess("SET_CONSTANT type", pa, tgt); + default: HaltUnsupportedAccess("SET_CONSTANT type", static_cast(pa), tgt); } for (uint32_t j = 0; j + 1u < cnt; ++j) - reg_file_[base + offset + j] = ReadPa32(pa + 8u + j * 4u); + WriteRegister(base + offset + j, ReadOperand(packet, j + 1u)); } - /* INDIRECT_BUFFER follow: drain context-state setup; draws FATAL (kgsl_pm4types.h). */ - void ScanIb(uint32_t ibaddr, uint32_t sizedwords) { + /* NXP linux-imx gsl_pm4types.h: PM4_HDR_INDIRECT_BUFFER. */ + void ScanIb(uint32_t ibaddr, uint32_t sizedwords, uint32_t depth = 1u) { + /* NXP linux-imx yamato/22/yamato_registers.h: CP_IB1/2_BASE, CP_IB1/2_BUFSZ. */ + if ((ibaddr & 3u) != 0 || sizedwords == 0 || sizedwords > 0xFFFFFu) + HaltUnsupportedAccess("CP IB address/size", ibaddr, sizedwords); + ReadSpan(ibaddr, uint64_t(sizedwords) * 4u); for (uint32_t i = 0; i < sizedwords; ) { - const uint32_t pa = ibaddr + i * 4u; - const uint32_t hdr = ReadPa32(pa); - const uint32_t type = hdr >> 30; - const uint32_t cnt = ((hdr >> 16) & 0x3FFFu) + 1u; - if (type == kPm4Type0) { StoreType0(hdr, pa, cnt); i += 1u + cnt; continue; } - if (type == kPm4Type2) { i += 1u; continue; } - if (type != kPm4Type3) - HaltUnsupportedAccess("IB packet type", pa, hdr); - switch ((hdr >> 8) & 0xFFu) { + const auto packet = DecodePacket(uint64_t(ibaddr) + uint64_t(i) * 4u, + sizedwords - i, Imx51Gpu3dPacketSource::IndirectBuffer); + const uint32_t pa = static_cast(packet.address), hdr = packet.header; + const uint32_t cnt = packet.payload_count; + if (packet.type == kPm4Type0) { StoreType0(packet); i += 1u + cnt; continue; } + if (packet.type == kPm4Type2) { i += 1u; continue; } + switch (packet.opcode) { + /* NXP linux-imx gsl_debug_pm4.c: WritePM4Packet_Type3, IB1/IB2 traversal. */ + case kPm4OpIndirectBuffer: + case kPm4OpIndirectBufferPfd: + if (depth >= 2u) + HaltUnsupportedAccess("CP unsupported IB nesting depth", pa, depth); + if (cnt != 2u) + HaltUnsupportedAccess("CP IB packet length", pa, cnt); + ScanIb(ReadOperand(packet, 0u), ReadOperand(packet, 1u), depth + 1u); + break; case kPm4OpNop: case kPm4OpWaitForIdle: case kPm4OpInvalidateState: break; /* invalidates GPU pipeline state groups so later draws reload; CERF's GPU3D caches no cross-draw state (each C2D blit reads its config fresh from reg_file_), so nothing to flush -> inert */ - case kPm4OpLoadConstantContext: break; /* loads ALU/TEX from memory the save never reg_to_mem's; render draws FATAL -> inert */ - case kPm4OpImStore: break; /* copies the (unmodeled) shader instruction memory to system memory (kgsl_pm4types.h:148), consumed only by a shader DRAW, which FATALs at HandleDrawIndx -> inert */ - case kPm4OpImLoad: /* pointer-based (kgsl_pm4types.h:118) */ - case kPm4OpImLoadImmediate: break; /* both load shader instruction memory (inline form kgsl_pm4types.h:121); the modeled C2D blit runs no shader (HandleDrawIndx = fixed-function copy) so it is never consumed -> inert */ - case kPm4OpSetShaderBases: break; /* sets vertex/pixel shader instruction base pointers; the modeled C2D blit (HandleDrawIndx) is a fixed-function surface copy that runs no shader, so the bases are never consumed -> inert */ - case kPm4OpRegRmw: { /* fixup RMW of SCRATCH_REG2; the operand it computes is read back only by SET_SHADER_BASES (0x4A), which is inert (fixed-function blit runs no shader) -> operand unused -> inert */ - const uint32_t rmw_reg = ReadPa32(pa + 4u); - if (rmw_reg != kIdxScratchReg2) - HaltUnsupportedAccess("REG_RMW target", pa, rmw_reg); - break; - } + case kPm4OpLoadConstantContext: emu_.Get().Load(packet, reg_file_, MmuConfig()); break; + case kPm4OpImStore: case kPm4OpImLoad: case kPm4OpImLoadImmediate: + case kPm4OpSetShaderBases: case 0x4Bu: case 0x34u: + emu_.Get().Packet(packet, reg_file_, MmuConfig()); break; + case kPm4OpRegRmw: HandleRegRmw(packet); break; case kPm4OpWaitRegEq: { /* [reg][ref][mask][poll] (lib2d-z430 emitter sub_41A62890); the Z430 completes synchronously, so the wait is met by the current register state, else self-reveal */ - const uint32_t reg = ReadPa32(pa + 4u); - const uint32_t ref = ReadPa32(pa + 8u); - const uint32_t mask = ReadPa32(pa + 12u); + const uint32_t reg = ReadOperand(packet, 0u); + const uint32_t ref = ReadOperand(packet, 1u); + const uint32_t mask = ReadOperand(packet, 2u); if ((ReadWord(kBase + reg * 4u) & mask) != ref) - HaltUnsupportedAccess("WAIT_REG_EQ condition unmet", pa, reg); + HaltUnsupportedAccess("WAIT_REG_EQ condition unmet", static_cast(pa), reg); break; } - case kPm4OpSetConstant: StoreSetConstant(pa, cnt); break; - case kPm4OpRegToMem: HandleRegToMem(pa); break; - case kPm4OpEventWrite: HandleEventWrite(pa); break; /* blit-tail CACHE_FLUSH (cnt=1) / CACHE_FLUSH_TS */ - case kPm4OpDrawIndx: HandleDrawIndx(pa); break; + case kPm4OpSetConstant: StoreSetConstant(packet); break; + case kPm4OpMemWrite: HandleMemWrite(packet); break; + case kPm4OpRegToMem: HandleRegToMem(packet); break; + case kPm4OpEventWrite: HandleEventWrite(packet); break; /* blit-tail CACHE_FLUSH (cnt=1) / CACHE_FLUSH_TS */ + case kPm4OpDrawIndx: HandleDrawIndx(packet); break; default: - HaltUnsupportedAccess("IB opcode", pa, hdr); /* unknown draw/opcode */ + HaltUnsupportedAccess("IB opcode", static_cast(pa), hdr); /* unknown draw/opcode */ } i += 1u + cnt; } } + /* NXP linux-imx a1638da9, gsl_drawctxt.c:1233-1245, shader partition fixup. */ + void HandleRegRmw(const Imx51Gpu3dPacket& packet) { + if (packet.payload_count != 3u) + HaltUnsupportedAccess("REG_RMW payload length", static_cast(packet.address), packet.payload_count); + const uint32_t target = ReadOperand(packet, 0u); + if (target != kIdxScratchReg2) + HaltUnsupportedAccess("REG_RMW unsupported target/flags", static_cast(packet.address), target); + const uint32_t and_mask = ReadOperand(packet, 1u), or_mask = ReadOperand(packet, 2u); + WriteRegister(target, (ReadWord(kBase + target * 4u) & and_mask) | or_mask); + } + + /* NXP linux-imx a1638da9, gsl_pm4types.h: PM4_MEM_WRITE; + Mesa e97ad748, adreno_pm4.xml: CP_MEM_WRITE A2XX-A4XX; + sync_2 EA5T-14D544-BA.sec, librenderboy.dll: 0x41CCCE5C. */ + void HandleMemWrite(const Imx51Gpu3dPacket& packet) { + const uint32_t pa = static_cast(packet.address); + if (packet.payload_count < 2u) + HaltUnsupportedAccess("PM4 malformed MEM_WRITE payload", pa, packet.header); + const uint32_t destination = ReadOperand(packet, 0u); + if ((destination & 3u) != 0) + HaltUnsupportedAccess("MEM_WRITE unsupported address low bits", pa, destination); + const uint32_t packet_bytes = (packet.payload_count + 1u) * 4u; + const uint32_t data_bytes = (packet.payload_count - 1u) * 4u; + const uint8_t* source = ReadSpan(packet.address, packet_bytes); + uint8_t* target = WriteSpan(destination, data_bytes); + const uint64_t source_host = reinterpret_cast(source); + const uint64_t target_host = reinterpret_cast(target); + if (target_host < source_host + packet_bytes && source_host < target_host + data_bytes) + HaltUnsupportedAccess("MEM_WRITE unsupported packet overlap", pa, destination); + std::memcpy(target, source + 8u, data_bytes); + } + /* EVENT_WRITE/CACHE_FLUSH_TS: write the EOP timestamp the guest polls via kgsl_cmdstream_check_timestamp (kgsl_ringbuffer.c:635-640); addr+value inline. */ - void HandleEventWrite(uint32_t pa) { - const uint32_t event = ReadPa32(pa + 4u); + void HandleEventWrite(const Imx51Gpu3dPacket& packet) { + const uint32_t pa = static_cast(packet.address); + const uint32_t event = ReadOperand(packet, 0u); if (event == kEventCacheFlush) return; /* no writeback; GPU MMU off -> DRAM already coherent */ if (event != kEventCacheFlushTs) HaltUnsupportedAccess("CP EVENT_WRITE event", pa, event); - const uint32_t addr = ReadPa32(pa + 8u); - uint8_t* dst = emu_.Get().TryTranslateWrite(addr); - if (!dst) - HaltUnsupportedAccess("EOP timestamp writeback unbacked", addr, ReadPa32(pa + 12u)); - *reinterpret_cast(dst) = ReadPa32(pa + 12u); + const uint32_t addr = ReadOperand(packet, 1u); + WritePa32(addr, ReadOperand(packet, 2u)); } /* REG_TO_MEM (draw-context save, kgsl_drawctxt.c reg_to_mem:416 / build_reg_to_mem_range:438): read GPU register `src` and write its value to memory at `dst`. Packet: [hdr cnt=2][src reg index (| shadow flag)][dst gpuaddr]. */ - void HandleRegToMem(uint32_t pa) { - const uint32_t src = ReadPa32(pa + 4u) & ~kRegToMemShadowFlag; - const uint32_t dst = ReadPa32(pa + 8u); + void HandleRegToMem(const Imx51Gpu3dPacket& packet) { + const uint32_t src = ReadOperand(packet, 0u) & ~kRegToMemShadowFlag; + const uint32_t dst = ReadOperand(packet, 1u); const uint32_t value = ReadWord(kBase + src * 4u); /* register-file / modeled read; unmodeled -> FATAL, self-revealing */ - uint8_t* out = emu_.Get().TryTranslateWrite(dst); - if (!out) - HaltUnsupportedAccess("REG_TO_MEM dst writeback unbacked", dst, value); - *reinterpret_cast(out) = value; - } - - uint32_t BlitReg(uint32_t idx, uint32_t pa) { - auto it = reg_file_.find(idx); - if (it == reg_file_.end()) - HaltUnsupportedAccess("blit config register not programmed", pa, idx); - return it->second; + WritePa32(dst, value); } - static float AsFloat(uint32_t u) { float f; std::memcpy(&f, &u, sizeof(f)); return f; } - - /* C2D2 2D-blit (lib2d-z430 sub_41A63F00): a 4-vertex screen-quad DRAW_INDX surface copy. - The source read-swizzle (BGRA, SQ_TEX Z,Y,X,W) and the dest store-swap (RB_COLOR_INFO - SWAP=1 = B8G8R8A8, mesa fd2_gmem.c fmt2swap) invert -> the copy is byte-identical 32bpp; - adding any channel permutation here would double-swap and corrupt colors. */ - void HandleDrawIndx(uint32_t pa) { - const uint32_t ctrl = ReadPa32(pa + 8u); /* DRAW_INDX word2 (vgt_draw_initiator; a2xx num_indices[31:16]) */ - if ((ctrl & 0x3Fu) != 6u || /* PRIM_TYPE = 4-vertex quad (not kgsl's 3-vertex RectList) */ - ((ctrl >> 6) & 0x3u) != 2u || /* SOURCE_SELECT = AUTO_INDEX */ - (ctrl >> 16) != 4u) /* num_indices = 4 */ - HaltUnsupportedAccess("DRAW_INDX not the C2D2 4-vert blit", pa, ctrl); - - /* dest surface: RB_COLOR_INFO (0x2001) FORMAT[3:0] = COLORX_8_8_8_8(5) or - COLORX_5_6_5(2), SWAP[10:9]=1 (BGRA), BASE[31:12]; RB_SURFACE_INFO (0x2000) - pitch[13:0] in pixels (a2xx.xml). */ - const uint32_t ci = BlitReg(0x2001u, pa); - const uint32_t dstFmt = ci & 0xFu; - if ((dstFmt != 5u && dstFmt != 2u) || ((ci >> 9) & 0x3u) != 1u) - HaltUnsupportedAccess("blit dest not COLORX_8_8_8_8/5_6_5 SWAP=1", pa, ci); - const uint32_t dstBase = ci & 0xFFFFF000u; - const uint32_t dstBpp = (dstFmt == 2u) ? 2u : 4u; /* COLORX_5_6_5=2B, _8_8_8_8=4B */ - const uint32_t dstPitch = BlitReg(0x2000u, pa) & 0x3FFFu; - - /* source surface = the SQ_TEX const (0x4800 + slot*6) whose base == the COHER-flushed - source (COHER_BASE_PM4 0xA2A). a2xx.xml A2XX_SQ_TEX: w0 PITCH[30:22]<<5/TILED[31], - w1 FORMAT[5:0]/BASE[31:12], w2 WIDTH[12:0]/HEIGHT[25:13] (size-1), w3 SWIZ_X/Y/Z/W + - XY_MAG/MIN_FILTER[20:19]/[22:21]. */ - const uint32_t cohBase = BlitReg(0x0A2Au, pa); - uint32_t fb = 0u; - for (uint32_t s = 0u; s < 16u && fb == 0u; ++s) { - auto it = reg_file_.find(0x4801u + s * 6u); /* word1 carries the base */ - if (it != reg_file_.end() && (it->second & 0xFFFFF000u) == cohBase) - fb = 0x4800u + s * 6u; - } - if (fb == 0u) - HaltUnsupportedAccess("blit source fetch const not found", pa, cohBase); - const uint32_t sw0 = BlitReg(fb + 0u, pa), sw1 = BlitReg(fb + 1u, pa); - const uint32_t sw2 = BlitReg(fb + 2u, pa), sw3 = BlitReg(fb + 3u, pa); - const uint32_t srcFmt = sw1 & 0x3Fu; /* SQ_TEX FORMAT (a2xx_sq_surfaceformat) */ - const uint32_t swizW = (sw3 >> 10) & 0x7u; - if ((srcFmt != 6u && srcFmt != 4u) || (sw0 >> 31) != 0u || /* FMT_8_8_8_8/5_6_5, not tiled */ - ((sw3 >> 19) & 0x3u) != 0u || ((sw3 >> 21) & 0x3u) != 0u || /* XY_MAG/MIN_FILTER = POINT */ - ((sw3 >> 1) & 0x7u) != 2u || ((sw3 >> 4) & 0x7u) != 1u || /* SWIZ_X=Z, SWIZ_Y=Y */ - ((sw3 >> 7) & 0x7u) != 0u || /* SWIZ_Z=X (BGRA) */ - (swizW != 3u && swizW != 5u)) { /* SWIZ_W = W (pass) or ONE (force opaque), a2xx.xml:1747 */ - HaltUnsupportedAccess("blit source not FMT_8888/565 POINT BGRA", pa, sw3); - } - const uint32_t srcBpp = (srcFmt == 4u) ? 2u : 4u; /* FMT_5_6_5=2B, FMT_8_8_8_8=4B */ - /* SWIZ_W=ONE forces the sampled alpha opaque; it changes the stored pixel only for an - alpha-bearing dest. Into 565 alpha is dropped (no-op); into 8888 it must write A=0xFF - (not the source alpha) - not yet modeled, so FATAL. */ - if (swizW == 5u && dstBpp == 4u) - HaltUnsupportedAccess("blit SWIZ_W=ONE into 8888 dest (alpha-force not modeled)", pa, sw3); - const uint32_t srcBase = sw1 & 0xFFFFF000u; - const uint32_t srcPitch = ((sw0 >> 22) & 0x1FFu) << 5; - const uint32_t srcW = (sw2 & 0x1FFFu) + 1u; - const uint32_t srcH = ((sw2 >> 13) & 0x1FFFu) + 1u; - /* opaque (RB_COLORCONTROL 0x2202 BLEND_DISABLE bit5) + all channels (RB_COLOR_MASK 0x2104 - == 0xF) + direct screen coords (PA_CL_VTE_CNTL 0x2206 viewport scale/offset [5:0] = 0). */ - if (((BlitReg(0x2202u, pa) >> 5) & 0x1u) != 1u || - (BlitReg(0x2104u, pa) & 0xFu) != 0xFu || - (BlitReg(0x2206u, pa) & 0x3Fu) != 0u) - HaltUnsupportedAccess("blit not opaque/full-mask/direct-coord", pa, ci); - - /* geometry: vertex ALU 0x4048 = (W/2,H/2,W/2,H/2) -> 1:1 with source, origin (0,0); - tex ALU 0x4098 = (0.5,0.5,0.5,0.5) -> full [0,1] source; window scissor (0x2081/0x2082, - adreno_reg_xy X[14:0]/Y[30:16]) origin (0,0) covering the full extent (no clip). */ - const uint32_t vhw = BlitReg(0x4048u, pa), vhh = BlitReg(0x4049u, pa); - if (AsFloat(vhw) * 2.0f != static_cast(srcW) || - AsFloat(vhh) * 2.0f != static_cast(srcH) || - BlitReg(0x404Au, pa) != vhw || BlitReg(0x404Bu, pa) != vhh) - HaltUnsupportedAccess("blit geometry not 1:1 full-screen", pa, srcW); - for (uint32_t k = 0u; k < 4u; ++k) - if (BlitReg(0x4098u + k, pa) != 0x3F000000u) /* 0.5f */ - HaltUnsupportedAccess("blit tex not full [0,1]", pa, 0x4098u + k); - const uint32_t tl = BlitReg(0x2081u, pa), br = BlitReg(0x2082u, pa); - if ((tl & 0x7FFFu) != 0u || ((tl >> 16) & 0x7FFFu) != 0u || - (br & 0x7FFFu) < srcW || ((br >> 16) & 0x7FFFu) < srcH) - HaltUnsupportedAccess("blit scissor origin/clip", pa, br); - - /* Same-format C2D copy source[0..W,0..H] -> dest[0..W,0..H] (gpuaddr==physical). Validate - each surface is one contiguously-backed span (start + last byte), then row-copy per bpp. */ - auto& mem = emu_.Get(); - const uint32_t sSpan = (srcH - 1u) * srcPitch * srcBpp + srcW * srcBpp; - const uint32_t dSpan = (srcH - 1u) * dstPitch * dstBpp + srcW * dstBpp; - const uint8_t* s0 = mem.TryTranslate(srcBase); - const uint8_t* sN = mem.TryTranslate(srcBase + sSpan - 1u); - uint8_t* d0 = mem.TryTranslateWrite(dstBase); - uint8_t* dN = mem.TryTranslateWrite(dstBase + dSpan - 1u); - if (!s0 || !d0 || sN != s0 + (sSpan - 1u) || dN != d0 + (dSpan - 1u)) - HaltUnsupportedAccess("blit surface not contiguously backed", srcBase, dstBase); - if (srcBpp == dstBpp) { /* same format: byte-identical row copy (swizzle+SWAP net identity) */ - for (uint32_t y = 0u; y < srcH; ++y) - std::memcpy(d0 + y * dstPitch * dstBpp, s0 + y * srcPitch * srcBpp, srcW * srcBpp); - } else if (srcBpp == 4u) { /* 8888 source -> 565 dest: pack each 0xAARRGGBB pixel to standard - RGB565 (the IPU BG scanout reads it back via Expand565). */ - for (uint32_t y = 0u; y < srcH; ++y) { - const uint32_t* srow = reinterpret_cast(s0 + y * srcPitch * 4u); - uint16_t* drow = reinterpret_cast(d0 + y * dstPitch * 2u); - for (uint32_t x = 0u; x < srcW; ++x) drow[x] = imx51_pixel::PackArgb565(srow[x]); - } - } else { /* 565 source -> 8888 dest: not yet fired */ - HaltUnsupportedAccess("blit 565 source into 8888 dest (expand not modeled)", srcBase, dstBase); - } + void HandleDrawIndx(const Imx51Gpu3dPacket& packet) { + const uint32_t control = ReadOperand(packet, 1u); + if (control == 0x00040086u) + emu_.Get().Draw(control, packet.address, reg_file_, MmuConfig()); + else emu_.Get().Packet(packet, reg_file_, MmuConfig()); } - /* CP ring kick: scan the pending ring commands as PM4 packets, model the completion, - then write rptr to the memptrs slot so kgsl_yamato_idle's rptr==wptr poll passes. */ + /* NXP linux-imx gsl_ringbuffer.c: gsl_ringbuffer_sizelog2quadwords; + yamato/22/yamato_registers.h: CP_RB_BASE, CP_RB_CNTL and CP_RB_WPTR. */ void HandleRbWptr(uint32_t wptr) { - /* On wrap only [0,wptr) holds new commands: the driver NOP-pads the tail and - submits it via a separate wptr=old_wptr+1 write this handler already consumed, - then sets wptr=0 (kgsl_ringbuffer.c:211-221). rptr_ resets to the ring start. */ - if (wptr < rptr_) - rptr_ = 0u; - ScanRing(rptr_, wptr); - rptr_ = wptr; + const uint32_t shift = rb_cntl_ & 0x3Fu; + if (shift >= 20u || (rb_base_ & 31u) != 0 || (rb_cntl_ & 0x30000u) != 0) + HaltUnsupportedAccess("CP ring geometry/swap", rb_base_, rb_cntl_); + const uint32_t size = 2u << shift; + if (wptr >= size || rptr_ >= size) + HaltUnsupportedAccess("CP ring cursor", rb_base_, wptr); + ReadSpan(rb_base_, uint64_t(size) * 4u); + if ((rb_cntl_ & 0x08000000u) == 0 && (rb_rptr_addr_ & 3u) != 0) + HaltUnsupportedAccess("CP RPTR unsupported swap", rb_rptr_addr_, rb_cntl_); wptr_ = wptr; - uint8_t* rp = emu_.Get().TryTranslateWrite(rb_rptr_addr_); - if (!rp) - HaltUnsupportedAccess("CP rptr writeback unbacked", rb_rptr_addr_, wptr); - *reinterpret_cast(rp) = wptr; + ScanRing(size); } - void ScanRing(uint32_t off, uint32_t end) { - for (; off < end; ) { - const uint32_t pa = rb_base_ + off * 4u; - const uint32_t hdr = ReadPa32(pa); - const uint32_t type = hdr >> 30; - const uint32_t cnt = ((hdr >> 16) & 0x3FFFu) + 1u; - if (type == kPm4Type0) { off += 1u + cnt; continue; } /* CP_TIMESTAMP */ - if (type == kPm4Type2) { off += 1u; continue; } - if (type != kPm4Type3) - HaltUnsupportedAccess("CP ring packet type", pa, hdr); - switch ((hdr >> 8) & 0xFFu) { - case kPm4OpMeInit: - case kPm4OpNop: - case kPm4OpWaitForIdle: break; - case kPm4OpIndirectBuffer: - case kPm4OpIndirectBufferPfd: - ScanIb(ReadPa32(pa + 4u), ReadPa32(pa + 8u)); - break; - case kPm4OpEventWrite: HandleEventWrite(pa); break; - /* CP INTERRUPT: no ARM CP-completion line (RM Table 3-2, GPU3D=IRQ102 idle only). */ - case kPm4OpInterrupt: break; - default: - HaltUnsupportedAccess("CP ring-scan opcode", pa, hdr); + /* NXP linux-imx gsl_ringbuffer.c: kgsl_ringbuffer_waitspace, kgsl_ringbuffer_addcmds. */ + void ScanRing(uint32_t size) { + while (rptr_ != wptr_) { + const uint32_t available = (wptr_ + size - rptr_) % size; + const uint32_t tail = size - rptr_; + const auto packet = DecodePacket(uint64_t(rb_base_) + uint64_t(rptr_) * 4u, + available < tail ? available : tail, + Imx51Gpu3dPacketSource::Ring); + const uint32_t pa = static_cast(packet.address), hdr = packet.header; + const uint32_t count = 1u + packet.payload_count; + if (count > tail) + HaltUnsupportedAccess("CP ring packet crosses tail", pa, hdr); + if (count > available) { + if (packet.type == kPm4Type3 && packet.opcode == kPm4OpNop && count == tail) + return; + HaltUnsupportedAccess("CP ring truncated packet", pa, hdr); + } + if (packet.type == kPm4Type0) StoreType0(packet); + if (packet.type == kPm4Type3) { + switch (packet.opcode) { + case kPm4OpMeInit: + case kPm4OpNop: + case kPm4OpWaitForIdle: break; + case kPm4OpIndirectBuffer: + case kPm4OpIndirectBufferPfd: + if (packet.payload_count != 2u) + HaltUnsupportedAccess("CP IB packet length", pa, packet.payload_count); + ScanIb(ReadOperand(packet, 0u), ReadOperand(packet, 1u)); + break; + case kPm4OpEventWrite: HandleEventWrite(packet); break; + case kPm4OpMemWrite: HandleMemWrite(packet); break; + case kPm4OpLoadConstantContext: emu_.Get().Load(packet, reg_file_, MmuConfig()); break; + case kPm4OpRegRmw: HandleRegRmw(packet); break; + case kPm4OpDrawIndx: HandleDrawIndx(packet); break; + case kPm4OpImStore: case kPm4OpImLoad: case kPm4OpImLoadImmediate: + case kPm4OpSetShaderBases: case 0x4Bu: case 0x34u: + emu_.Get().Packet(packet, reg_file_, MmuConfig()); break; + /* MCIMX51RM Table 3-2: GPU3D IRQ102 idle indication. */ + case kPm4OpInterrupt: break; + default: HaltUnsupportedAccess("CP ring-scan opcode", pa, hdr); + } } - off += 1u + cnt; + rptr_ = (rptr_ + count) % size; + /* NXP linux-imx gsl_ringbuffer.c: kgsl_ringbuffer_start, rb_no_update. */ + if ((rb_cntl_ & 0x08000000u) == 0) + WritePa32(rb_rptr_addr_, rptr_); } } diff --git a/cerf/socs/imx51/imx51_gpu3d_blit.cpp b/cerf/socs/imx51/imx51_gpu3d_blit.cpp new file mode 100644 index 00000000..b3de38b0 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_blit.cpp @@ -0,0 +1,123 @@ +#include "imx51_gpu3d_blit.h" +#include "imx51_gpu3d_memory.h" +#include "imx51_pixel_pack.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../boards/board_context.h" +#include + +REGISTER_SERVICE(Imx51Gpu3dBlit); + +bool Imx51Gpu3dBlit::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} + +void Imx51Gpu3dBlit::HaltUnsupportedAccess(const char* op, uint32_t address, uint64_t value) const { + emu_.Get().Die("GPU blit rejected %s at 0x%08X (value 0x%016llX)", + op, address, static_cast(value)); +} + +uint32_t Imx51Gpu3dBlit::BlitReg(const std::unordered_map& registers, uint32_t idx, uint64_t pa) { + auto it = registers.find(idx); + if (it == registers.end()) + HaltUnsupportedAccess("blit config register not programmed", static_cast(pa), idx); + return it->second; +} +float Imx51Gpu3dBlit::AsFloat(uint32_t u) { float f; std::memcpy(&f, &u, sizeof(f)); return f; } + +/* sync_2 EA5T-14D544-BA.sec, lib2d-z430.dll: 0x41A63F00; + Mesa e97ad748 a2xx.xml: A2XX_SQ_TEX, RB_COLOR_INFO, RB_SURFACE_INFO, + RB_COLORCONTROL, RB_COLOR_MASK, PA_CL_VTE_CNTL and PA_SC_WINDOW_SCISSOR. */ +void Imx51Gpu3dBlit::Draw(uint32_t ctrl, uint64_t pa, + const std::unordered_map& registers, + uint32_t mmu_config) { + /* NXP a1638da9 PA_SU_SC_MODE_CNTL: the C2D shortcut must not bypass the + draw frontend's exclusion of face-stream side effects. */ + const uint32_t face = BlitReg(registers, 0x2205u, pa); + if (face & 0xF0000000u) + HaltUnsupportedAccess("faceness controls", static_cast(pa), face); + if ((ctrl & 0x3Fu) != 6u || + ((ctrl >> 6) & 0x3u) != 2u || + (ctrl >> 16) != 4u) + HaltUnsupportedAccess("DRAW_INDX not the C2D2 4-vert blit", static_cast(pa), ctrl); + + const uint32_t ci = BlitReg(registers, 0x2001u, pa); + const uint32_t dstFmt = ci & 0xFu; + if ((dstFmt != 5u && dstFmt != 2u) || ((ci >> 9) & 0x3u) != 1u) + HaltUnsupportedAccess("blit dest not COLORX_8_8_8_8/5_6_5 SWAP=1", static_cast(pa), ci); + const uint32_t dstBase = ci & 0xFFFFF000u; + const uint32_t dstBpp = (dstFmt == 2u) ? 2u : 4u; + const uint32_t dstPitch = BlitReg(registers, 0x2000u, pa) & 0x3FFFu; + + const uint32_t cohBase = BlitReg(registers, 0x0A2Au, pa); + uint32_t fb = 0u; + for (uint32_t s = 0u; s < 16u && fb == 0u; ++s) { + auto it = registers.find(0x4801u + s * 6u); + if (it != registers.end() && (it->second & 0xFFFFF000u) == cohBase) + fb = 0x4800u + s * 6u; + } + if (fb == 0u) + HaltUnsupportedAccess("blit source fetch const not found", static_cast(pa), cohBase); + const uint32_t sw0 = BlitReg(registers, fb + 0u, pa), sw1 = BlitReg(registers, fb + 1u, pa); + const uint32_t sw2 = BlitReg(registers, fb + 2u, pa), sw3 = BlitReg(registers, fb + 3u, pa); + const uint32_t srcFmt = sw1 & 0x3Fu; + const uint32_t swizW = (sw3 >> 10) & 0x7u; + if ((srcFmt != 6u && srcFmt != 4u) || (sw0 >> 31) != 0u || + ((sw3 >> 19) & 0x3u) != 0u || ((sw3 >> 21) & 0x3u) != 0u || + ((sw3 >> 1) & 0x7u) != 2u || ((sw3 >> 4) & 0x7u) != 1u || + ((sw3 >> 7) & 0x7u) != 0u || + (swizW != 3u && swizW != 5u)) { + HaltUnsupportedAccess("blit source not FMT_8888/565 POINT BGRA", static_cast(pa), sw3); + } + const uint32_t srcBpp = (srcFmt == 4u) ? 2u : 4u; + + if (swizW == 5u && dstBpp == 4u) + HaltUnsupportedAccess("blit SWIZ_W=ONE into 8888 dest (alpha-force not modeled)", static_cast(pa), sw3); + const uint32_t srcBase = sw1 & 0xFFFFF000u; + const uint32_t srcPitch = ((sw0 >> 22) & 0x1FFu) << 5; + const uint32_t srcW = (sw2 & 0x1FFFu) + 1u; + const uint32_t srcH = ((sw2 >> 13) & 0x1FFFu) + 1u; + + if (((BlitReg(registers, 0x2202u, pa) >> 5) & 0x1u) != 1u || + (BlitReg(registers, 0x2104u, pa) & 0xFu) != 0xFu || + (BlitReg(registers, 0x2206u, pa) & 0x3Fu) != 0u) + HaltUnsupportedAccess("blit not opaque/full-mask/direct-coord", static_cast(pa), ci); + + const uint32_t vhw = BlitReg(registers, 0x4048u, pa), vhh = BlitReg(registers, 0x4049u, pa); + if (AsFloat(vhw) * 2.0f != static_cast(srcW) || + AsFloat(vhh) * 2.0f != static_cast(srcH) || + BlitReg(registers, 0x404Au, pa) != vhw || BlitReg(registers, 0x404Bu, pa) != vhh) + HaltUnsupportedAccess("blit geometry not 1:1 full-screen", static_cast(pa), srcW); + for (uint32_t k = 0u; k < 4u; ++k) + if (BlitReg(registers, 0x4098u + k, pa) != 0x3F000000u) + HaltUnsupportedAccess("blit tex not full [0,1]", static_cast(pa), 0x4098u + k); + const uint32_t tl = BlitReg(registers, 0x2081u, pa), br = BlitReg(registers, 0x2082u, pa); + if ((tl & 0x7FFFu) != 0u || ((tl >> 16) & 0x7FFFu) != 0u || + (br & 0x7FFFu) < srcW || ((br >> 16) & 0x7FFFu) < srcH) + HaltUnsupportedAccess("blit scissor origin/clip", static_cast(pa), br); + + const uint64_t sSpan = uint64_t(srcH - 1u) * srcPitch * srcBpp + uint64_t(srcW) * srcBpp; + const uint64_t dSpan = uint64_t(srcH - 1u) * dstPitch * dstBpp + uint64_t(srcW) * dstBpp; + const uint8_t* s0 = emu_.Get().ReadSpan(srcBase, sSpan, mmu_config); + uint8_t* d0 = emu_.Get().WriteSpan(dstBase, dSpan, mmu_config); + if (srcBpp == dstBpp) { + for (uint32_t y = 0u; y < srcH; ++y) + std::memmove(d0 + uint64_t(y) * dstPitch * dstBpp, s0 + uint64_t(y) * srcPitch * srcBpp, srcW * srcBpp); + } else if (srcBpp == 4u) { + for (uint32_t y = 0u; y < srcH; ++y) { + const uint8_t* srow = s0 + uint64_t(y) * srcPitch * 4u; + uint8_t* drow = d0 + uint64_t(y) * dstPitch * 2u; + for (uint32_t x = 0u; x < srcW; ++x) { + const uint8_t* p = srow + x * 4u; + const uint32_t pixel = uint32_t(p[0]) | (uint32_t(p[1]) << 8) | + (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24); + const uint16_t packed = imx51_pixel::PackArgb565(pixel); + drow[x * 2u] = static_cast(packed); + drow[x * 2u + 1u] = static_cast(packed >> 8); + } + } + } else { + HaltUnsupportedAccess("blit 565 source into 8888 dest (expand not modeled)", srcBase, dstBase); + } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_blit.h b/cerf/socs/imx51/imx51_gpu3d_blit.h new file mode 100644 index 00000000..8ca26c19 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_blit.h @@ -0,0 +1,17 @@ +#pragma once +#include "../../core/service.h" +#include +#include + +class Imx51Gpu3dBlit : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + void Draw(uint32_t ctrl, uint64_t packet_address, + const std::unordered_map& registers, uint32_t mmu_config); + +private: + uint32_t BlitReg(const std::unordered_map& registers, uint32_t index, uint64_t pa); + static float AsFloat(uint32_t value); + [[noreturn]] void HaltUnsupportedAccess(const char* op, uint32_t address, uint64_t value) const; +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_context.cpp b/cerf/socs/imx51/imx51_gpu3d_context.cpp new file mode 100644 index 00000000..b8adddfc --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_context.cpp @@ -0,0 +1,71 @@ +#include "imx51_gpu3d_context.h" +#include "imx51_gpu3d_memory.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../boards/board_context.h" +#include "../../state/state_stream.h" +#include + +REGISTER_SERVICE(Imx51Gpu3dContext); + +bool Imx51Gpu3dContext::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} + +/* NXP linux-imx a1638da9, gsl_drawctxt.c:74-107,1005-1076; gsl_ringbuffer.h:64. */ +void Imx51Gpu3dContext::Load(const Imx51Gpu3dPacket& packet, + std::unordered_map& registers, uint32_t config) { + auto& memory = emu_.Get(); + auto& fatal = emu_.Get(); + if (packet.payload_count < 3u || (packet.payload_count & 1u) == 0u) + fatal.Die("GPU context invalid packet length %u", packet.payload_count); + memory.ReadSpan(packet.address, uint64_t(packet.payload_count + 1u) * 4u, config); + const uint32_t address = memory.ReadPa32(packet.address + 4u, config); + const uint32_t first = memory.ReadPa32(packet.address + 8u, config); + const uint32_t type = (first >> 16) & 7u; + if ((address & 0x1FFFu) > 1u || (first & ~0x0107FFFFu) != 0u || (type != 0u && type != 1u && type != 4u)) + fatal.Die("GPU context unsupported address/type %08X %08X", address, first); + const uint32_t slot = type == 4u ? 2u : type; + const uint32_t base = type == 4u ? 0x2000u : type == 1u ? 0x4800u : 0x4000u; + const uint32_t limit = type == 4u ? 1024u : type == 1u ? 192u : 2048u; + const uint32_t physical = address & ~0x1FFFu; + const bool enabled = (first & 0x01000000u) != 0u; + /* NXP linux-imx a1638da9, gsl_drawctxt.c:1023-1044: force mismatch and shadow enable. */ + const bool load = (address & 1u) || !banks_[slot].enabled || banks_[slot].address != physical; + std::vector> values; + for (uint32_t operand = 1u; operand < packet.payload_count; operand += 2u) { + const uint32_t descriptor = memory.ReadPa32(packet.address + uint64_t(operand + 1u) * 4u, config); + const uint32_t count = memory.ReadPa32(packet.address + uint64_t(operand + 2u) * 4u, config); + const uint32_t offset = descriptor & 0xFFFFu; + const uint32_t allowed = operand == 1u ? 0x0107FFFFu : 0x0007FFFFu; + if ((descriptor & ~allowed) || ((descriptor >> 16) & 7u) != type || offset > limit || count > limit - offset) + fatal.Die("GPU context invalid range %08X %u", descriptor, count); + if (count && load) { + memory.ReadSpan(uint64_t(physical) + uint64_t(offset) * 4u, uint64_t(count) * 4u, config); + for (uint32_t word = 0; word < count; ++word) + values.emplace_back(base + offset + word, + memory.ReadPa32(uint64_t(physical) + uint64_t(offset + word) * 4u, config)); + } + } + for (const auto& [index, value] : values) registers[index] = value; + banks_[slot] = {physical, enabled}; +} + +/* NXP linux-imx a1638da9, gsl_drawctxt.c:620-636,1043,1063,1074: whole-bank shadowing. */ +void Imx51Gpu3dContext::ShadowWrite(uint32_t index, uint32_t value, uint32_t config) { + uint32_t slot, base; + if (index >= 0x4000u && index < 0x4800u) { slot = 0u; base = 0x4000u; } + else if (index >= 0x4800u && index < 0x48C0u) { slot = 1u; base = 0x4800u; } + else if (index >= 0x2000u && index < 0x2400u) { slot = 2u; base = 0x2000u; } + else return; + if (banks_[slot].enabled) + emu_.Get().WritePa32(uint64_t(banks_[slot].address) + uint64_t(index - base) * 4u, value, config); +} + +void Imx51Gpu3dContext::SaveState(StateWriter& writer) { + for (const auto& bank : banks_) { writer.Write(bank.address); writer.Write(bank.enabled); } +} +void Imx51Gpu3dContext::RestoreState(StateReader& reader) { + for (auto& bank : banks_) { reader.Read(bank.address); reader.Read(bank.enabled); } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_context.h b/cerf/socs/imx51/imx51_gpu3d_context.h new file mode 100644 index 00000000..7cb70b64 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_context.h @@ -0,0 +1,21 @@ +#pragma once +#include "../../core/service.h" +#include "imx51_gpu3d_packet.h" +#include +#include + +class StateWriter; +class StateReader; + +class Imx51Gpu3dContext : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + void Load(const Imx51Gpu3dPacket& packet, std::unordered_map& registers, uint32_t mmu_config); + void ShadowWrite(uint32_t index, uint32_t value, uint32_t mmu_config); + void SaveState(StateWriter& writer); + void RestoreState(StateReader& reader); +private: + struct Bank { uint32_t address = 0; bool enabled = false; }; + std::array banks_{}; +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_draw.cpp b/cerf/socs/imx51/imx51_gpu3d_draw.cpp new file mode 100644 index 00000000..e40099f4 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_draw.cpp @@ -0,0 +1,279 @@ +#include "imx51_gpu3d_draw.h" +#include "imx51_gpu3d_memory.h" +#include "imx51_gpu3d_regs.h" +#include "imx51_gpu3d_shader.h" +#include "imx51_gpu3d_raster.h" +#include "../../boards/board_context.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../state/state_stream.h" +#include +#include +#include +#include + +REGISTER_SERVICE(Imx51Gpu3dDraw); + +bool Imx51Gpu3dDraw::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} + +[[noreturn]] void Imx51Gpu3dDraw::Reject(const char* reason, uint64_t value) { + emu_.Get().Die("GPU draw %s (value 0x%016llX)", reason, + static_cast(value)); +} + +uint32_t Imx51Gpu3dDraw::Operand(const Imx51Gpu3dPacket& packet, uint32_t index, uint32_t mmu) { + uint64_t address = 0; + if (!packet.OperandAddress(index, address)) Reject("packet operand", index); + return emu_.Get().ReadPa32(address, mmu); +} + +/* NXP linux-imx a1638da9, gsl_drawctxt.c:84-103,511-516,1233-1240; yamato_registers.h: SQ_INST_STORE_MANAGMENT. */ +uint32_t Imx51Gpu3dDraw::InstructionOffset(uint32_t stage, uint32_t start, uint32_t count) { + if (stage > 1u) Reject("unsupported shared instruction load", stage); + if (uint64_t(start) + count > instructions_[stage].size()) Reject("instruction extent", uint64_t(start) + count); + return start; +} + +std::span Imx51Gpu3dDraw::Program(uint32_t stage) { + const uint32_t descriptor = start_size_[stage], count = descriptor & 0xFFFFu; + const uint32_t offset = InstructionOffset(stage, descriptor >> 16, count); + if (count == 0) return {}; + if (count % 3u != 0) Reject("incomplete shader", stage); + for (uint32_t i = 0; i < count; ++i) + if (!valid_[stage][offset + i]) Reject("uninitialized shader instruction", offset + i); + return {instructions_[stage].data() + offset, count}; +} + +/* NXP linux-imx a1638da9, gsl_drawctxt.c:511-516,1200-1214. */ +void Imx51Gpu3dDraw::Load(const Imx51Gpu3dPacket& packet, uint32_t mmu) { + const bool immediate = packet.opcode == imx51_gpu3d_regs::kPm4OpImLoadImmediate; + const uint32_t address_type = Operand(packet, 0, mmu), descriptor = Operand(packet, 1, mmu); + const uint32_t stage = address_type & 3u, count = descriptor & 0xFFFFu; + if ((immediate && (address_type > 2u || packet.payload_count != count + 2u)) || + (!immediate && packet.payload_count != 2u) || stage > 2u) + Reject("shader load payload", packet.header); + if (stage == 2u && count == 0u) { start_size_[stage] = descriptor; return; } + const uint32_t offset = InstructionOffset(stage, descriptor >> 16, count); + if (count + (start_size_[stage ^ 1u] & 0xFFFFu) > instructions_[stage].size()) + Reject("combined shader instruction capacity", count); + auto& memory = emu_.Get(); + if (count) memory.ReadSpan(immediate ? packet.address + 12u : address_type & ~3u, + uint64_t(count) * 4u, mmu); + std::vector words(count); + for (uint32_t i = 0; i < count; ++i) + words[i] = immediate ? Operand(packet, i + 2u, mmu) : + memory.ReadPa32(uint64_t(address_type & ~3u) + uint64_t(i) * 4u, mmu); + std::copy(words.begin(), words.end(), instructions_[stage].begin() + offset); + std::fill_n(valid_[stage].begin() + offset, count, uint8_t{1}); + start_size_[stage] = descriptor; +} + +/* NXP linux-imx a1638da9, gsl_drawctxt.c:1269-1283. */ +void Imx51Gpu3dDraw::Store(const Imx51Gpu3dPacket& packet, uint32_t mmu) { + if (packet.payload_count != 2u) Reject("shader store payload", packet.header); + const uint32_t address_type = Operand(packet, 0, mmu), descriptor_address = Operand(packet, 1, mmu); + const uint32_t stage = address_type & 3u; + if (stage > 2u || (descriptor_address & 3u)) Reject("shader store stage/address", address_type); + const uint32_t descriptor = start_size_[stage], count = descriptor & 0xFFFFu; + const uint32_t offset = count ? InstructionOffset(stage, descriptor >> 16, count) : 0u; + auto& memory = emu_.Get(); + if (count) memory.WriteSpan(address_type & ~3u, uint64_t(count) * 4u, mmu); + memory.WriteSpan(descriptor_address, 4u, mmu); + for (uint32_t i = 0; i < count; ++i) + if (!valid_[stage][offset + i]) Reject("store uninitialized shader", offset + i); + for (uint32_t i = 0; i < count; ++i) + memory.WritePa32(uint64_t(address_type & ~3u) + uint64_t(i) * 4u, instructions_[stage][offset + i], mmu); + memory.WritePa32(descriptor_address, descriptor, mmu); +} + +void Imx51Gpu3dDraw::Packet(const Imx51Gpu3dPacket& packet, + std::unordered_map& registers, uint32_t mmu) { + using namespace imx51_gpu3d_regs; + switch (packet.opcode) { + case kPm4OpImLoad: case kPm4OpImLoadImmediate: Load(packet, mmu); return; + case kPm4OpImStore: Store(packet, mmu); return; + /* NXP linux-imx a1638da9, gsl_drawctxt.c:1233-1240; yamato_registers.h: SQ_INST_STORE_MANAGMENT. */ + case kPm4OpSetShaderBases: { + const uint32_t value = Operand(packet, 0, mmu); + if (packet.payload_count != 1u || (value & 0xF000F000u) != 0x80000000u || + (value & 0xFFFu) >= 512u || ((value >> 16) & 0xFFFu) >= 512u) + Reject("shader partition", value); + bases_ = value; + registers[kIdxSqInstStoreManagment] = value & 0x0FFF0FFFu; + return; + } + /* NXP linux-imx a1638da9, gsl_pm4types.h: PM4_SET_BIN_BASE_OFFSET. */ + case 0x4Bu: + if (packet.payload_count != 1u) Reject("bin base payload", packet.header); + bin_base_ = Operand(packet, 0, mmu); return; + case kPm4OpDrawIndx: case 0x34u: Draw(packet, registers, mmu); return; + default: Reject("unsupported packet", packet.header); + } +} + +void Imx51Gpu3dDraw::SaveState(StateWriter& writer) { + writer.Write(instructions_); writer.Write(valid_); writer.Write(start_size_); + writer.Write(bases_); writer.Write(bin_base_); +} +void Imx51Gpu3dDraw::RestoreState(StateReader& reader) { + reader.Read(instructions_); reader.Read(valid_); reader.Read(start_size_); + reader.Read(bases_); reader.Read(bin_base_); +} + +/* Mesa e97ad748, fd2_gmem.c:591-600,609-635; fd2_util.c: fd2_pipe2color; + NXP linux-imx a1638da9, yamato_registers.h: VGT_CURRENT_BIN_ID_MIN/MAX. */ +void Imx51Gpu3dDraw::Export(const Imx51Gpu3dShaderState& state, uint32_t mmu) { + for (const auto& output : state.memory_exports) { + const uint32_t x = std::bit_cast(output.address[0]); + const uint32_t z = std::bit_cast(output.address[2]); + const uint32_t w = std::bit_cast(output.address[3]); + const float index = output.address[1]; + if ((x & 0xC0000000u) != 0x40000000u || z != 0x4B00D000u || + (w & 0xFF800000u) != 0x4B000000u || !std::isfinite(index) || + index < 0 || index >= static_cast(w & 0x7FFFFFu) || std::floor(index) != index) + Reject("unsupported memory export descriptor/index", z); + std::array component{}; + for (uint32_t i = 0; i < component.size(); ++i) { + if (!std::isfinite(output.data[i])) Reject("nonfinite bin coordinate", i); + component[i] = static_cast(std::clamp(output.data[i], 0.0f, 1.0f) * 255.0f); + } + const uint64_t address = uint64_t(x & 0x3FFFFFFFu) * 4u + static_cast(index); + auto* destination = emu_.Get().WriteSpan(address, 1u, mmu); + *destination = static_cast((component[0] >> 5) | ((component[1] >> 5) << 3) | + ((component[2] >> 6) << 6)); + } +} + +/* NXP linux-imx a1638da9, yamato_registers.h: VGT_DRAW_INITIATOR/VGT_DMA_SIZE; + Mesa e97ad748, freedreno_draw.h: fd_draw/fd_draw_emit; fd2_draw.c: draw_impl. */ +void Imx51Gpu3dDraw::Draw(const Imx51Gpu3dPacket& packet, + const std::unordered_map& registers, uint32_t mmu) { + const uint32_t control = Operand(packet, 1, mmu), primitive = control & 0x3Fu; + const uint32_t source = (control >> 6) & 3u, count = control >> 16; + const bool bin = packet.opcode == 0x34u; + const uint32_t width = (control & 0x800u) ? 4u : 2u, dma_operand = bin ? 4u : 2u; + const uint32_t query = Operand(packet, 0, mmu); + /* NXP a1638da9 PA_SU_SC_MODE_CNTL: reject face producers before shader exports, + zero-count/degenerate returns, or raster work. FACE_KILL alone is a binning pass. */ + const auto face = registers.find(0x2205u); + if (face == registers.end() || (face->second & 0xB0000000u)) + Reject("unsupported face production/modifiers", face == registers.end() ? UINT32_MAX : face->second); + if (query != 0u || (control & 0x3700u) != 0u || + (primitive != 4u && primitive != 6u && primitive != 8u) || (source != 0u && source != 2u) || + (primitive == 8u && (bin || count != 3u)) || + (!bin && (control & 0xC000u)) || (bin && (control & 0xC000u) != 0xC000u) || + packet.payload_count != dma_operand + (source == 0u ? 2u : 0u)) + Reject("unsupported draw flags/primitive/extent", control); + auto& memory = emu_.Get(); + const uint8_t* indices = nullptr; + const uint8_t* bin_bytes = nullptr; + if (source == 0u) { + const uint32_t address = Operand(packet, dma_operand, mmu); + const uint32_t size = Operand(packet, dma_operand + 1u, mmu); + if ((address % width) || (size & 0xFF000000u) || uint64_t(count) * width > size) + Reject("index buffer extent/swap", size); + if (size) indices = memory.ReadSpan(address, size, mmu); + } + if (bin) { + const uint32_t offset = Operand(packet, 2, mmu), size = Operand(packet, 3, mmu); + /* NXP linux-imx a1638da9, yamato_registers.h:2274-2292, VGT_BIN_SIZE. */ + const uint32_t extent = size & 0x00FFFFFFu, reserved = size & 0x3F000000u; + const bool fetch = (size & 0x40000000u) != 0; + const char* invalid = reserved ? "bin buffer reserved bits" : extent < count ? "bin buffer extent" : + fetch ? "unsupported bin faceness fetch" : nullptr; + if (invalid) Reject(invalid, size); + /* Model RESET within the excluded face subsystem: all face writers and + FETCH/cull consumers are rejected, including the C2D path. No face cursor + is observable in this subset. See docs/gpu_bin_draws.md. */ + if (extent) bin_bytes = memory.ReadSpan(uint64_t(bin_base_) + offset, extent, mmu); + } + std::vector fetched(count); + for (uint32_t i = 0; i < count; ++i) { + fetched[i] = i; + if (indices) { + fetched[i] = 0; + for (uint32_t byte = 0; byte < width; ++byte) + fetched[i] |= uint32_t(indices[uint64_t(i) * width + byte]) << (byte * 8u); + } + } + /* NXP linux-imx a1638da9, gsl_yamato.c:275-300; Mesa e97ad748, fd2_draw.c:75-105. */ + if (bin && primitive == 4u && count == 3u && fetched[0] == fetched[1] && fetched[1] == fetched[2]) return; + std::vector visible(count, !bin), needed(count, !bin); + if (bin) { + /* Mesa e97ad748 fd2_gmem.c:609-630; physical SYNC 2 B023_00 cases 6/9/10. + XY bins use the same vertex triplets as list/strip assembly below. */ + const auto low = registers.find(0x2207u), high = registers.find(0x2203u); + if (primitive == 4u && count % 3u) Reject("unsupported bin primitive/count", control); + if (low == registers.end() || high == registers.end()) Reject("missing bin bounds", packet.address); + if ((low->second | high->second) & ~0x3Fu) Reject("unsupported bin guard band", low->second); + for (unsigned shift : {0u, 3u}) + if (((low->second >> shift) & 7u) > ((high->second >> shift) & 7u)) + Reject("reversed bin bounds", low->second); + for (uint32_t i = 0; i < count; ++i) + if ((bin_bytes[i] >> 6) != 1u) Reject("unsupported bin Z code", bin_bytes[i]); + for (uint32_t i = 2; i < count; i += primitive == 4u ? 3u : 1u) { + bool overlap = true; + for (unsigned shift : {0u, 3u}) { + const unsigned a = (bin_bytes[i-2] >> shift) & 7u; + const unsigned b = (bin_bytes[i-1] >> shift) & 7u; + const unsigned c = (bin_bytes[i] >> shift) & 7u; + overlap &= std::max({a,b,c}) >= ((low->second >> shift) & 7u) && + std::min({a,b,c}) <= ((high->second >> shift) & 7u); + } + visible[i] = overlap; + if (overlap) needed[i-2] = needed[i-1] = needed[i] = 1; + } + if (std::none_of(visible.begin(), visible.end(), [](uint8_t v) { return v != 0; })) return; + } + /* NXP linux-imx a1638da9, yamato_registers.h: VGT_INDX_OFFSET; Mesa e97ad748, fd2_draw.c:75-76. */ + const auto offset_reg = registers.find(0x2102u); + if (offset_reg == registers.end() || (offset_reg->second & 0xFF000000u)) + Reject("unsupported index offset", offset_reg == registers.end() ? UINT32_MAX : offset_reg->second); + const auto vertex_program = Program(0), pixel_program = Program(1); + if (count && vertex_program.empty()) Reject("vertex shader not loaded", packet.address); + std::vector vertices(count); + for (uint32_t i = 0; i < count; ++i) { + if (!needed[i]) continue; + const uint64_t effective = uint64_t(fetched[i]) + offset_reg->second; + if (effective > 0xFFFFFFu) Reject("unsupported vertex index arithmetic", effective); + const uint32_t index = static_cast(effective); + if (index > 0xFFFFFFu) Reject("unsupported vertex index width", index); + vertices[i].registers[0][0] = static_cast(index); + /* Mesa e97ad748, fd2_program.c: GEN_INDEX_VTX; ir2_nir.c: binning index input 2. */ + const auto program_control = registers.find(0x2180u); + if (program_control != registers.end() && (program_control->second & 0x80000000u)) + vertices[i].registers[2][0] = static_cast(i); + emu_.Get().Run(vertex_program, false, registers, mmu, vertices[i]); + if (bin && !vertices[i].memory_exports.empty()) Reject("unsupported bin replay memory exports", packet.address); + Export(vertices[i], mmu); + if (!(vertices[i].export_mask & (uint64_t{1} << 62))) Reject("vertex position not exported", i); + } + /* NXP linux-imx a1638da9, gsl_drawctxt.c:389-394,806-810,1107-1121. */ + if (primitive == 8u) { + const auto& a = vertices[0].exports[62]; + const auto& b = vertices[1].exports[62]; + const auto& c = vertices[2].exports[62]; + if (a[1] != b[1] || a[0] != c[0] || a[3] != b[3] || a[3] != c[3]) + Reject("unsupported rectangle alignment/perspective", packet.address); + Imx51Gpu3dShaderState fourth; + fourth.export_mask = vertices[0].export_mask & vertices[1].export_mask & vertices[2].export_mask; + for (uint32_t slot = 0; slot < fourth.exports.size(); ++slot) + if (fourth.export_mask & (uint64_t{1} << slot)) + for (uint32_t component = 0; component < 4; ++component) + fourth.exports[slot][component] = vertices[1].exports[slot][component] + + vertices[2].exports[slot][component] - vertices[0].exports[slot][component]; + emu_.Get().Triangle({vertices[0],vertices[1],vertices[2]}, registers, pixel_program, mmu); + emu_.Get().Triangle({vertices[2],vertices[1],fourth}, registers, pixel_program, mmu); + return; + } + for (uint32_t i = 2; i < count; i += primitive == 4u ? 3u : 1u) { + if (!visible[i]) continue; + const uint32_t a = primitive == 6u && (i & 1u) ? i - 1u : i - 2u; + const uint32_t b = primitive == 6u && (i & 1u) ? i - 2u : i - 1u; + const std::array triangle{vertices[a], vertices[b], vertices[i]}; + emu_.Get().Triangle(triangle, registers, pixel_program, mmu); + } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_draw.h b/cerf/socs/imx51/imx51_gpu3d_draw.h new file mode 100644 index 00000000..041a3cf3 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_draw.h @@ -0,0 +1,36 @@ +#pragma once +#include "../../core/service.h" +#include "imx51_gpu3d_packet.h" +#include +#include +#include + +class StateWriter; +class StateReader; +struct Imx51Gpu3dShaderState; + +class Imx51Gpu3dDraw : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + void Packet(const Imx51Gpu3dPacket& packet, + std::unordered_map& registers, uint32_t mmu_config); + void SaveState(StateWriter& writer); + void RestoreState(StateReader& reader); + +private: + uint32_t Operand(const Imx51Gpu3dPacket& packet, uint32_t index, uint32_t mmu); + uint32_t InstructionOffset(uint32_t stage, uint32_t start, uint32_t count); + std::span Program(uint32_t stage); + void Load(const Imx51Gpu3dPacket& packet, uint32_t mmu); + void Store(const Imx51Gpu3dPacket& packet, uint32_t mmu); + void Export(const Imx51Gpu3dShaderState& state, uint32_t mmu); + void Draw(const Imx51Gpu3dPacket& packet, + const std::unordered_map& registers, uint32_t mmu); + [[noreturn]] void Reject(const char* reason, uint64_t value); + std::array, 2> instructions_{}; + std::array, 2> valid_{}; + std::array start_size_{}; + uint32_t bases_ = 0; + uint32_t bin_base_ = 0; +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_memory.cpp b/cerf/socs/imx51/imx51_gpu3d_memory.cpp new file mode 100644 index 00000000..bd193f23 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_memory.cpp @@ -0,0 +1,43 @@ +#include "imx51_gpu3d_memory.h" +#include "imx51_gpu3d_regs.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../cpu/emulated_memory.h" +#include "../../boards/board_context.h" + +REGISTER_SERVICE(Imx51Gpu3dMemory); + +bool Imx51Gpu3dMemory::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} + +uint8_t* Imx51Gpu3dMemory::ReadSpan(uint64_t pa, uint64_t size, uint32_t mmu_config) { + return MemorySpan(pa, size, false, mmu_config); +} +uint8_t* Imx51Gpu3dMemory::WriteSpan(uint64_t pa, uint64_t size, uint32_t mmu_config) { + return MemorySpan(pa, size, true, mmu_config); +} + +/* NXP linux-imx a1638da9, gsl_mmu.c:506-526; Mesa e97ad748 a2xx.xml:1042, BEH_NEVR. */ +uint8_t* Imx51Gpu3dMemory::MemorySpan(uint64_t pa, uint64_t size, bool write, uint32_t config) { + if ((config & 1u) && config != 1u) + emu_.Get().Die("GPU memory rejected MMU configuration at 0x%08X (value 0x%016llX)", + imx51_gpu3d_regs::kBase + imx51_gpu3d_regs::kIdxMhMmuConfig * 4u, + static_cast(config)); + uint8_t* p = emu_.Get().TryTranslateRange(pa, size, write); + if (!p) emu_.Get().Die("GPU memory rejected %s at 0x%08X (size 0x%016llX)", + write ? "write range" : "read range", static_cast(pa), + static_cast(size)); + return p; +} + +uint32_t Imx51Gpu3dMemory::ReadPa32(uint64_t pa, uint32_t mmu_config) { + const uint8_t* p = ReadSpan(pa, 4u, mmu_config); + return uint32_t(p[0]) | (uint32_t(p[1]) << 8) | + (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24); +} +void Imx51Gpu3dMemory::WritePa32(uint64_t pa, uint32_t value, uint32_t mmu_config) { + uint8_t* p = WriteSpan(pa, 4u, mmu_config); + for (unsigned i = 0; i < 4; ++i) p[i] = static_cast(value >> (i * 8)); +} diff --git a/cerf/socs/imx51/imx51_gpu3d_memory.h b/cerf/socs/imx51/imx51_gpu3d_memory.h new file mode 100644 index 00000000..dc338bb5 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_memory.h @@ -0,0 +1,16 @@ +#pragma once +#include "../../core/service.h" +#include + +class Imx51Gpu3dMemory : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + uint8_t* ReadSpan(uint64_t pa, uint64_t size, uint32_t mmu_config); + uint8_t* WriteSpan(uint64_t pa, uint64_t size, uint32_t mmu_config); + uint32_t ReadPa32(uint64_t pa, uint32_t mmu_config); + void WritePa32(uint64_t pa, uint32_t value, uint32_t mmu_config); + +private: + uint8_t* MemorySpan(uint64_t pa, uint64_t size, bool write, uint32_t mmu_config); +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_packet.cpp b/cerf/socs/imx51/imx51_gpu3d_packet.cpp new file mode 100644 index 00000000..2d72bf48 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_packet.cpp @@ -0,0 +1,54 @@ +#include "imx51_gpu3d_packet.h" +#include "imx51_gpu3d_regs.h" + +/* NXP linux-imx a1638da9d9fda588979360b33390cf612a256829, + drivers/mxc/amd-gpu/include/api/gsl_pm4types.h: packet macros and header types. */ +const char* Imx51Gpu3dPacket::Decode(uint32_t header, uint64_t address, + uint32_t available_dwords, + Imx51Gpu3dPacketSource source, + Imx51Gpu3dPacket& packet) { + using namespace imx51_gpu3d_regs; + packet = {}; + packet.address = address; + packet.header = header; + packet.source = source; + packet.type = header >> 30; + if (available_dwords == 0 || address > UINT32_MAX || (address & 3u) != 0) + return "PM4 malformed header address/extent"; + if (packet.type == 1u) + return "PM4 unsupported TYPE1"; + if (packet.type == kPm4Type2) + return nullptr; + packet.payload_count = ((header >> 16) & 0x3FFFu) + 1u; + packet.same_register = packet.type == kPm4Type0 && (header & 0x8000u) != 0; + if (packet.type == kPm4Type3) { + packet.opcode = (header >> 8) & 0xFFu; + packet.predicate = (header & 1u) != 0; + if (packet.predicate) + return "PM4 unsupported predicate"; + if ((header & 0x80FEu) != 0) + return "PM4 unsupported reserved header flags"; + /* NXP linux-imx a1638da9, gsl_drawctxt.c:1237, shader partition fixup. */ + if (packet.opcode == kPm4OpRegRmw && packet.payload_count < 3u) + return "PM4 malformed REG_RMW payload"; + /* sync_2 EA5T-14D544-BA.sec, lib2d-z430.dll: 0x41A62890, literal 0x41A62900. */ + if (packet.opcode == kPm4OpWaitRegEq && packet.payload_count < 4u) + return "PM4 malformed WAIT_REG_EQ payload"; + } + if (address + uint64_t(packet.payload_count) * 4u > UINT32_MAX) + return "PM4 malformed packet address overflow"; + /* NXP linux-imx a1638da9d9fda588979360b33390cf612a256829, + drivers/mxc/amd-gpu/common/gsl_ringbuffer.c: kgsl_ringbuffer_waitspace. */ + const bool ring_nop = source == Imx51Gpu3dPacketSource::Ring && + packet.type == kPm4Type3 && packet.opcode == kPm4OpNop; + if (packet.payload_count >= available_dwords && !ring_nop) + return "PM4 malformed truncated payload"; + return nullptr; +} + +bool Imx51Gpu3dPacket::OperandAddress(uint32_t index, uint64_t& operand_address) const { + if (index >= payload_count) + return false; + operand_address = address + (uint64_t(index) + 1u) * 4u; + return operand_address <= UINT32_MAX; +} diff --git a/cerf/socs/imx51/imx51_gpu3d_packet.h b/cerf/socs/imx51/imx51_gpu3d_packet.h new file mode 100644 index 00000000..10d8a504 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_packet.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +enum class Imx51Gpu3dPacketSource { Ring, IndirectBuffer }; + +struct Imx51Gpu3dPacket { + uint64_t address = 0; + uint32_t header = 0; + uint32_t type = 0; + uint32_t opcode = 0; + uint32_t payload_count = 0; + bool same_register = false; + bool predicate = false; + Imx51Gpu3dPacketSource source = Imx51Gpu3dPacketSource::Ring; + + static const char* Decode(uint32_t header, uint64_t address, uint32_t available_dwords, + Imx51Gpu3dPacketSource source, Imx51Gpu3dPacket& packet); + bool OperandAddress(uint32_t index, uint64_t& address) const; +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_raster.cpp b/cerf/socs/imx51/imx51_gpu3d_raster.cpp new file mode 100644 index 00000000..30482bb8 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_raster.cpp @@ -0,0 +1,413 @@ +#include "imx51_gpu3d_raster.h" +#include "imx51_gpu3d_memory.h" +#include "imx51_gpu3d_tiling.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../state/state_stream.h" +#include "../../boards/board_context.h" +#include +#include +#include +#include + +/* ImageMagick config/thresholds.xml: o4x4 ordered thresholds, divisor 17. */ +static uint32_t ApproximateDitherQuantize(float channel, uint32_t maximum, unsigned component, + uint32_t x, uint32_t y, bool enabled) { + const float scaled = channel * maximum; + if (!enabled || component == 3u) return static_cast(std::lround(scaled)); + constexpr uint8_t thresholds[4][4] = {{1,9,3,11},{13,5,15,7},{4,12,2,10},{16,8,14,6}}; + const double threshold = thresholds[y & 3u][x & 3u] / 17.0; + return static_cast(std::clamp(std::floor(scaled + 1.0 - threshold),0.0,double(maximum))); +} + +REGISTER_SERVICE(Imx51Gpu3dRaster); +bool Imx51Gpu3dRaster::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} +void Imx51Gpu3dRaster::SaveState(StateWriter& writer) { + writer.Write(gmem_binding_); writer.Write(gmem_pitch_); + writer.WriteBytes(gmem_.data(),gmem_.size()); +} +void Imx51Gpu3dRaster::RestoreState(StateReader& reader) { + reader.Read(gmem_binding_); reader.Read(gmem_pitch_); + reader.ReadBytes(gmem_.data(),gmem_.size()); + if (gmem_binding_ != 0xFFFFFFFFu && + ((gmem_binding_ & 0xFF0u) != 0u || (gmem_binding_ & 0xFFFFF000u) >= gmem_.size() || gmem_pitch_ == 0 || gmem_pitch_ > 16383u || + ((gmem_binding_ & 15u) != 0u && (gmem_binding_ & 15u) != 2u && (gmem_binding_ & 15u) != 5u))) + emu_.Get().Die("GPU raster invalid saved GMEM binding"); +} + +/* Khronos OpenGL ES 2.0.25 sections 2.13 and 2.13.1. */ +void Imx51Gpu3dRaster::Triangle(const std::array& vertices, + const std::unordered_map& registers, + std::span pixel_program, uint32_t mmu_config) { + RasterWrites writes{{},gmem_binding_,gmem_pitch_}; + const auto clip = registers.find(0x2204), vte = registers.find(0x2206), raster = registers.find(0x2205); + auto fail = [&](const char* reason, uint32_t value) { + emu_.Get().Die("GPU raster unsupported %s value=%08X",reason,value); + }; + if (clip != registers.end() && clip->second != 0u && clip->second != 0x10000u) fail("clip controls",clip->second); + bool outside = false; + const bool enabled = clip != registers.end() && clip->second == 0u && vte != registers.end() && + (vte->second == 0x43Fu || vte->second == 0x40Fu || vte->second == 0x30Fu) && + raster != registers.end() && (raster->second & 0xC0000000u) != 0x40000000u; + if (enabled) for (unsigned i = 0; i < 3u; ++i) { + const auto& p = vertices[i].exports[62]; + if (!(vertices[i].export_mask & (uint64_t{1} << 62))) fail("missing position",i); + for (float component : p) if (!std::isfinite(component)) fail("homogeneous clipping",i); + if (p[3] <= 0.0f) fail("homogeneous clipping",i); + if (vte->second == 0x30Fu && p[3] != 1.0f) fail("premultiplied nonunit W",i); + outside |= std::abs(p[0]) > p[3] || std::abs(p[1]) > p[3] || std::abs(p[2]) > p[3]; + } + if (!outside) RasterizeTriangle(vertices,vertices,registers,pixel_program,mmu_config,writes); + else { + if (raster->second & 0xC000B818u) fail("MSAA/polygon/faceness",raster->second); + std::vector polygon(vertices.begin(),vertices.end()), next; + for (unsigned plane = 0; plane < 6u && !polygon.empty(); ++plane) { + next.clear(); + const unsigned axis = plane / 2u; + const double sign = (plane & 1u) ? -1.0 : 1.0; + auto distance = [&](const Imx51Gpu3dShaderState& v) { return double(v.exports[62][3]) + sign * v.exports[62][axis]; }; + for (size_t i = 0; i < polygon.size(); ++i) { + const auto& a = polygon[i]; const auto& b = polygon[(i+1u)%polygon.size()]; + const double da = distance(a), db = distance(b); + if (da >= 0.0) next.push_back(a); + if ((da < 0.0) == (db < 0.0)) continue; + const auto& inside = da >= 0.0 ? a : b; const auto& out = da < 0.0 ? a : b; + const double di = da >= 0.0 ? da : db, dout = da < 0.0 ? da : db; + const double t = di / (di-dout); + Imx51Gpu3dShaderState intersection{}; + intersection.export_mask = inside.export_mask & out.export_mask; + /* Khronos EXT_gpu_shader4 issue 10: window-linear varying clipping. */ + const double varying_t = (raster->second & 0x100000u) ? t*out.exports[62][3] / + ((1.0-t)*inside.exports[62][3]+t*out.exports[62][3]) : t; + for (unsigned slot = 0; slot < 64u; ++slot) if (intersection.export_mask & (uint64_t{1} << slot)) { + const double fraction = slot == 62u ? t : varying_t; + for (unsigned c = 0; c < 4u; ++c) + intersection.exports[slot][c] = static_cast((1.0-fraction)*inside.exports[slot][c]+fraction*out.exports[slot][c]); + } + intersection.exports[62][axis] = static_cast(-sign * intersection.exports[62][3]); + next.push_back(std::move(intersection)); + } + polygon.swap(next); + } + for (size_t i = 1; i+1 < polygon.size(); ++i) + RasterizeTriangle({polygon[0],polygon[i],polygon[i+1]},vertices,registers,pixel_program,mmu_config,writes); + } + for (const auto& pixel : writes.pixels) for (unsigned i = 0; i < pixel.bytes; ++i) pixel.target[i] = pixel.data[i]; + gmem_binding_ = writes.binding; gmem_pitch_ = writes.pitch; +} + +/* Mesa e97ad748 a2xx.xml: PA_CL_VTE_CNTL, PA_SU_VTX_CNTL, RB_COLOR_INFO; + fd2_gmem.c: fmt2swap, fd2_emit_sysmem_prep, fd2_emit_tile_renderprep. */ +void Imx51Gpu3dRaster::RasterizeTriangle(const std::array& vertices, + const std::array& depth_vertices, + const std::unordered_map& registers, + std::span pixel_program, uint32_t mmu_config, RasterWrites& writes) { + auto fail = [&](const char* reason, uint32_t value) { + emu_.Get().Die("GPU raster unsupported %s value=%08X",reason,value); + }; + for (const auto& vertex : vertices) + if ((vertex.export_mask & (uint64_t{1} << 62)) == 0) fail("missing position",0); + const auto& p0 = vertices[0].exports[62]; + const auto& p1 = vertices[1].exports[62]; + const auto& p2 = vertices[2].exports[62]; + if (p0 == p1 || p1 == p2 || p2 == p0) return; + auto reg = [&](uint32_t index) { + const auto i = registers.find(index); + if (i == registers.end()) fail("missing register",index); + return i->second; + }; + const uint32_t raster = reg(0x2205); + /* Mesa e97ad748 fd2_emit.c:169-215, fd2_emit_state_binning; + NXP a1638da9 yamato_registers.h:400, FACE_KILL_ENABLE. */ + if ((raster & 0xC0000000u) == 0x40000000u) return; + const uint32_t vte = reg(0x2206), control = reg(0x2202); + const uint32_t clip = reg(0x2204); + if (clip != 0u && clip != 0x10000u) fail("clip controls",clip); + /* Mesa e97ad748 a2xx.xml:1491-1502, PA_CL_VTE_CNTL. */ + if (vte != 0x43Fu && vte != 0x40Fu && vte != 0xB00u && vte != 0x30Fu) fail("viewport format",vte); + if ((raster & 0xC000B818u) != 0) fail("MSAA/polygon/faceness",raster); + /* NXP a1638da9 yamato_registers.h: RB_DEPTHCONTROL; yamato_enum.h: CompareFrag. */ + const uint32_t depth_control = reg(0x2200); + /* Mesa e97ad748 fd2_zsa.c:38-70; NXP a1638da9 yamato_registers.h: RB_DEPTHCONTROL.STENCIL_ENABLE. */ + if (depth_control & 1u) fail("depth/stencil",depth_control); + const bool depth_enabled = (depth_control & 2u) != 0; + const bool depth_write = depth_enabled && (depth_control & 4u) != 0; + /* NXP a1638da9 yamato_enum.h:1603-1617; Mesa e97ad748 a2xx.xml:1440-1449. */ + const uint32_t dither_mode = (control >> 12) & 3u; + const bool blend = (control & 0x20u) == 0; + const auto blend_register = registers.find(0x2201u); + /* NXP a1638da9 BlendOpX/CombFuncX; Navigation 20260906_222205 RB_BLEND_CONTROL=07060706. */ + const bool measured_blend = blend && (control & ~0x3007u) == 0xC00u && + blend_register != registers.end() && blend_register->second == 0x07060706u; + if (!measured_blend && (control & ~0x3007u) != 0x20u && (control & ~0x3007u) != 0xC20u) + fail("blend/alpha/ROP/dither type",control); + if (dither_mode == 3u) fail("dither mode",control); + const uint32_t mode = reg(0x2208); + if (mode != 4u && mode != 6u) fail("render mode",mode); + const bool resolve = mode == 6u; + if (resolve && depth_enabled) fail("resolve depth",depth_control); + /* NXP a1638da9 gsl_drawctxt.c:960-989, build_sys2gmem_cmds: VTE=B00, mode=4. */ + if (vte == 0xB00u && !resolve && clip != 0x10000u) fail("window-space clipping",clip); + const uint32_t info = reg(0x2001), surface = reg(0x2000), format = info & 15u; + if ((surface & ~0x3FFFu) != 0 || (surface & 0x3FFFu) == 0) fail("surface/MSAA",surface); + if (format != 0u && format != 2u && format != 5u) fail("color format",format); + if (blend && (resolve || format != 2u)) fail("blend target/resolve",info); + const uint32_t swap = (info >> 9) & 3u; + /* Ford SYNC 2 librenderboy.dll: 0x41CDB4B0-0x41CDB500 (format/swap), + 0x41CDBF4C-0x41CDBF90; libGLESv2.dll: 0x41BEE2FC (RGBA4444). */ + if ((info & 0x180u) != 0 || (swap > 1u && !(format == 0u && swap == 3u))) fail("endian/swap",info); + const bool nonlinear = (info & 0x40u) == 0; + /* NXP a1638da9 gsl_yamato.c:36-56, mapping_mode=0, range=gpu_base>>14. */ + if (nonlinear && reg(0xF02) != 3u) fail("GMEM configuration",reg(0xF02)); + const bool gmem = nonlinear && (info & 0xFFFFF000u) < gmem_.size(); + const bool tiled = nonlinear && !gmem; + if (tiled && (surface & 31u)) fail("tiled target pitch",surface); + const uint32_t binding = info & 0xFFFFF00Fu; + if (gmem) { + if (swap == 3u) fail("GMEM swap",info); + if ((mmu_config & 1u) && mmu_config != 1u) fail("GMEM MMU mode",mmu_config); + if (gmem_binding_ != 0xFFFFFFFFu && (gmem_binding_ != binding || gmem_pitch_ != (surface & 0x3FFFu))) + fail("GMEM format/pitch/base reinterpretation",info); + } + if (resolve && (!gmem || gmem_binding_ == 0xFFFFFFFFu)) fail("uninitialized resolve source",info); + const uint32_t pitch = surface & 0x3FFFu, bytes = format == 5u ? 4u : 2u; + /* NXP a1638da9 yamato_registers.h: RB_DEPTH_INFO; yamato_enum.h: DEPTHX_16; + Ford SYNC 2 librenderboy.dll: 0x41CDA5A4, 0x41CDB6E8-0x41CDB778. */ + const uint32_t depth_info = depth_enabled ? reg(0x2002) : 0; + const uint32_t depth_base = depth_info & 0xFFFFF000u; + if (depth_enabled && (!gmem || bytes != 2u || (depth_info & 0xFFFu) != 0 || + depth_base >= gmem_.size() || depth_base % (pitch * 2u) != 0)) + fail("depth attachment",depth_info); + uint32_t color_mask = reg(0x2104), target_info = info, target_pitch = pitch, offset_x = 0, offset_y = 0; + uint32_t target_base = info & 0xFFFFF000u; + /* NXP a1638da9 gsl_drawctxt.c:735-819, build_gmem2sys_cmds; + Mesa e97ad748 fd2_gmem.c:70-112, emit_gmem2mem_surf. */ + if (resolve) { + const uint32_t copy = reg(0x231B), offset = reg(0x231C); + const uint32_t copy_control = reg(0x2318); + const char* invalid = copy_control != 0u ? "resolve sample/clear control" : + (copy & 7u) ? "resolve destination endian" : !(copy & 8u) ? "resolve tiled destination" : + ((copy >> 4) & 15u) != format ? "resolve format conversion" : + ((copy >> 8) & 3u) > 1u ? "resolve destination swap" : + ((info >> 9) & 3u) > 1u ? "resolve source swap" : + (copy & 0xFFFFFC00u & ~0x3C000u) ? "resolve destination dither/reserved" : nullptr; + if (invalid) fail(invalid,copy); + if (offset & 0xFC000000u) fail("resolve offset",offset); + target_base = reg(0x2319); target_pitch = reg(0x231A) * 32u; + if ((target_base & 4095u) || reg(0x231A) > 511u || !target_pitch) fail("resolve destination",target_base); + color_mask = (copy >> 14) & 15u; target_info = (copy & 0x300u) << 1; + offset_x = offset & 8191u; offset_y = (offset >> 13) & 8191u; + } + if ((color_mask & ~15u) != 0) fail("color mask",color_mask); + const uint32_t vtx = reg(0x2302); + if (vtx != 5u) fail("pixel center/quantization",vtx); + struct Point { double x, y, inverse_w, z; }; + std::array points{}; + /* NXP a1638da9 yamato_registers.h: PA_CL_VTE_CNTL; + Ford SYNC 2 librenderboy.dll: 0x41CD2628-0x41CD2648. */ + auto window_depth = [&](const Imx51Gpu3dVec4& p) { + double z = p[2] * ((vte & 0x200u) ? 1.0 : 1.0 / p[3]); + if (vte & 0x10u) z *= std::bit_cast(reg(0x2113)); + if (vte & 0x20u) z += std::bit_cast(reg(0x2114)); + return z; + }; + const double original_depth = depth_enabled ? window_depth(depth_vertices[0].exports[62]) : 0.0; + const bool constant_depth = !depth_enabled || + (original_depth == window_depth(depth_vertices[1].exports[62]) && + original_depth == window_depth(depth_vertices[2].exports[62])); + for (unsigned i = 0; i < 3; ++i) { + const auto& p = vertices[i].exports[62]; + if (!std::isfinite(p[0]) || !std::isfinite(p[1]) || !std::isfinite(p[2]) || + !std::isfinite(p[3]) || (vte != 0xB00u && p[3] <= 0.0f)) fail("homogeneous clipping",i); + /* NXP a1638da9 gsl_drawctxt.c:731, premultiplied XY/Z; native VTE=30F, W=1. */ + if (vte == 0x30Fu && p[3] != 1.0f) fail("premultiplied nonunit W",i); + if (vte == 0xB00u && !resolve && p[3] != 1.0f) fail("window-space nonunit W",i); + const double inverse_w = vte == 0xB00u ? 1.0 : vte == 0x30Fu ? 1.0 : 1.0 / p[3]; + const double xy_scale = (vte == 0x43Fu || vte == 0x40Fu) ? inverse_w : 1.0; + const double x = vte == 0xB00u ? p[0] : p[0] * xy_scale * std::bit_cast(reg(0x210F)) + std::bit_cast(reg(0x2110)); + const double y = vte == 0xB00u ? p[1] : p[1] * xy_scale * std::bit_cast(reg(0x2111)) + std::bit_cast(reg(0x2112)); + if (!std::isfinite(x) || !std::isfinite(y) || std::abs(x) > 32768 || std::abs(y) > 32768) + fail("viewport coordinate range",i); + double z = 0; + if (depth_enabled) { + z = constant_depth ? original_depth : window_depth(p); + if (!std::isfinite(z) || z < 0.0 || z > 1.0) + fail("out-of-range depth",std::bit_cast(static_cast(z))); + + } + points[i] = {std::nearbyint(x * 16.0) / 16.0,std::nearbyint(y * 16.0) / 16.0,inverse_w,z}; + } + auto edge = [](const Point& a, const Point& b, double x, double y) { + return (b.x-a.x)*(y-a.y)-(b.y-a.y)*(x-a.x); + }; + double area = edge(points[0],points[1],points[2].x,points[2].y); + if (area == 0.0) return; + /* NXP a1638da9 yamato_registers.h:373-377, PA_SU_SC_MODE_CNTL; + Khronos OpenGL ES 2.0 section 3.5.1, polygon rasterization. */ + const bool front = (area > 0.0) == ((raster & 4u) != 0); + if (raster & (front ? 1u : 2u)) return; + const double sign = area < 0.0 ? -1.0 : 1.0; + area *= sign; + const uint32_t offset = reg(0x2080), window_tl = reg(0x2081), window_br = reg(0x2082); + auto signed15 = [](uint32_t x) { return static_cast((x & 0x7FFFu) ^ 0x4000u) - 0x4000; }; + const int ox = (raster & 0x10000u) ? signed15(offset) : 0; + const int oy = (raster & 0x10000u) ? signed15(offset >> 16) : 0; + for (auto& p : points) { p.x += ox; p.y += oy; } + const int wx = (window_tl & 0x80000000u) ? 0 : ox, wy = (window_tl & 0x80000000u) ? 0 : oy; + const uint32_t screen_tl = reg(0x200E), screen_br = reg(0x200F); + int left = (std::max)(int(screen_tl & 0x7FFFu),int(window_tl & 0x7FFFu)+wx); + int top = (std::max)(int((screen_tl >> 16) & 0x7FFFu),int((window_tl >> 16) & 0x7FFFu)+wy); + int right = (std::min)(int(screen_br & 0x7FFFu),int(window_br & 0x7FFFu)+wx); + int bottom = (std::min)(int((screen_br >> 16) & 0x7FFFu),int((window_br >> 16) & 0x7FFFu)+wy); + left = (std::max)(left,int(std::floor((std::min)({points[0].x,points[1].x,points[2].x})))); + top = (std::max)(top,int(std::floor((std::min)({points[0].y,points[1].y,points[2].y})))); + right = (std::min)(right,int(std::ceil((std::max)({points[0].x,points[1].x,points[2].x})))); + bottom = (std::min)(bottom,int(std::ceil((std::max)({points[0].y,points[1].y,points[2].y})))); + if (left >= right || top >= bottom || (color_mask == 0 && !depth_write)) return; + if (left < 0 || top < 0 || right > static_cast(pitch)) fail("target bounds",pitch); + if (!resolve && pixel_program.empty()) fail("missing pixel shader",0); + const uint64_t extent = (uint64_t(bottom-1)*pitch+right)*bytes; + const uint64_t base = info & 0xFFFFF000u; + if (gmem && base+extent > gmem_.size()) fail("GMEM capacity",static_cast(base)); + if (depth_enabled && uint64_t(depth_base)+(uint64_t(bottom-1)*pitch+right)*2u > gmem_.size()) + fail("depth GMEM capacity",depth_base); + if (depth_enabled && color_mask && base+extent > uint64_t(depth_base)+(uint64_t(top)*pitch+left)*2u && + uint64_t(depth_base)+(uint64_t(bottom-1)*pitch+right)*2u > base+(uint64_t(top)*pitch+left)*bytes) + fail("overlapping depth/color attachments",depth_base); + /* NXP a1638da9 gsl_drawctxt.c:787-800, build_gmem2sys_cmds: COPY_DEST_OFFSET is the page-alignment pixel remainder. */ + auto* target = tiled ? nullptr : gmem && !resolve ? gmem_.data()+base : emu_.Get().WriteSpan(target_base, + (uint64_t(bottom-1+offset_y)*target_pitch+right+offset_x)*bytes,mmu_config); + auto top_left = [&](const Point& a, const Point& b) { + const double dx = (b.x-a.x)*sign, dy = (b.y-a.y)*sign; + return dy < 0.0 || (dy == 0.0 && dx > 0.0); + }; + for (int y = top; y < bottom; ++y) for (int x = left; x < right; ++x) { + const double a = edge(points[1],points[2],x+0.5,y+0.5)*sign; + const double b = edge(points[2],points[0],x+0.5,y+0.5)*sign; + const double c = edge(points[0],points[1],x+0.5,y+0.5)*sign; + if (a < 0 || b < 0 || c < 0 || (a == 0 && !top_left(points[1],points[2])) || + (b == 0 && !top_left(points[2],points[0])) || (c == 0 && !top_left(points[0],points[1]))) continue; + std::array weights{a/area,b/area,c/area}; + /* Khronos OpenGL ES 2.0.25 section 3.5.1: window-z linear interpolation; + Ford SYNC 2 RUN_20260906_174030_00 F067-F090, F097-F100, F113-F132: depth16 conversion. */ + const double depth = constant_depth ? original_depth : + weights[0]*points[0].z + weights[1]*points[1].z + weights[2]*points[2].z; + const uint16_t incoming_depth = static_cast((std::min)(65535.0,std::floor(depth * 65536.0))); + auto* depth_destination = depth_enabled ? gmem_.data()+depth_base+(uint64_t(y)*pitch+x)*2u : nullptr; + if ((raster & 0x100000u) == 0) { + double total = 0; + for (unsigned i = 0; i < 3; ++i) { weights[i] *= points[i].inverse_w; total += weights[i]; } + for (auto& weight : weights) weight /= total; + } + Imx51Gpu3dVec4 color{}; + if (resolve) { + const auto* p = gmem_.data()+base+(uint64_t(y)*pitch+x)*bytes; + if (bytes == 4u) for (unsigned i=0;i<4;++i) color[i]=float(p[i])/255.0f; + else { + const uint32_t packed=uint32_t(p[0])|(uint32_t(p[1])<<8); + if (format == 0u) for (unsigned i=0;i<4;++i) color[i]=float((packed>>(i*4u))&15u)/15.0f; + else color={float(packed&31u)/31.0f,float((packed>>5)&63u)/63.0f,float((packed>>11)&31u)/31.0f,1.0f}; + } + /* Mesa e97ad748 a2xx.xml: RB_COLOR_INFO.SWAP; Navigation20260906_235040 source_info=202. */ + if (swap == 1u) std::swap(color[0],color[2]); + } else { + std::array fragments{}; + const unsigned pixel_lane = unsigned(y & 1) * 2u + unsigned(x & 1); + auto& fragment = fragments[pixel_lane]; + const uint64_t varyings = vertices[0].export_mask & vertices[1].export_mask & vertices[2].export_mask; + for (unsigned slot = 0; slot < 32; ++slot) if (varyings & (uint64_t{1} << slot)) + for (unsigned component = 0; component < 4; ++component) + for (unsigned i = 0; i < 3; ++i) + fragment.registers[slot][component] += static_cast(weights[i]*vertices[i].exports[slot][component]); + /* Khronos GLES 2.0.25 section 3.7.7, texture-coordinate derivatives; GLSL ES 1.00 section 8.8. */ + std::array,4> quad_weights{}; + bool gradients_valid = true; + for (unsigned lane = 0; lane < 4u; ++lane) { + const double qx = (x & ~1) + (lane & 1u) + 0.5; + const double qy = (y & ~1) + (lane >> 1) + 0.5; + auto& q = quad_weights[lane]; + q = {edge(points[1],points[2],qx,qy)*sign/area, + edge(points[2],points[0],qx,qy)*sign/area, + edge(points[0],points[1],qx,qy)*sign/area}; + if ((raster & 0x100000u) == 0) { + double total = 0; + for (unsigned i = 0; i < 3u; ++i) { q[i] *= points[i].inverse_w; total += q[i]; } + if (!std::isfinite(total) || total == 0) { gradients_valid = false; continue; } + for (auto& weight : q) weight /= total; + } + } + fragment.gradient_mask = gradients_valid ? varyings & 0xFFFFFFFFu : 0; + for (unsigned slot = 0; slot < 32u; ++slot) if (fragment.gradient_mask & (uint64_t{1} << slot)) + for (unsigned component = 0; component < 4u; ++component) { + std::array q{}; + for (unsigned lane = 0; lane < 4u; ++lane) + for (unsigned i = 0; i < 3u; ++i) + q[lane] += static_cast(quad_weights[lane][i]*vertices[i].exports[slot][component]); + for (unsigned lane = 0; lane < 4u; ++lane) + if (lane != pixel_lane) fragments[lane].registers[slot][component] = q[lane]; + fragment.gradients_x[slot][component] = q[(y & 1)*2+1] - q[(y & 1)*2]; + fragment.gradients_y[slot][component] = q[(x & 1)+2] - q[x & 1]; + } + // Extrapolated lanes are helpers only: only pixel_lane is committed below. + if (gradients_valid) + emu_.Get().RunQuad(pixel_program,registers,mmu_config,fragments); + else + emu_.Get().Run(pixel_program,true,registers,mmu_config,fragment); + if (!fragment.memory_exports.empty()) fail("pixel memory export",0); + if (fragment.killed) continue; + if (depth_enabled && (fragment.export_mask & ~uint64_t{1})) + fail("depth fragment exports",static_cast(fragment.export_mask)); + /* NXP a1638da9 yamato_enum.h: CompareFrag; Khronos GLES 2.0 glDepthFunc. */ + if (depth_enabled) { + const uint32_t stored = uint32_t(depth_destination[0]) | (uint32_t(depth_destination[1]) << 8); + const bool passes[] = {false,incoming_depth < stored,incoming_depth == stored,incoming_depth <= stored, + incoming_depth > stored,incoming_depth != stored,incoming_depth >= stored,true}; + if (!passes[(depth_control >> 4) & 7u]) continue; + } + if (depth_write) writes.pixels.push_back({depth_destination, + {static_cast(incoming_depth),static_cast(incoming_depth >> 8),0,0},2}); + if (color_mask == 0) continue; + if ((fragment.export_mask & 1u) == 0) fail("missing fragment color",0); + color = fragment.exports[0]; + } + for (auto& channel : color) { + if (!std::isfinite(channel)) fail("nonfinite fragment",0); + channel = std::clamp(channel,0.0f,1.0f); + } + const uint64_t address = (uint64_t(y+offset_y)*target_pitch+x+offset_x)*bytes; + auto* destination = tiled ? emu_.Get().WriteSpan( + Imx51Gpu3dTiledAddress(target_base,target_pitch,bytes,uint32_t(x),uint32_t(y)),bytes,mmu_config) : target+address; + RasterWrites::Pixel pixel{destination,{},bytes}; + if (((target_info >> 9) & 3u) == 1u) std::swap(color[0],color[2]); + uint32_t mask = color_mask; + if (((target_info >> 9) & 3u) == 1u) mask = (mask & 10u) | ((mask & 1u) << 2) | ((mask & 4u) >> 2); + /* OpenGL ES 2.0.25 section 4.1.7; NXP a1638da9 RB_COLOR_INFO.COLOR_ROUND_MODE; + AMD Z430 GLES2: RGBA8888, round mode 0, disabled-dither color readback. */ + const bool truncate_8888 = (target_info & 0x30u) == 0u && (resolve || dither_mode == 0u); + if (bytes == 4u) for (unsigned i = 0; i < 4; ++i) + pixel.data[i] = (mask & (1u << i)) ? static_cast(truncate_8888 ? color[i] * 255u : + ApproximateDitherQuantize(color[i],255u,i,uint32_t(x)+offset_x,uint32_t(y)+offset_y, + !resolve && dither_mode != 0u)) : destination[i]; + else { + uint32_t packed = uint32_t(destination[0]) | (uint32_t(destination[1]) << 8); + const std::array shifts = format == 0u ? + (((target_info >> 9) & 3u) == 3u ? std::array{12,8,4,0} : std::array{0,4,8,12}) : + std::array{0,5,11,0}; + const std::array maxima = format == 0u ? std::array{15,15,15,15} : std::array{31,63,31,0}; + /* NXP a1638da9 BlendOpX: SRC_ALPHA/ONE_MINUS_SRC_ALPHA; CombFuncX: DST_PLUS_SRC. */ + if (blend) for (unsigned i = 0; i < 3u; ++i) { + const float stored = static_cast((packed >> shifts[i]) & maxima[i]) / maxima[i]; + color[i] = color[i] * color[3] + stored * (1.0f - color[3]); + } + for (unsigned i = 0; i < (format == 0u ? 4u : 3u); ++i) if (mask & (1u << i)) + packed = (packed & ~(maxima[i] << shifts[i])) | (ApproximateDitherQuantize(color[i],maxima[i],i, + uint32_t(x)+offset_x,uint32_t(y)+offset_y,!resolve && dither_mode != 0u) << shifts[i]); + pixel.data[0] = static_cast(packed); pixel.data[1] = static_cast(packed >> 8); + } + writes.pixels.push_back(pixel); + } + if (gmem && !resolve && !writes.pixels.empty()) { writes.binding = binding; writes.pitch = pitch; } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_raster.h b/cerf/socs/imx51/imx51_gpu3d_raster.h new file mode 100644 index 00000000..1dfa0ccf --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_raster.h @@ -0,0 +1,26 @@ +#pragma once +#include "../../core/service.h" +#include "imx51_gpu3d_shader.h" +class Imx51Gpu3dRaster : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + void SaveState(class StateWriter& writer); + void RestoreState(class StateReader& reader); + void Triangle(const std::array& vertices, + const std::unordered_map& registers, + std::span pixel_program, uint32_t mmu_config); +private: + struct RasterWrites { + struct Pixel { uint8_t* target; std::array data; uint32_t bytes; }; + std::vector pixels; + uint32_t binding, pitch; + }; + void RasterizeTriangle(const std::array& vertices, + const std::array& depth_vertices, + const std::unordered_map& registers, + std::span pixel_program, uint32_t mmu_config, RasterWrites& writes); + std::array gmem_{}; + uint32_t gmem_binding_ = 0xFFFFFFFFu; + uint32_t gmem_pitch_ = 0; +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_regs.h b/cerf/socs/imx51/imx51_gpu3d_regs.h index 80830d5d..e7c77d0b 100644 --- a/cerf/socs/imx51/imx51_gpu3d_regs.h +++ b/cerf/socs/imx51/imx51_gpu3d_regs.h @@ -71,9 +71,10 @@ constexpr uint32_t kIdxMasterIntSignal = 0x03B7u; default (a2xx.xml documents no reset; no shader instruction store modelled -> 0). */ constexpr uint32_t kIdxSqInstStoreManagment = 0x0D02u; -/* SCRATCH_REG2 (0x57A, yamato_reg.h:356): the draw-context fixup IB's scratch register, - used only to compute the SET_SHADER_BASES operand (kgsl_drawctxt.c:1245-1260). */ +/* NXP linux-imx a1638da9, yamato/22/yamato_offset.h:450-465. */ +constexpr uint32_t kIdxScratchReg0 = 0x0578u; constexpr uint32_t kIdxScratchReg2 = 0x057Au; +constexpr uint32_t kIdxScratchReg7 = 0x057Fu; /* MH_MMU_MPU_BASE/END (0x46/0x47): memory-protection-unit range, write-only. The guest DISABLES the MPU (base=0, end=0xFFFFF000 = all pages; @@ -123,6 +124,7 @@ constexpr uint32_t kPm4OpInterrupt = 0x40u; /* PM4_INTERRUPT */ constexpr uint32_t kPm4OpSetConstant = 0x2Du; /* PM4_SET_CONSTANT */ constexpr uint32_t kPm4OpLoadConstantContext = 0x2Eu; /* PM4_LOAD_CONSTANT_CONTEXT */ constexpr uint32_t kPm4OpRegToMem = 0x3Eu; /* PM4_REG_TO_MEM, kgsl_pm4types.h:68 */ +constexpr uint32_t kPm4OpMemWrite = 0x3Du; /* NXP gsl_pm4types.h: PM4_MEM_WRITE. */ constexpr uint32_t kPm4OpImStore = 0x2Cu; /* PM4_IM_STORE, kgsl_pm4types.h:148 */ constexpr uint32_t kPm4OpRegRmw = 0x21u; /* PM4_REG_RMW, kgsl_pm4types.h:65 */ constexpr uint32_t kPm4OpWaitRegEq = 0x52u; /* PM4_WAIT_REG_EQ, kgsl_pm4types.h:53 */ diff --git a/cerf/socs/imx51/imx51_gpu3d_shader.cpp b/cerf/socs/imx51/imx51_gpu3d_shader.cpp new file mode 100644 index 00000000..9d2f9178 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_shader.cpp @@ -0,0 +1,400 @@ +#include "imx51_gpu3d_shader.h" +#include "imx51_gpu3d_memory.h" +#include "imx51_gpu3d_texture.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../boards/board_context.h" +#include +#include +#include +#include + +REGISTER_SERVICE(Imx51Gpu3dShader); +bool Imx51Gpu3dShader::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} +void Imx51Gpu3dShader::Reject(const char* reason, uint32_t value) { + emu_.Get().Die("GPU shader rejected %s (0x%08X)", reason, value); +} +uint32_t Imx51Gpu3dShader::Register(const std::unordered_map& regs, uint32_t index) { + auto it = regs.find(index); + if (it == regs.end()) Reject("unprogrammed register", index); + return it->second; +} + +/* Mesa e97ad748 instr-a2xx.h: instr_alu_t; disasm-a2xx.c: print_srcreg; + ir2_assemble.c: alu_swizzle_scalar, alu_swizzle_scalar2, src_reg_byte; + ir2_nir.c: emit_alu, store_output, extra_position_exports; + ir2_assemble.c: relative_addr on export32; fd2_gmem.c: binning export constants. */ +void Imx51Gpu3dShader::Alu(std::array w, bool pixel, + const std::unordered_map& regs, + Imx51Gpu3dShaderState& state, bool& predicate, float& previous) { + const uint32_t pred = (w[1] >> 27) & 3u; + if (pred == 1u) Reject("ALU predicate selection", pred); + if (pred && predicate != ((pred & 1u) != 0)) return; + // Snapshot addressing before either paired slot can update MOVA state. + const int32_t old_address = state.address_register, loop_address = state.loop_address; + auto temporary_index = [&](uint32_t index, bool relative) { + const int64_t effective = int64_t(index) + (relative ? loop_address : 0); + if (effective < 0 || effective >= 64) Reject("ALU register extent", static_cast(effective)); + return static_cast(effective); + }; + auto source = [&](uint32_t which, bool force_constant = false) { + const uint32_t shift = (3u - which) * 8u; + const uint32_t index = (w[2] >> shift) & 255u; + const bool temporary = !force_constant && ((w[2] >> (32u - which)) & 1u) != 0; + Imx51Gpu3dVec4 raw{}, result{}; + if (temporary) { + raw = state.registers[temporary_index(index & 63u, (index & 64u) != 0)]; + if (index & 128u) for (auto& value : raw) value = std::abs(value); + } else { + // Related Xenia model: first constant uses const_0_rel_abs; later + // constant operands share const_1_rel_abs. A2xx has the same fields. + const bool first = which == 1u || (which == 2u ? (w[2] & 0x80000000u) != 0 : (w[2] & 0xC0000000u) == 0xC0000000u); + const bool relative = ((w[1] >> (first ? 31u : 30u)) & 1u) != 0; + const bool use_address = (w[1] & 0x20000000u) != 0; + // Keep Mesa's special absolute export32 constant base workaround. + const bool export32 = (w[0] & 0x803Fu) == 0x8020u && use_address && !relative; + const uint32_t base = export32 ? 0u : Register(regs, pixel ? 0x2308u : 0x2307u) & 511u; + const int64_t effective = int64_t(base) + index + (relative ? (use_address ? old_address : loop_address) : 0); + if (effective < 0 || effective >= 512) Reject("constant extent", static_cast(effective)); + for (uint32_t i = 0; i < 4u; ++i) raw[i] = std::bit_cast(Register(regs, 0x4000u + static_cast(effective) * 4u + i)); + } + const uint32_t swizzle = (w[1] >> shift) & 255u; + const bool negate = ((w[1] >> (27u - which)) & 1u) != 0; + for (uint32_t i = 0; i < 4u; ++i) result[i] = raw[((swizzle >> (i * 2u)) + i) & 3u] * (negate ? -1.0f : 1.0f); + return result; + }; + const uint32_t vm = (w[0] >> 16) & 15u, sm = (w[0] >> 20) & 15u; + const uint32_t vector_op = (w[2] >> 24) & 31u, scalar_op = w[0] >> 26; + /* Mesa instr-a2xx.h: reserved encodings and compiler-only NONE sentinels. + Validate before reading operands, but preserve unused zero-mask halves. */ + if (vm) { + if (vector_op == 31u) Reject("active VECTOR_NONE", vector_op); + if (vector_op == 30u) Reject("reserved vector opcode", vector_op); + } + if (sm) { + if (scalar_op == 63u) Reject("active SCALAR_NONE", scalar_op); + if (scalar_op == 41u || scalar_op > 50u) Reject("reserved scalar opcode", scalar_op); + } + if (vector_op == 29u && (scalar_op == 23u || scalar_op == 24u)) + Reject("simultaneous address writes", scalar_op); + if (!pixel && ((vector_op >= 24u && vector_op <= 27u) || (scalar_op >= 35u && scalar_op <= 39u))) + Reject("vertex kill opcode", vector_op >= 24u && vector_op <= 27u ? vector_op : scalar_op); + // Match Xenia ProcessAluInstruction: both slots use entry predication; + // scalar predicate writes follow vector writes and win when both are present. + auto compare = [](uint32_t op, float a, float b) { + return op == 0u ? a == b : op == 1u ? a != b : op == 2u ? a > b : a >= b; + }; + auto address = [](float value, bool round) { + // MOVA conversion follows the related Xenia kMaxAs/kMaxAsf model. + // Saturate before casting, including NaN, to avoid host conversion UB. + const float integral = std::floor(value + (round ? 0.5f : 0.0f)); + return !(integral >= -256.0f) ? -256 : integral > 255.0f ? 255 : static_cast(integral); + }; + auto multiply = [](float a, float b) { + // Xenia ucode.h: legacy multiply, citing R5xx 8.7.5 and Adreno 200 tests. + // Zero/subnormal inputs produce +0 even when the other input is NaN/Inf. + if (std::abs(a) < std::numeric_limits::min() || + std::abs(b) < std::numeric_limits::min()) return 0.0f; + return a * b; + }; + Imx51Gpu3dVec4 vector{}; + float scalar = previous; + /* NXP yamato_enum.h: PRED_SETE_PUSHv..KILLNEv; Mesa ir2_ra.c: has_side_effects; + Xenia ucode.h: AluVectorOpcode::kSetpEqPush..kKillNe. */ + if (vm || vector_op == 29u || (vector_op >= 20u && vector_op <= 27u)) { + const auto a = source(1u), b = (vector_op >= 8u && vector_op <= 10u) || vector_op == 19u || (vector_op == 29u && !vm) ? Imx51Gpu3dVec4{} : source(2u); + const uint32_t op = (w[2] >> 24) & 31u; + Imx51Gpu3dVec4 c{}; + if ((op >= 11u && op <= 14u) || op == 17u) c = source(3u); + if (op == 18u) { + // Mesa ir2_nir.c emits direction.zzxy, direction.yxzz and consumes + // (T, S, 2*major, face). The axis/tie formula follows Xenia kCube; + // arbitrary operand pairs and nonfinite directions remain unsupported. + const float x = a[2], y = a[3], z = a[0]; + if (a[1] != z || b[0] != y || b[1] != x || b[2] != z || b[3] != z) + Reject("cube operand layout", w[1]); + if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || + (x == 0.0f && y == 0.0f && z == 0.0f)) + Reject("cube direction", w[1]); + if (std::abs(z) >= std::abs(x) && std::abs(z) >= std::abs(y)) + vector = {-y, z < 0.0f ? -x : x, 2.0f * z, z < 0.0f ? 5.0f : 4.0f}; + else if (std::abs(y) >= std::abs(x)) + vector = {y < 0.0f ? -z : z, x, 2.0f * y, y < 0.0f ? 3.0f : 2.0f}; + else + vector = {-y, x < 0.0f ? z : -z, 2.0f * x, x < 0.0f ? 1.0f : 0.0f}; + } + float dot = 0; + if (op == 15u || op == 16u || op == 17u) { + const uint32_t count = op == 15u ? 4u : op == 16u ? 3u : 2u; + for (uint32_t i = 0; i < count; ++i) dot += multiply(a[i], b[i]); + // Xenia kDp2Add/MSDN model: XY dot plus post-swizzle C.x. + // Mesa fdot2 uses a zero addend, which alone cannot distinguish lanes. + if (op == 17u) dot += c[0]; + } + for (uint32_t i = 0; i < 4u; ++i) { + switch (op) { + case 0: vector[i] = a[i] + b[i]; break; + case 1: vector[i] = multiply(a[i], b[i]); break; + case 2: vector[i] = std::fmax(a[i], b[i]); break; + case 3: vector[i] = std::fmin(a[i], b[i]); break; + case 4: vector[i] = a[i] == b[i] ? 1.0f : 0.0f; break; + case 5: vector[i] = a[i] > b[i] ? 1.0f : 0.0f; break; + case 6: vector[i] = a[i] >= b[i] ? 1.0f : 0.0f; break; + case 7: vector[i] = a[i] != b[i] ? 1.0f : 0.0f; break; + case 8: vector[i] = a[i] - std::floor(a[i]); break; + case 9: vector[i] = std::trunc(a[i]); break; + case 10: vector[i] = std::floor(a[i]); break; + case 11: vector[i] = multiply(a[i], b[i]) + c[i]; break; + case 12: vector[i] = a[i] == 0 ? b[i] : c[i]; break; + case 13: vector[i] = a[i] >= 0 ? b[i] : c[i]; break; + case 14: vector[i] = a[i] > 0 ? b[i] : c[i]; break; + case 15: case 16: case 17: vector[i] = dot; break; + case 18: break; + /* NXP yamato_enum.h: MAX4v; Xenia ucode.h: AluVectorOpcode::kMax4. */ + case 19: + vector[i] = a[0] > a[1] && a[0] > a[2] && a[0] > a[3] ? a[0] + : a[1] > a[2] && a[1] > a[3] ? a[1] : a[2] > a[3] ? a[2] : a[3]; break; + case 20: case 21: case 22: case 23: + predicate = a[3] == 0.0f && compare(op - 20u, b[3], 0.0f); + vector[i] = a[0] == 0.0f && compare(op - 20u, b[0], 0.0f) ? 0.0f : a[0] + 1.0f; + break; + case 24: case 25: case 26: case 27: { + const uint32_t comparison = op == 24u ? 0u : op == 25u ? 2u : op == 26u ? 3u : 1u; + bool kill = false; + for (uint32_t lane = 0; lane < 4u; ++lane) kill |= compare(comparison, a[lane], b[lane]); + state.killed |= kill; + vector[i] = kill ? 1.0f : 0.0f; + break; + } + /* NXP yamato_enum.h: DSTv; Xenia ucode.h: AluVectorOpcode::kDst. */ + case 28: vector[i] = i == 0u ? 1.0f : i == 1u ? multiply(a[1], b[1]) : i == 2u ? a[2] : b[3]; break; + // Mesa names MOVAv but does not lower it. Use the related Xenia + // MAXA model: a0 comes from A.w, result is per-lane max(A, B). + case 29: state.address_register = address(a[3], true); vector[i] = std::fmax(a[i], b[i]); break; + default: Reject("vector opcode", op); + } + } + } + /* Mesa ir2_ra.c: has_side_effects; Xenia ucode.h: AluScalarOpcodeInfo. */ + if (sm || scalar_op == 23u || scalar_op == 24u || (scalar_op >= 27u && scalar_op <= 39u)) { + const bool constant_op = scalar_op >= 42u && scalar_op <= 47u; + const auto c = scalar_op == 33u || scalar_op == 50u ? Imx51Gpu3dVec4{} : source(3u, constant_op); + const float a = c[3]; + float b = c[2]; + if (constant_op) { + // Xenia scalar_const_reg_op_src_temp_reg / ParseAluInstruction: + // A2xx shares these opcode/operand fields. Source 3 is the full-byte + // constant index; opcode bit 0, src3_sel and swizzle bits 2..5 name R. + // This special form uses constant W and temporary X swizzle fields. + const uint32_t swizzle = w[1] & 255u; + const uint32_t index = (scalar_op & 1u) | (((w[2] >> 29) & 1u) << 1) | (swizzle & 60u); + b = state.registers[index][swizzle & 3u]; + if (w[1] & 0x1000000u) b = -b; + } + const uint32_t op = w[0] >> 26; + switch (op) { + case 0: scalar = a + b; break; + case 1: scalar = a + previous; break; + case 2: scalar = multiply(a, b); break; + case 3: scalar = multiply(a, previous); break; + /* NXP yamato_enum.h: MUL_PREV2s; Xenia ucode.h: AluScalarOpcode::kMulsPrev2. */ + case 4: + scalar = previous == -std::numeric_limits::max() || !std::isfinite(previous) || !std::isfinite(b) || b <= 0.0f + ? -std::numeric_limits::max() : multiply(a, previous); break; + case 5: scalar = std::fmax(a, b); break; + case 6: scalar = std::fmin(a, b); break; + /* NXP yamato_enum.h: SETEs..SETNEs; Xenia ucode.h: AluScalarOpcode::kSeqs..kSnes. */ + case 7: case 8: case 9: case 10: + scalar = compare(op == 7u ? 0u : op == 8u ? 2u : op == 9u ? 3u : 1u, a, 0.0f) ? 1.0f : 0.0f; break; + case 11: scalar = a - std::floor(a); break; + case 12: scalar = std::trunc(a); break; + case 13: scalar = std::floor(a); break; + case 14: scalar = std::exp2(a); break; + /* Xenia ucode.h: AluScalarOpcode::kLogc, kRcpf, kRsqc, kRsqf. */ + case 15: + scalar = std::log2(a); + if (scalar == -std::numeric_limits::infinity()) scalar = -std::numeric_limits::max(); + break; + case 16: scalar = std::log2(a); break; + case 17: scalar = std::clamp(1.0f / a, -std::numeric_limits::max(), std::numeric_limits::max()); break; + case 18: scalar = 1.0f / a; if (std::isinf(scalar)) scalar = std::copysign(0.0f, scalar); break; + case 19: scalar = 1.0f / a; break; + case 20: scalar = std::clamp(1.0f / std::sqrt(a), -std::numeric_limits::max(), std::numeric_limits::max()); break; + case 21: scalar = 1.0f / std::sqrt(a); if (std::isinf(scalar)) scalar = std::copysign(0.0f, scalar); break; + case 22: scalar = 1.0f / std::sqrt(a); break; + case 23: state.address_register = address(a, true); scalar = std::fmax(a, b); break; + case 24: state.address_register = address(a, false); scalar = std::fmax(a, b); break; + case 25: scalar = a - b; break; + case 26: scalar = a - previous; break; + /* NXP yamato_enum.h: PRED_SETEs..KILLONEs; Mesa ir2_nir.c: emit_if; + Xenia ucode.h: AluScalarOpcode::kSetpEq..kKillsOne. */ + case 27: case 28: case 29: case 30: + predicate = compare(op - 27u, a, 0.0f); scalar = predicate ? 0.0f : 1.0f; break; + case 31: predicate = a == 1.0f; scalar = predicate ? 0.0f : a == 0.0f ? 1.0f : a; break; + case 32: scalar = a - 1.0f; predicate = scalar <= 0.0f; if (predicate) scalar = 0.0f; break; + case 33: predicate = false; scalar = std::numeric_limits::max(); break; + case 34: predicate = a == 0.0f; scalar = a; break; + case 35: case 36: case 37: case 38: case 39: { + const bool kill = op == 39u ? a == 1.0f : compare(op == 35u ? 0u : op == 36u ? 2u : op == 37u ? 3u : 1u, a, 0.0f); + state.killed |= kill; scalar = kill ? 1.0f : 0.0f; break; + } + case 40: scalar = std::sqrt(a); break; + case 42: case 43: scalar = multiply(a, b); break; + case 44: case 45: scalar = a + b; break; + case 46: case 47: scalar = a - b; break; + /* Mesa ir2_nir.c: nir_op_fsin, nir_op_fcos; Xenia ucode.h: kSin, kCos. */ + case 48: scalar = std::sin(a); break; + case 49: scalar = std::cos(a); break; + case 50: break; + default: Reject("scalar opcode", op); + } + previous = scalar; + } + auto write = [&](uint32_t index, uint32_t mask, const Imx51Gpu3dVec4& values, bool clamp, bool relative) { + if (!mask) return; + const bool output = (w[0] & 0x8000u) != 0; + if (output && state.killed) return; + if (output && relative) Reject("relative ALU export", index); + if (!output) index = temporary_index(index, relative); + if (output && index >= 34u && index < 62u) Reject("memory export register", index); + if (!output) state.gradient_mask &= ~(uint64_t{1} << index); + auto& target = output ? state.exports[index] : state.registers[index]; + for (uint32_t i = 0; i < 4u; ++i) if ((mask >> i) & 1u) target[i] = clamp ? std::clamp(values[i], 0.0f, 1.0f) : values[i]; + if (output) state.export_mask |= uint64_t(1) << index; + if (output && index == 33u) { + if (!(state.export_mask & (uint64_t(1) << 32)) || mask != 15u) Reject("incomplete memory export", mask); + state.memory_exports.push_back({state.exports[32], state.exports[33]}); + } + }; + write(w[0] & 63u, vm, vector, ((w[0] >> 24) & 1u) != 0, (w[0] & 64u) != 0); + write((w[0] >> 8) & 63u, sm, {scalar, scalar, scalar, scalar}, ((w[0] >> 25) & 1u) != 0, (w[0] & 0x4000u) != 0); +} + +/* Mesa e97ad748 instr-a2xx.h: instr_fetch_vtx_t, instr_fetch_tex_t; + fd2_program.c: patch_vtx_fetch; fd2_emit.c: fd2_emit_vertex_bufs; + a2xx.xml: a2xx_sq_surfaceformat; NXP yamato_registers.h: TP0_CHICKEN; + gsl_drawctxt.c: sys2gmem_vtx_pgm, TP0_CHICKEN=0; fd2_emit.c: TP0_CHICKEN=2. */ +void Imx51Gpu3dShader::Fetch(std::array w, + const std::unordered_map& regs, + uint32_t config, Imx51Gpu3dShaderState& state, bool predicate) { + if ((w[1] >> 31) && predicate != ((w[2] >> 31) != 0)) return; + auto fetch_index = [&](uint32_t shift) { + const int64_t index = int64_t((w[0] >> shift) & 63u) + (((w[0] >> (shift + 6u)) & 1u) ? state.loop_address : 0); + if (index < 0 || index >= 64) Reject("fetch register extent", static_cast(index)); + return static_cast(index); + }; + const uint32_t source = fetch_index(5u), destination = fetch_index(12u); + const auto& input = state.registers[source]; + Imx51Gpu3dVec4 value{}; + const uint32_t op = w[0] & 31u; + if (op == 24u) { + // Mesa ir2.c schedule_instrs / ir2_assemble.c: the setter selects one + // source component and preserves all ordinary destination components. + // Keep its value for subsequent samples; zero is our invocation default. + state.texture_lod = input[(w[0] >> 26) & 3u]; + return; + } + if (op == 25u || op == 26u) { + // Provisional Xenos-compatible XYZ gradient register layout; A2xx + // shares the setter encodings. Missing components begin at zero. + auto& gradient = op == 25u ? state.texture_gradients_x : state.texture_gradients_y; + for (uint32_t i = 0; i < 3u; ++i) + gradient[i] = input[(w[0] >> (26u + i * 2u)) & 3u]; + return; + } + if (op == 18u) { + if ((w[2] & 0x7FFFFFFDu) || (w[1] & 0x60000000u)) Reject("gradient query controls", w[1]); + if (!(state.gradient_mask & (uint64_t{1} << source))) Reject("unavailable query gradients", w[0]); + // Provisional Xenos layout: XZ=ddx(source.xy), YW=ddy(source.xy). + // The quad executor supplies finite differences after coordinate ALU. + for (uint32_t i = 0; i < 2u; ++i) { + const uint32_t component = (w[0] >> (26u + i * 2u)) & 3u; + value[i * 2u] = state.gradients_x[source][component]; + value[i * 2u + 1u] = state.gradients_y[source][component]; + } + } else if (op == 1u || op == 16u || op == 17u || op == 19u) { + Imx51Gpu3dVec4 coords{}, dx{}, dy{}; + for (uint32_t i = 0; i < 3u; ++i) { + const uint32_t component = (w[0] >> (26u + i * 2u)) & 3u; + coords[i] = input[component]; + dx[i] = state.gradients_x[source][component]; + dy[i] = state.gradients_y[source][component]; + } + const bool explicit_gradients = (w[2] & 1u) != 0; + if (explicit_gradients) { dx = state.texture_gradients_x; dy = state.texture_gradients_y; } + const bool gradients = explicit_gradients || (state.gradient_mask & (uint64_t{1} << source)) != 0; + value = emu_.Get().Sample(regs, config, (w[0] >> 20) & 31u, coords, w, + gradients ? &dx : nullptr, gradients ? &dy : nullptr, state.texture_lod); + } else if (op == 0u) { + const uint32_t slot = (w[0] >> 20) & 31u, select = (w[0] >> 25) & 3u; + if (select == 3u) Reject("vertex constant selector", select); + const uint32_t base = Register(regs, 0x4800u + slot * 6u + select * 2u); + const uint32_t size = Register(regs, 0x4801u + slot * 6u + select * 2u); + if ((base & 3u) != 3u) Reject("vertex buffer type", base); + const float index = input[w[0] >> 30]; + if (!std::isfinite(index) || index < 0 || index >= 4294967296.0f || index != std::floor(index)) Reject("vertex index", std::bit_cast(index)); + const uint32_t unit = (Register(regs, 0x0E1Eu) & 2u) ? 1u : 4u; + const uint64_t offset = (uint64_t(static_cast(index)) * (w[2] & 255u) + ((w[2] >> 8) & 0x3FFFFFu)) * unit; + const uint32_t format = (w[1] >> 16) & 63u; + // Mesa fd2_pipe2surface / patch_vtx_fetch: component widths and signed, + // normalized and fixed-point controls are independent of the surface format. + uint32_t count = 0, component_bytes = 0; + bool floating = false; + switch (format) { + case 2: count = 1; component_bytes = 1; break; + case 10: count = 2; component_bytes = 1; break; + case 6: count = 4; component_bytes = 1; break; + case 24: case 25: case 26: + count = 1u << (format - 24u); component_bytes = 2; break; + case 30: case 31: case 32: + count = 1u << (format - 30u); component_bytes = 2; floating = true; break; + case 33: case 34: case 35: + count = 1u << (format - 33u); component_bytes = 4; break; + case 36: case 37: case 38: case 57: + count = format == 57u ? 3u : 1u << (format - 36u); + component_bytes = 4; floating = true; break; + default: Reject("vertex format", format); + } + const uint32_t bytes = count * component_bytes; + if (offset + bytes > size) Reject("vertex buffer extent", size); + const bool signed_components = (w[1] & 0x1000u) != 0; + const bool normalized = (w[1] & 0x2000u) == 0; + if (!floating && signed_components && normalized && (w[1] & 0x4000u)) + Reject("vertex signed repeating fraction mode", w[1]); + const int exponent = int((w[1] >> 24) & 31u) - int((w[1] >> 24) & 32u); + const uint8_t* data = emu_.Get().ReadSpan(uint64_t(base & ~3u) + offset, bytes, config); + for (uint32_t i = 0; i < count; ++i) { + uint32_t packed = 0; + for (uint32_t j = 0; j < component_bytes; ++j) + packed |= uint32_t(data[i * component_bytes + j]) << (j * 8u); + if (floating && component_bytes == 4u) value[i] = std::bit_cast(packed); + else if (floating) { + const uint32_t exp = (packed >> 10) & 31u, fraction = packed & 1023u; + if (exp == 31u) value[i] = std::bit_cast(0x7F800000u | (fraction << 13)); + else value[i] = std::ldexp(float(exp ? fraction + 1024u : fraction), exp ? int(exp) - 25 : -24); + if (packed & 0x8000u) value[i] = -value[i]; + } else { + const uint32_t bits = component_bytes * 8u; + const uint64_t range = uint64_t{1} << bits; + const int64_t integer = signed_components && (packed & (range >> 1)) ? + int64_t(packed) - int64_t(range) : int64_t(packed); + double converted = double(integer); + if (normalized) converted = signed_components ? + (std::max)(-1.0, converted / double((range >> 1) - 1u)) : converted / double(range - 1u); + value[i] = static_cast(converted); + } + value[i] = std::ldexp(value[i], exponent); + } + } else Reject("fetch opcode", op); + state.gradient_mask &= ~(uint64_t{1} << destination); + auto& dest = state.registers[destination]; + for (uint32_t i = 0; i < 4u; ++i) { + const uint32_t swizzle = (w[1] >> (i * 3u)) & 7u; + if (swizzle < 4u) dest[i] = value[swizzle]; + else if (swizzle == 4u || swizzle == 5u) dest[i] = swizzle == 5u ? 1.0f : 0.0f; + else if (swizzle != 7u) Reject("fetch destination swizzle", swizzle); + } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_shader.h b/cerf/socs/imx51/imx51_gpu3d_shader.h new file mode 100644 index 00000000..43267a7d --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_shader.h @@ -0,0 +1,45 @@ +#pragma once +#include "../../core/service.h" +#include +#include +#include +#include +#include + +using Imx51Gpu3dVec4 = std::array; +struct Imx51Gpu3dMemoryExport { Imx51Gpu3dVec4 address, data; }; +struct Imx51Gpu3dShaderState { + std::array registers{}, exports{}; + std::vector memory_exports; + std::array gradients_x{}, gradients_y{}; + uint64_t gradient_mask = 0; + uint64_t export_mask = 0; + float texture_lod = 0; + int32_t loop_address = 0; + Imx51Gpu3dVec4 texture_gradients_x{}, texture_gradients_y{}; + bool killed = false; + int32_t address_register = 0; +}; +class Imx51Gpu3dShader : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + void Run(std::span program, bool pixel, + const std::unordered_map& registers, + uint32_t mmu_config, Imx51Gpu3dShaderState& state); + void RunQuad(std::span program, + const std::unordered_map& registers, + uint32_t mmu_config, std::array& states); +private: + void RunInvocations(std::span program, bool pixel, + const std::unordered_map& registers, + uint32_t mmu_config, std::span states); + void Alu(std::array words, bool pixel, + const std::unordered_map& registers, + Imx51Gpu3dShaderState& state, bool& predicate, float& previous); + void Fetch(std::array words, + const std::unordered_map& registers, + uint32_t mmu_config, Imx51Gpu3dShaderState& state, bool predicate); + uint32_t Register(const std::unordered_map& registers, uint32_t index); + [[noreturn]] void Reject(const char* reason, uint32_t value); +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_shader_execution.cpp b/cerf/socs/imx51/imx51_gpu3d_shader_execution.cpp new file mode 100644 index 00000000..387c1737 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_shader_execution.cpp @@ -0,0 +1,252 @@ +#include "imx51_gpu3d_shader.h" +#include + +/* Mesa e97ad748 src/freedreno/ir2/instr-a2xx.h: instr_cf_exec_t, + instr_cf_jmp_call_t; disasm-a2xx.c: disasm_a2xx; ir2_assemble.c: CF address fixups. */ +void Imx51Gpu3dShader::Run(std::span program, bool pixel, + const std::unordered_map& regs, + uint32_t config, Imx51Gpu3dShaderState& state) { + RunInvocations(program, pixel, regs, config, {&state, 1}); +} + +void Imx51Gpu3dShader::RunQuad(std::span program, + const std::unordered_map& regs, + uint32_t config, std::array& states) { + RunInvocations(program, true, regs, config, states); +} + +void Imx51Gpu3dShader::RunInvocations(std::span program, bool pixel, + const std::unordered_map& regs, + uint32_t config, std::span states) { + if (program.empty() || program.size() % 3u || program.size() > 1536u) + Reject("program length", static_cast(program.size())); + auto control = [&](uint32_t pc) { + if (uint64_t(pc) * 3u + 2u >= uint64_t(program.size()) * 2u) Reject("control address", pc); + const size_t offset = size_t(pc / 2u) * 3u; + return (pc & 1u) ? (uint64_t(program[offset + 1u]) >> 16) | (uint64_t(program[offset + 2u]) << 16) + : uint64_t(program[offset]) | (uint64_t(program[offset + 1u] & 0xFFFFu) << 32); + }; + uint32_t limit = static_cast(program.size() / 3u * 2u); + for (uint32_t i = 0; i < limit; ++i) { + const uint64_t cf = control(i); + const uint32_t op = static_cast(cf >> 44); + if ((op >= 1u && op <= 6u) || op == 13u || op == 14u) { + const uint32_t address = static_cast(cf & 511u); + const uint32_t count = static_cast((cf >> 12) & 7u); + // Empty clauses have no instruction extent, including an empty EXEC_END. + if (!count) continue; + if (uint64_t(address + count) * 3u > program.size()) Reject("instruction span", address); + if (address * 2u <= i) Reject("instruction overlaps control", address); + // CF targets count 48-bit entries; EXEC addresses count 96-bit slots. + limit = std::min(limit, address * 2u); + } + } + struct Loop { uint32_t remaining, id; int32_t step, saved_address; }; + struct Cursor { + uint32_t pc = 0, steps = 0, address = 0, remaining = 0, sequence = 0; + bool predicate = false, end = false, finished = false; + float previous = 0; + std::vector return_stack; + std::vector loops; + }; + std::array cursors{}; + const bool quad = states.size() == 4; + for (auto& state : states) { + state.export_mask = 0; + state.memory_exports.clear(); + state.killed = false; + state.address_register = 0; + state.texture_lod = 0; + state.texture_gradients_x = {}; + state.texture_gradients_y = {}; + state.loop_address = 0; + } + auto finish = [&](size_t lane) { + cursors[lane].finished = true; + if (states[lane].killed) { + states[lane].export_mask = 0; + states[lane].memory_exports.clear(); + } + }; + // Cooperatively advance each invocation to its next FETCH. ALU executes + // with real helper values, so nonlinear coordinate derivatives are not + // approximated by propagating input gradients through arithmetic. + auto advance = [&](size_t lane) { + auto& cursor = cursors[lane]; + auto& state = states[lane]; + auto& pc = cursor.pc; + auto& predicate = cursor.predicate; + auto& previous = cursor.previous; + auto& loops = cursor.loops; + auto& return_stack = cursor.return_stack; + while (!cursor.finished) { + if (cursor.remaining) { + if (cursor.sequence & 1u) return true; + const size_t offset = size_t(cursor.address) * 3u; + const bool was_killed = state.killed; + Alu({program[offset], program[offset + 1u], program[offset + 2u]}, + pixel, regs, state, predicate, previous); + ++cursor.address; + --cursor.remaining; + cursor.sequence >>= 2; + if (state.killed && !was_killed) { + if (!quad) finish(lane); + // Let the other lanes reach a kill or FETCH before deciding + // whether all are dead. Surviving lanes still need helpers. + return false; + } + continue; + } + if (cursor.end) { finish(lane); return false; } + if (cursor.steps++ == 4096u) Reject("instruction budget", 4096u); + if (pc >= limit) Reject("control fallthrough", pc); + const uint64_t cf = control(pc++); + const uint32_t op = static_cast(cf >> 44); + if (op == 12u) { + // ALLOC reserves hardware output capacity (Mesa write_cfs/ir2_assemble). + // Exports are preallocated and memory exports grow on demand here. + continue; + } + if (op == 15u) { + // MARK_VS_FETCH_DONE is a scheduling hint (Xenia ucode.h). + // Fetch completes synchronously; no outstanding work needs draining. + continue; + } + if (op == 0u) continue; + if (op == 7u || op == 8u) { + // Mesa's A2xx loop layout has no Xenos repeat/predicated-break bits. + if (cf & 0x7FFFFE0FC00ull) Reject("loop reserved fields", static_cast(cf)); + const uint32_t target = static_cast(cf & 1023u); + if (target >= limit) Reject("loop target", target); + const uint32_t id = static_cast((cf >> 16) & 31u); + if (op == 8u) { + if (loops.empty() || loops.back().id != id) Reject("unmatched loop end", id); + auto& loop = loops.back(); + if (--loop.remaining) { + state.loop_address += loop.step; + pc = target; + } else { + state.loop_address = loop.saved_address; + loops.pop_back(); + } + continue; + } + // NXP SQ_CF_LOOP: 8-bit count/start/step; signed step follows Xenia's model. + const uint32_t value = Register(regs, 0x4908u + id); + const uint32_t count = value & 255u; + if (!count) { pc = target; continue; } + if (loops.size() == 64u) Reject("loop stack budget", 64u); + const uint32_t step = (value >> 16) & 255u; + loops.push_back({count, id, step < 128u ? int32_t(step) : int32_t(step) - 256, state.loop_address}); + state.loop_address = static_cast((value >> 8) & 255u); + continue; + } + if (op == 10u) { + // Xenia sequencer model: an empty RETURN falls through. + if (!return_stack.empty()) { + pc = return_stack.back(); + return_stack.pop_back(); + } + continue; + } + auto boolean = [&] { + const uint32_t index = static_cast((cf >> 34) & 255u); + return ((Register(regs, 0x4900u + index / 32u) >> (index % 32u)) & 1u) != 0; + }; + const bool condition = ((cf >> 42) & 1u) != 0; + if (op == 9u || op == 11u) { + // Mesa emits mode zero with a CF-entry target and a direction hint. + // The alternate address mode has no established A2xx execution rule. + if ((cf >> 43) & 1u) Reject("jump address mode", 1u); + const bool force = ((cf >> 13) & 1u) != 0; + const bool test = force ? condition : ((cf >> 14) & 1u) ? predicate : boolean(); + if (force || test == condition) { + const uint32_t target = static_cast(cf & 1023u); + if (target >= limit) Reject("jump target", target); + const bool forward = ((cf >> 33) & 1u) != 0; + if (op == 11u && forward != (target > pc - 1u)) Reject("jump direction", target); + if (op == 9u) { + // Emulator safety bound, not a claim about hardware stack depth. + if (return_stack.size() == 64u) Reject("call stack budget", 64u); + return_stack.push_back(pc); + } + pc = target; + } + continue; + } + if (!((op >= 1u && op <= 6u) || op == 13u || op == 14u)) Reject("control opcode", op); + // CLEAN avoids a hardware predicate stall; clauses execute synchronously here. + // Xenia ucode.h models CLEAN as boolean-conditioned, without clearing P. + bool execute = true; + if (op == 3u || op == 4u || op == 13u || op == 14u) execute = boolean() == condition; + if (op == 5u || op == 6u) execute = predicate == condition; + if (execute) { + cursor.address = static_cast(cf & 511u); + cursor.remaining = static_cast((cf >> 12) & 7u); + cursor.sequence = static_cast((cf >> 16) & 4095u); + } + // Save clause-entry eligibility: ALU may change the predicate later. + cursor.end = op == 2u || ((op == 4u || op == 6u || op == 14u) && execute); + } + return false; + }; + for (;;) { + std::array ready{}; + bool pending_kill = false; + for (size_t lane = 0; lane < states.size(); ++lane) { + ready[lane] = advance(lane); + pending_kill |= !ready[lane] && !cursors[lane].finished; + } + bool live = false; + for (size_t lane = 0; lane < states.size(); ++lane) + live |= !cursors[lane].finished && !states[lane].killed; + if (!live) { + for (size_t lane = 0; lane < states.size(); ++lane) finish(lane); + return; + } + if (pending_kill) continue; + if (std::none_of(ready.begin(), ready.end(), [](bool value) { return value; })) return; + bool aligned = quad; + for (size_t lane = 0; lane < states.size(); ++lane) + aligned &= ready[lane] && cursors[lane].address == cursors[0].address; + // Resolve each lane separately: loop-relative operands may name different registers. + std::array sources{}; + std::array source_valid{}; + for (size_t lane = 0; quad && lane < states.size(); ++lane) if (ready[lane]) { + const size_t offset = size_t(cursors[lane].address) * 3u; + const uint32_t word = program[offset]; + const bool active = !(program[offset + 1u] >> 31) || + cursors[lane].predicate == ((program[offset + 2u] >> 31) != 0); + const int64_t index = int64_t((word >> 5) & 63u) + + ((word & (1u << 11)) ? states[lane].loop_address : 0); + source_valid[lane] = active && index >= 0 && index < 64; + aligned &= source_valid[lane]; + if (source_valid[lane]) { + sources[lane] = static_cast(index); + states[lane].gradient_mask &= ~(uint64_t{1} << sources[lane]); + } + } + // Snapshot before any FETCH overwrites its source; divergent sites stay unavailable. + for (size_t lane = 0; aligned && lane < states.size(); ++lane) { + const uint32_t source = sources[lane]; + for (unsigned component = 0; component < 4u; ++component) { + states[lane].gradients_x[source][component] = + states[lane | 1u].registers[sources[lane | 1u]][component] - + states[lane & ~size_t{1}].registers[sources[lane & ~size_t{1}]][component]; + states[lane].gradients_y[source][component] = + states[lane | 2u].registers[sources[lane | 2u]][component] - + states[lane & ~size_t{2}].registers[sources[lane & ~size_t{2}]][component]; + } + states[lane].gradient_mask |= uint64_t{1} << source; + } + for (size_t lane = 0; lane < states.size(); ++lane) if (ready[lane]) { + auto& cursor = cursors[lane]; + const size_t offset = size_t(cursor.address) * 3u; + Fetch({program[offset], program[offset + 1u], program[offset + 2u]}, + regs, config, states[lane], cursor.predicate); + ++cursor.address; + --cursor.remaining; + cursor.sequence >>= 2; + } + } +} diff --git a/cerf/socs/imx51/imx51_gpu3d_texture.cpp b/cerf/socs/imx51/imx51_gpu3d_texture.cpp new file mode 100644 index 00000000..f699fe81 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_texture.cpp @@ -0,0 +1,207 @@ +#include "imx51_gpu3d_texture.h" +#include "imx51_gpu3d_memory.h" +#include "imx51_gpu3d_tiling.h" +#include "../../core/cerf_emulator.h" +#include "../../core/fatal.h" +#include "../../boards/board_context.h" +#include +#include +#include + +REGISTER_SERVICE(Imx51Gpu3dTexture); +bool Imx51Gpu3dTexture::ShouldRegister() { + auto* board = emu_.TryGet(); + return board && board->GetSoc() == SocFamily::iMX51; +} + +/* Mesa e97ad748, a2xx.xml: A2XX_SQ_TEX; instr-a2xx.h: instr_fetch_tex_t; + fd2_gmem.c: emit_mem2gmem_surf. */ +Imx51Gpu3dVec4 Imx51Gpu3dTexture::Sample(const std::unordered_map& registers, + uint32_t mmu_config, uint32_t slot, const Imx51Gpu3dVec4& coordinates, + std::array instruction, const Imx51Gpu3dVec4* dx, const Imx51Gpu3dVec4* dy, float register_lod) { + + auto fail = [&](const char* reason, uint32_t value) { + emu_.Get().Die("GPU texture %s slot=%u value=%08X", reason, slot, value); + }; + if (slot >= 32u) fail("slot", slot); + const bool query_weights = (instruction[0] & 31u) == 19u; + const bool query_border = (instruction[0] & 31u) == 16u; + std::array state{}; + for (uint32_t i = 0; i < state.size(); ++i) { + const auto found = registers.find(0x4800u + slot * 6u + i); + if (found == registers.end()) fail("missing descriptor", i); + state[i] = found->second; + } + const uint32_t format = state[1] & 63u, pitch = ((state[0] >> 22) & 511u) * 32u; + const uint32_t width = (state[2] & 8191u) + 1u, height = ((state[2] >> 13) & 8191u) + 1u; + const uint32_t clamp_x = (state[0] >> 10) & 7u, clamp_y = (state[0] >> 13) & 7u; + const bool tiled = (state[0] & 0x80000000u) != 0; + const uint32_t dimension = (state[5] >> 9) & 3u; + const bool cube = dimension == 3u; + if ((state[0] & 0x000003FDu) != 0 || ((state[1] >> 6) & 15u) != 0 || (state[3] & 1u) != 0) + fail("unsupported type/sign/endian", state[0]); + if ((dimension != 1u && !cube) || (state[2] >> 26) != 0 || pitch < width) + fail("unsupported dimension/pitch", state[5]); + auto compute_lod = [&] { + if (!dx || !dy) fail("unavailable texture gradients",instruction[0]); + for (unsigned i = 0; i < 2u; ++i) + if (!std::isfinite((*dx)[i]) || !std::isfinite((*dy)[i])) fail("nonfinite texture gradients",slot); + const double sx = (instruction[0] & (1u << 25)) ? 1.0 : double(width); + const double sy = (instruction[0] & (1u << 25)) ? 1.0 : double(height); + /* Khronos GLES 2.0.25 section 3.7.7, equation 3.12. */ + const double rho = (std::max)(std::hypot((*dx)[0]*sx,(*dx)[1]*sy), + std::hypot((*dy)[0]*sx,(*dy)[1]*sy)); + return rho > 0.0 ? std::log2(rho) : -INFINITY; + }; + if ((instruction[0] & 31u) == 17u) { + const uint32_t aniso = (instruction[1] >> 18) & 7u; + if (cube || (instruction[2] & 0x7FFFFFFDu) || (instruction[1] & 0x60000000u) || + (aniso != 0u && aniso != 7u)) fail("unsupported LOD query controls",instruction[1]); + // Provisional Xenos query model: implicit, unbiased/unclamped LOD in X. + // Leave YZW zero; ordinary fetch swizzles can retain destination lanes. + return {static_cast(compute_lod()),0,0,0}; + } + if ((clamp_x != 0u && clamp_x != 1u && clamp_x != 2u) || + (clamp_y != 0u && clamp_y != 1u && clamp_y != 2u)) fail("unsupported clamp", state[0]); + if ((state[4] & 0x003FFC3Cu) != 0 || (state[3] & 0xFE07E000u) != 0 || + (instruction[2] & 0x7FFFFFFCu) != 0) fail("unsupported LOD/offset", state[4]); + const uint32_t aniso = (instruction[1] >> 18) & 7u, arbitrary = (instruction[1] >> 21) & 7u; + const uint32_t reg_lod = (instruction[1] >> 29) & 3u; + const bool computed_lod = (instruction[1] & (1u << 28)) != 0 || (instruction[2] & 1u) != 0; + if ((aniso != 0u && aniso != 7u) || (arbitrary != 0u && arbitrary != 7u) || + reg_lod > 1u) fail("unsupported anisotropy/register LOD",instruction[1]); + if (reg_lod && std::isnan(register_lod)) fail("NaN register LOD",instruction[1]); + auto filter = [&](uint32_t shift, uint32_t constant_shift) { + const uint32_t selected = (instruction[1] >> shift) & 3u; + return selected == 3u ? (state[3] >> constant_shift) & 3u : selected; + }; + const uint32_t mag = filter(12u,19u), min = filter(14u,21u), mip = filter(16u,23u); + const bool mipmapped = mip <= 1u; + if (mag > 1u || mag != min || (mip != 2u && !mipmapped)) fail("unsupported filter",instruction[1]); + if (format != 6u && format != 4u && format != 2u && format != 15u && format != 10u) fail("unsupported format", format); + const uint32_t bytes = format == 6u ? 4u : (format == 4u || format == 15u || format == 10u) ? 2u : 1u; + uint32_t face = 0; + if (cube) { + // Mesa fd2_layout_resource allocates each linear face with a 32-row + // padded height and 4096-byte size alignment; fd2_tile_mode disables tiling. + if (tiled || mipmapped || width != height || (state[5] & 0xFFFu) != 0x600u || + clamp_x != 2u || clamp_y != 2u || (instruction[0] & (1u << 25))) + fail("unsupported cube layout/filter",state[5]); + const float selected = coordinates[2]; + if (!std::isfinite(selected) || selected < 0 || selected > 5 || selected != std::floor(selected)) + fail("cube face",slot); + face = static_cast(selected); + } + double lod = 0; + const uint32_t last_level = std::bit_width(width)-1u; + if (mipmapped) { + /* i.MX51 libGLESv2.so.2 rb_init_tile_info 0xE7AA8: uncompressed bytes-per-texel layout; + NXP yamato_enum.h: FMT_8_8_8_8=6, FMT_8_8=10. */ + const char* invalid = !tiled ? "mip linear layout" : (format != 10u && format != 6u) ? "mip format" : + + (width < 32u || width != height || !std::has_single_bit(width)) ? "mip dimensions" : pitch != width ? "mip pitch" : + state[4] != (last_level << 6) ? "mip levels/LOD state" : (state[5] & 0xFFFu) != 0xA00u ? "mip packing controls" : + (instruction[0] & (1u << 25)) ? "mip denormalized coordinates" : nullptr; + if (invalid) fail(invalid,state[5]); + if (computed_lod) lod = compute_lod(); + // Mesa emits register mode 1 for its extra LOD/bias source, with + // computed LOD enabled in fragment shaders and disabled in vertex shaders. + // Model it as a bias to computed LOD, or an explicit LOD when disabled. + if (reg_lod) lod += register_lod; + if (std::isnan(lod)) fail("indeterminate combined LOD",instruction[1]); + lod = std::clamp(lod, 0.0, double(last_level)); + } + auto sample_level = [&](uint32_t level) { + const uint32_t level_width = (std::max)(1u,width >> level), level_height = (std::max)(1u,height >> level); + /* Same-chip libGLESv2.so.2 rb_init_tile_info 0xE7AA8: 32-texel pitch alignment, 4096-byte allocations, 16-texel tail. */ + const uint32_t tail_level = last_level >= 4u ? last_level-4u : 0u; + const uint32_t level_pitch = level ? (std::max)(32u,pitch >> level) : pitch; + uint32_t mip_x = 0, mip_y = 0; + uint64_t base = (level ? state[5] : state[1]) & 0xFFFFF000u; + if (cube) base += face * ((uint64_t(pitch) * ((height + 31u) & ~31u) * bytes + 4095u) & ~uint64_t{4095u}); + if (level) { + for (uint32_t preceding = 1; preceding < (std::min)(level,tail_level); ++preceding) { + const uint64_t side = (std::max)(32u,width >> preceding); + base += (side*side*bytes+4095u) & ~uint64_t{4095u}; + } + if (level >= tail_level) { + const uint32_t relative = level-tail_level; + if (relative < 3u) mip_x = 16u >> relative; + else mip_y = 16u >> (relative-2u); + } + } + if (base > UINT32_MAX) fail("mip address overflow",level); + double u = coordinates[0], v = coordinates[1]; + if (!std::isfinite(u) || !std::isfinite(v)) fail("nonfinite coordinate", instruction[0]); + // Mesa ir2_nir emits CUBE, reciprocal major axis, +1.5, then YXW fetch. + if (cube) { u -= 1.0; v -= 1.0; } + if ((instruction[0] & (1u << 25)) == 0) { u *= level_width; v *= level_height; } + auto reduce = [](double x, uint32_t size, uint32_t clamp) { + if (clamp == 2u) return std::clamp(x, 0.0, double(size)); + const double period = double(size) * (clamp == 1u ? 2.0 : 1.0); + return x - std::floor(x / period) * period; + }; + u = reduce(u,level_width,clamp_x); v = reduce(v,level_height,clamp_y); + // Provisional Xenos query model: border contribution in X. The accepted + // repeat/mirror/edge modes never sample border; border clamp modes still reject. + if (query_border) return Imx51Gpu3dVec4{}; + if (query_weights) { + // Provisional Xenos layout: XY spatial factors at the lower mip, Z=0 + // for 2D/cube, W mip factor. Point filtering has no interpolation. + const float fx = mag ? static_cast(u - 0.5 - std::floor(u - 0.5)) : 0.0f; + const float fy = mag ? static_cast(v - 0.5 - std::floor(v - 0.5)) : 0.0f; + return Imx51Gpu3dVec4{fx,fy,0,static_cast(lod - std::floor(lod))}; + } + auto& memory = emu_.Get(); + const auto* data = tiled ? nullptr : memory.ReadSpan(base, + uint64_t(height - 1u) * pitch * bytes + uint64_t(width) * bytes, mmu_config); + auto index = [](int value, uint32_t size, uint32_t clamp) { + const int n = static_cast(size); + if (clamp == 2u) return std::clamp(value, 0, n - 1); + const int period = clamp == 1u ? n * 2 : n; + int wrapped = value % period; if (wrapped < 0) wrapped += period; + return wrapped >= n ? period - wrapped - 1 : wrapped; + }; + auto texel = [&](int x, int y) { + x = index(x,level_width,clamp_x); y = index(y,level_height,clamp_y); + const uint8_t* p = tiled ? memory.ReadSpan(Imx51Gpu3dTiledAddress(static_cast(base),level_pitch,bytes, + static_cast(x)+mip_x,static_cast(y)+mip_y),bytes,mmu_config) : + data + (uint64_t(y) * pitch + static_cast(x)) * bytes; + Imx51Gpu3dVec4 raw{}; + if (bytes == 4u) for (unsigned c = 0; c < 4; ++c) raw[c] = float(p[c]) / 255.0f; + else if (bytes == 2u) { + const uint32_t packed = uint32_t(p[0]) | (uint32_t(p[1]) << 8); + /* Mesa e97ad748, fd2_util.c: pipe2surface, CASE(8,8,0,0), FMT_8_8. */ + if (format == 10u) raw = {float(p[0])/255.0f,float(p[1])/255.0f,0.0f,1.0f}; + else if (format == 15u) for (unsigned i=0;i<4;++i) raw[i]=float((packed>>(i*4u))&15u)/15.0f; + else raw = {float(packed & 31u) / 31.0f,float((packed >> 5) & 63u) / 63.0f, + float((packed >> 11) & 31u) / 31.0f,1.0f}; + } else raw = {float(p[0]) / 255.0f,0.0f,0.0f,1.0f}; + Imx51Gpu3dVec4 result{}; + for (unsigned c = 0; c < 4; ++c) { + const uint32_t swizzle = (state[3] >> (1u + c * 3u)) & 7u; + if (swizzle > 5u) fail("unsupported swizzle", swizzle); + result[c] = swizzle < 4u ? raw[swizzle] : swizzle == 5u ? 1.0f : 0.0f; + } + return result; + }; + if (mag == 0u) return texel(static_cast(std::floor(u)),static_cast(std::floor(v))); + u -= 0.5; v -= 0.5; + const int x = static_cast(std::floor(u)), y = static_cast(std::floor(v)); + const float fx = static_cast(u - x), fy = static_cast(v - y); + const auto a = texel(x,y), b = texel(x+1,y), c = texel(x,y+1), d = texel(x+1,y+1); + Imx51Gpu3dVec4 result{}; + for (unsigned k = 0; k < 4; ++k) result[k] = std::lerp(std::lerp(a[k],b[k],fx),std::lerp(c[k],d[k],fx),fy); + return result; + }; + // Mesa instr-a2xx.h TEX_FILTER_POINT; GLES 2.0 equation 3.17: + // nearest-mipmap filters select the closest level, with ties going lower. + if (mip == 0u) lod = (std::max)(0.0, std::ceil(lod + 0.5) - 1.0); + const uint32_t lower = static_cast(std::floor(lod)); + auto result = sample_level(lower); + if (!query_weights && !query_border && lod > lower) { + const auto upper = sample_level(lower+1u); + for (unsigned c = 0; c < 4u; ++c) result[c] = std::lerp(result[c],upper[c],static_cast(lod-lower)); + } + return result; +} diff --git a/cerf/socs/imx51/imx51_gpu3d_texture.h b/cerf/socs/imx51/imx51_gpu3d_texture.h new file mode 100644 index 00000000..073a5ba8 --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_texture.h @@ -0,0 +1,12 @@ +#pragma once +#include "../../core/service.h" +#include "imx51_gpu3d_shader.h" +class Imx51Gpu3dTexture : public Service { +public: + using Service::Service; + bool ShouldRegister() override; + Imx51Gpu3dVec4 Sample(const std::unordered_map& registers, + uint32_t mmu_config, uint32_t slot, const Imx51Gpu3dVec4& coordinates, + std::array instruction, const Imx51Gpu3dVec4* dx = nullptr, const Imx51Gpu3dVec4* dy = nullptr, + float register_lod = 0); +}; diff --git a/cerf/socs/imx51/imx51_gpu3d_tiling.cpp b/cerf/socs/imx51/imx51_gpu3d_tiling.cpp new file mode 100644 index 00000000..2dbf291c --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_tiling.cpp @@ -0,0 +1,15 @@ +#include "imx51_gpu3d_tiling.h" + +/* Ford SYNC 2 librenderboy.dll: 0x41CE3440 (2D layout), + 0x41CE3A1C-0x41CE3ABC (CPU tiled upload/readback address). */ +uint64_t Imx51Gpu3dTiledAddress(uint32_t base, uint32_t pitch, uint32_t bytes, + uint32_t x, uint32_t y) { + const uint64_t macro = (uint64_t(pitch >> 5) * (y >> 5) + (x >> 5)) * (bytes << 7); + const uint64_t micro = ((x & 7u) + ((y & 6u) << 2)) * bytes; + const uint64_t offset = macro + ((y >> 3) & 1u) * (bytes << 6) + + ((micro + ((y & 1u) << 3)) << 1) - (micro & 15u) + (base >> 3); + const uint32_t bank = (((((y >> 2) & ~1u) + (x >> 3)) << 1) & 6u) + ((y >> 4) & 1u); + const uint64_t permutation = (bank & ~1u) + ((bank & 1u) << 6); + return (offset & 63u) + (((offset & 448u) + + (((offset & ~uint64_t{511}) + (permutation << 2)) << 1)) << 2); +} diff --git a/cerf/socs/imx51/imx51_gpu3d_tiling.h b/cerf/socs/imx51/imx51_gpu3d_tiling.h new file mode 100644 index 00000000..00d2da4e --- /dev/null +++ b/cerf/socs/imx51/imx51_gpu3d_tiling.h @@ -0,0 +1,5 @@ +#pragma once +#include + +uint64_t Imx51Gpu3dTiledAddress(uint32_t base, uint32_t pitch, uint32_t bytes, + uint32_t x, uint32_t y);