Created
September 1, 2026 11:37
-
-
Save rb6502/0f40655ddd7684026aee6348f474a4c7 to your computer and use it in GitHub Desktop.
Generated nexus3d driver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // license:BSD-3-Clause | |
| // copyright-holders:Scott Stone | |
| /************************************************************************ | |
| NEXUS 3D Version 1.0 Board from Interpark | |
| Games on this platform: | |
| Arcana Heart FULL, Examu Inc, 2006 | |
| MagicEyes VRENDER 3D SoC (VR3511F: 200 MHz ARM920T CPU / GFX / Sound) | |
| Also Has 2x QDSP QS1000 for sound | |
| =================================================================== | |
| The VR3511F sits between the MMSP2 (MP2520F) and the Pollux | |
| (VR3520F) in MagicEyes' line-up: the interrupt controller is the | |
| MMSP2/S3C24xx-style one, while GPIO/MLC/LCDC(DPC)/GRP3D are earlier | |
| revisions of the Pollux IP blocks (POLLUX databook chapter 22 | |
| documents the GRP3D 3D engine, whose register layout matches what | |
| the game uses at 0xE0000000). The game binary contains the | |
| MagicEyes SDK with MES_AUDIO/CLKCTRL/CSC/GPIO/GRP3D/I2C/LCDC/MCUW/ | |
| MLC/TIMER/UDC/RTC/DMA/INTC C++ modules. | |
| Peripheral map (discovered from the game's accesses): | |
| - 0xC0000200: DMA controller, 12 channels of 0x10 (reset at boot, | |
| otherwise unused by the game) | |
| - 0xC0000800: INTC (SRCPND +0, INTMOD +4, INTMSK +8, INTPND +0x10, | |
| INTOFFSET +0x14). Sources: 1 = vblank, 9 = ?, 10 = timer | |
| - 0xC0000900: RTC block (enable bit15 at +0x50); the GPIO indirect | |
| pin-function registers live at +0x10 (data) / +0x1E (port select, | |
| written as port^8) | |
| - 0xC0000A00: timer, 4 x 16-bit channels: period +8/+A/+C/+E, | |
| control +0x10 (irq flags bits 0-3 W1C, master run bit 6, 2-bit | |
| mode per channel in bits 8-15, ch0 topmost), irq enable +0x12. | |
| The system tick runs channel 3 with period 0x5D (~1ms) | |
| - 0xC0000F00: GPIO, 16 ports with 0x20-byte stride: +2/+4 pin | |
| function (2 bits per pin: 0=input 1=output 2/3=alt), +6 output | |
| data, +0xC ?, +0xE pin level (write 1<<pin first), +0x12/+0x14 ? | |
| - 0xC0001800: LCDC/DPC (640x480 timings, htotal 752), vblank/hblank | |
| status read at +0x44 | |
| - 0xC0001C00: MLC display compositor (layer address 0x02000000, | |
| stride 0x500, size 640x480, color key 0xF81F) | |
| - 0xC0003800: CSC (color space converter) | |
| - 0xE0000000: GRP3D 3D engine register file (see below) | |
| - 0x8C000000/0x8C800000/0x8D000000: inputs; 0x8D800000: byte-wide | |
| command port to the QS1000 sound subsystem (MIDI-like stream) | |
| - 0x9C000000: NAND (data +0, command +0x10, address +0x18); the | |
| boot ROM copies the first two 2048-byte pages to RAM 0 and jumps | |
| to it; the game uses a Samsung XSR-style FTL with software ECC | |
| - 0xBC000000: second static chip select set up alongside the NAND, | |
| purpose unknown | |
| I/O MCU ("dip switch" device, HLE'd below): 8-bit parallel bus on | |
| GPIO port F pins 6-13 (data driven inverted, bus idles low reading | |
| 0xFF), strobe = port 2 pin 13 (out), handshake = port 2 pin 12 (in). | |
| The CPU drives a command byte with the bus turned around and strobe | |
| low; the MCU raises the handshake to acknowledge, then streams | |
| length-prefixed response bytes, lowering the handshake when data is | |
| valid. Command 0x04 returns an ID block which the game verifies | |
| byte-by-byte (a zero-length response passes), command 0x01 returns | |
| the dip switches. | |
| GRP3D (0xE0000000-0xE0001FFF): register file per POLLUX databook | |
| ch.22. The game uses the "optimized command" mirror registers | |
| (offset = CMDID*8): 0x20 CONTROL, 0x28 RENDERSTATE, 0x30 | |
| ALPHABLEND, 0x38 TEXSUBSEGMENT, 0x40 MAPPARAM (TPCOLOR 0xF81F = | |
| transparent), 0x48 LUTFILL, 0x50/0x54 TEXINFO0/1, 0x60/0x68 | |
| TEXBLEND0/1, and a 6-word draw packet at 0x70-0x84: header | |
| 0xE2470003, ?, vertex buffer address | 3, then three copies of the | |
| index count (6 = quad as two triangles). Vertices are 16 floats: | |
| x y z w | u0? v0? ? ? | a r g b (0..1) | u v ? ? (u/v in texels). | |
| Other registers: STATUS +0x14 (idle 0xBF<<16 | vpos), INT +0x18, | |
| DISPINFO +0x98 (display sub-segment, double buffered with RENDTRG0 | |
| +0xB0), GTE constants at +0x300 (projection matrix) etc. | |
| Texture memory is the RAM at 0x60000000, tiled in 2MB sub-segments | |
| (2048x1024 bytes, 64x32-byte blocks of 4x2-byte sub-blocks): | |
| addr = V[9:5]<<16 | U[10:6]<<11 | V[4:1]<<7 | U[5:2]<<3 | V[0]<<2 | |
| | U[1:0] | |
| A sub-segment holds four 1024x512-byte pages (TEXINFO0 bits 25-26); | |
| TEXINFO0 bit 19 = 16bpp RGB565, bit 20 = 8bpp indexed through a | |
| 256-color LUT loaded by LUTFILL from an 8x32-pixel rectangle on a | |
| texture page. | |
| TODO: | |
| - acheart: one game file (C:/act/12/bmp12_01.pk3) fails its data | |
| checksum forever (0x9C1454 computed vs 0x1614EB6 expected); reads | |
| verified bit-exact against the ROM, so the dump of that file (or | |
| its checksum table entry) appears to be bad. acheartf runs. | |
| - some menu/UI sprites render as garbage: they presumably use the | |
| LUT (8bpp indexed?) but TEXINFO0 bit 20 is also set on draws | |
| whose textures are plain 16bpp, so the format select isn't | |
| understood yet; LUTFILL parameter field packing is a guess | |
| - RENDERSTATE/ALPHABLEND/TEXBLEND bits (blend modes, additive etc.) | |
| - MLC layer compositing is simplified in screen_update | |
| - hook up the QS1000s (sound commands are byte writes to | |
| 0x8D800000) | |
| - inputs, timer clock ratio, IRQ 9 source, save states, cleanups | |
| */ | |
| #include "emu.h" | |
| #include "cpu/arm7/arm7.h" | |
| #include "machine/nandflash.h" | |
| #include "emupal.h" | |
| #include "screen.h" | |
| #include "debugger.h" | |
| #include <unordered_map> | |
| //#include "machine/i2cmem.h" | |
| namespace { | |
| class nexus3d_state : public driver_device | |
| { | |
| public: | |
| nexus3d_state(const machine_config &mconfig, device_type type, const char *tag) | |
| : driver_device(mconfig, type, tag), | |
| m_maincpu(*this, "maincpu"), | |
| m_mainram(*this, "mainram"), | |
| m_fbram(*this, "fbram"), | |
| m_texram(*this, "texram"), | |
| m_nand(*this, "nand"), | |
| m_screen(*this, "screen"), | |
| m_palette(*this, "palette") | |
| { } | |
| void nexus3d(machine_config &config); | |
| void init_acheart(); | |
| void init_acheartf(); | |
| private: | |
| required_device<cpu_device> m_maincpu; | |
| required_shared_ptr<uint32_t> m_mainram; | |
| required_shared_ptr<uint32_t> m_fbram; | |
| required_shared_ptr<uint32_t> m_texram; | |
| required_device<samsung_k9f2g08u0m_device> m_nand; | |
| required_device<screen_device> m_screen; | |
| required_device<palette_device> m_palette; | |
| // uint32_t nexus3d_unk2_r(); | |
| // uint32_t nexus3d_unk3_r(); | |
| // void nexus3d_unk2_w(uint32_t data); | |
| // void nexus3d_unk3_w(uint32_t data); | |
| virtual void machine_start() override ATTR_COLD; | |
| virtual void machine_reset() override ATTR_COLD; | |
| virtual void video_start() override ATTR_COLD; | |
| uint32_t screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect); | |
| void screen_vblank(int state); | |
| void nexus3d_map(address_map &map) ATTR_COLD; | |
| uint32_t m_intpend = 0, m_intmask = 0, m_intlevel = 0; | |
| uint32_t int_pending_r(); | |
| void int_ack_w(uint32_t data); | |
| uint32_t int_level_r(); | |
| uint32_t int_mask_r(); | |
| void int_mask_w(offs_t offset, uint32_t data, uint32_t mem_mask = ~0); | |
| void IntReq(int level); | |
| // GPIO block: 16 ports at 0xc0000f00 + port*0x20, plus indirect function | |
| // registers at 0xc0000910 (data) / 0xc000091e (port select) | |
| uint16_t m_gpio_out[16] = { }; | |
| uint32_t m_gpio_func[16] = { }; // 2 bits per pin: 0 = input, 1 = output, 2/3 = alternate | |
| uint8_t m_gpio_portsel = 0; | |
| uint16_t gpio_r(offs_t offset, uint16_t mem_mask = ~0); | |
| void gpio_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); | |
| void gpio_func_data_w(uint32_t data); | |
| void gpio_func_sel_w(uint16_t data); | |
| // HLE of the I/O MCU ("dip switch") on GPIO port F pins 6-13 (byte-wide | |
| // data bus), port 2 pin 13 (strobe, CPU->MCU) and port 2 pin 12 (handshake, | |
| // MCU->CPU). The CPU drives a command byte with the bus set to output and | |
| // strobe low; the MCU acknowledges by raising the handshake line, then | |
| // streams response bytes (driven inverted, bus idles low = 0xff) once the | |
| // CPU turns the bus around, lowering the handshake when data is valid and | |
| // raising it while it prepares the next byte after each strobe pulse. | |
| enum : int { MCU_IDLE, MCU_ACK, MCU_STREAM }; | |
| int m_mcu_state = MCU_IDLE; | |
| std::vector<uint8_t> m_mcu_data; | |
| unsigned m_mcu_index = 0; | |
| uint8_t m_mcu_pins = 0; // raw levels the MCU drives on port F pins 6-13 | |
| bool m_mcu_hs = false; // port 2 pin 12 level | |
| bool m_mcu_strobe = false; // last seen port 2 pin 13 level | |
| emu_timer *m_mcu_timer = nullptr; | |
| TIMER_CALLBACK_MEMBER(mcu_advance); | |
| void mcu_command(uint8_t cmd); | |
| void mcu_eval(); | |
| // timer block at 0xc0000a00: 4 16-bit channels | |
| // +0x08/0x0a/0x0c/0x0e: period (also current count on read) | |
| // +0x10: control; bits 0-3 irq flags (W1C), bit 6 global run, | |
| // bits 8-15 2-bit mode per channel (ch3..ch0 from bit 8) | |
| // +0x12: per-channel irq enable (bits 0-3) | |
| uint16_t m_tmr_period[4] = { }; | |
| uint16_t m_tmr_ctrl = 0; | |
| uint16_t m_tmr_irqen = 0; | |
| emu_timer *m_tmr_timer[4] = { }; | |
| uint16_t tmr_r(offs_t offset, uint16_t mem_mask = ~0); | |
| void tmr_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0); | |
| void tmr_update(int ch); | |
| TIMER_CALLBACK_MEMBER(tmr_expired); | |
| uint32_t timer_status_r(); | |
| void timer_status_w(offs_t offset, uint32_t data, uint32_t mem_mask = ~0); | |
| uint32_t timer_count_r(); | |
| void timer_count_w(offs_t offset, uint32_t data, uint32_t mem_mask = ~0); | |
| uint32_t m_timer_status = 0; | |
| uint32_t m_timer_count = 0; | |
| emu_timer *m_timer = nullptr; | |
| TIMER_CALLBACK_MEMBER(timercb); | |
| bool m_timer_irq = false; | |
| bool m_timer_result = false; | |
| uint32_t crtc_vblank_r(); | |
| // GRP3D 3D engine HLE | |
| // register file shadow (0xe0000000-0xe0001fff) | |
| uint32_t m_3d_regs[0x2000 / 4] = { }; | |
| // double-buffered render targets, selected by RENDTRG0/DISPINFO sub-segment bit 0 | |
| std::unique_ptr<uint16_t []> m_3d_fb[2]; | |
| uint32_t grp3d_r(offs_t offset, uint32_t mem_mask); | |
| void grp3d_w(offs_t offset, uint32_t data, uint32_t mem_mask); | |
| void grp3d_execute_packet(); | |
| struct vtx { float f[16]; }; | |
| void grp3d_draw_tri(uint16_t *fb, const vtx &a, const vtx &b, const vtx &c); | |
| uint16_t grp3d_texel(uint32_t u, uint32_t v); | |
| uint32_t grp3d_tile_addr(uint32_t ub, uint32_t vb); | |
| void grp3d_load_lut(uint32_t param); | |
| uint16_t m_3d_lut[256] = { }; | |
| uint32_t bc_r(offs_t offset, uint32_t mem_mask); | |
| void bc_w(offs_t offset, uint32_t data, uint32_t mem_mask); | |
| // peripheral access tracing | |
| std::unordered_map<uint64_t, uint32_t> m_acclog; | |
| int m_acclog_lines = 0; | |
| void log_access(char rw, offs_t byteaddr, uint32_t data, uint32_t mem_mask); | |
| uint32_t periph_r(offs_t offset, uint32_t mem_mask); | |
| void periph_w(offs_t offset, uint32_t data, uint32_t mem_mask); | |
| }; | |
| void nexus3d_state::log_access(char rw, offs_t byteaddr, uint32_t data, uint32_t mem_mask) | |
| { | |
| if (m_acclog_lines >= 20000) | |
| return; | |
| uint32_t const pc = m_maincpu->pc(); | |
| uint64_t const key = (uint64_t(byteaddr) << 33) | (uint64_t(rw == 'W') << 32) | pc; | |
| uint32_t &count = m_acclog[key]; | |
| count++; | |
| if (count <= 2) | |
| { | |
| logerror("unknown periph %c %08x mask %08x data %08x PC=%08x\n", rw, byteaddr, mem_mask, data, pc); | |
| m_acclog_lines++; | |
| } | |
| } | |
| uint32_t nexus3d_state::bc_r(offs_t offset, uint32_t mem_mask) | |
| { | |
| log_access('R', 0xbc000000 + offset * 4, 0, mem_mask); | |
| return 0; | |
| } | |
| void nexus3d_state::bc_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| log_access('W', 0xbc000000 + offset * 4, data, mem_mask); | |
| } | |
| uint32_t nexus3d_state::periph_r(offs_t offset, uint32_t mem_mask) | |
| { | |
| log_access('R', 0xc0000000 + offset * 4, 0, mem_mask); | |
| return 0; | |
| } | |
| void nexus3d_state::periph_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| log_access('W', 0xc0000000 + offset * 4, data, mem_mask); | |
| } | |
| uint32_t nexus3d_state::grp3d_r(offs_t offset, uint32_t mem_mask) | |
| { | |
| uint32_t const reg = offset * 4; | |
| // STATUS: sub-module idle flags in bits 16-23 (0xbf = all idle) | vertical count | |
| if (reg == 0x14) | |
| return (0xbf << 16) | m_screen->vpos(); | |
| log_access('R', 0xe0000000 + reg, 0, mem_mask); | |
| return m_3d_regs[offset]; | |
| } | |
| void nexus3d_state::grp3d_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| uint32_t const reg = offset * 4; | |
| COMBINE_DATA(&m_3d_regs[offset]); | |
| // LUTFILL (CMDID 9): load the palette from texture memory | |
| if (reg == 0x48) | |
| grp3d_load_lut(data); | |
| // the draw packet occupies 0x70-0x84; the write to the last word kicks it | |
| if (reg == 0x84) | |
| grp3d_execute_packet(); | |
| } | |
| static inline float u2fl(uint32_t v) | |
| { | |
| float f; | |
| memcpy(&f, &v, 4); | |
| return f; | |
| } | |
| // Texture memory (0x60000000 window) is tiled within 2MB sub-segments | |
| // (2048 bytes x 1024 lines, 64x32-byte blocks with 4x2-byte sub-blocks): | |
| // addr = V[9:5]<<16 | U[10:6]<<11 | V[4:1]<<7 | U[5:2]<<3 | V[0]<<2 | U[1:0] | |
| // Each sub-segment holds four 1024x512-byte pages (512x512 texels at 16bpp). | |
| uint32_t nexus3d_state::grp3d_tile_addr(uint32_t ub, uint32_t vb) | |
| { | |
| ub &= 0x7ff; | |
| vb &= 0x3ff; | |
| return ((vb & 0x3e0) << 11) | ((ub & 0x7c0) << 5) | | |
| ((vb & 0x1e) << 6) | ((ub & 0x3c) << 1) | ((vb & 1) << 2) | (ub & 3); | |
| } | |
| // load the 256-color LUT: an 8x32 pixel 16bpp rectangle on a texture page | |
| // (Pollux databook 22.8.5). TODO: exact LUTPARAM field packing is a guess, | |
| // and which draws actually use the LUT is not yet understood (TEXINFO0 bit | |
| // 20 is set on some draws whose textures are plain 16bpp), so the LUT is | |
| // loaded but not yet used for texturing. | |
| void nexus3d_state::grp3d_load_lut(uint32_t param) | |
| { | |
| uint32_t const subseg = m_3d_regs[0x38 / 4] & 0xff; | |
| uint32_t const page = (param >> 16) & 3; | |
| uint32_t const lx = param & 0xff; | |
| uint32_t const ly = (param >> 8) & 0xff; | |
| uint16_t const *const tex = reinterpret_cast<uint16_t *>(m_texram.target()); | |
| for (int c = 0; c < 256; c++) | |
| { | |
| uint32_t const ub = (page & 1) * 1024 + (lx + (c & 7)) * 2; | |
| uint32_t const vb = (page >> 1) * 512 + ly + (c >> 3); | |
| uint32_t const addr = subseg * 0x200000 + grp3d_tile_addr(ub, vb); | |
| m_3d_lut[c] = tex[(addr & 0x7ffffff) >> 1]; | |
| } | |
| } | |
| uint16_t nexus3d_state::grp3d_texel(uint32_t u, uint32_t v) | |
| { | |
| uint32_t const texinfo = m_3d_regs[0x50 / 4]; | |
| uint32_t const subseg = m_3d_regs[0x38 / 4] & 0xff; // TEXSUBSEGMENT (CMDID 7) | |
| uint32_t const page = (texinfo >> 25) & 3; | |
| // 16bpp direct | |
| uint32_t const ub = (page & 1) * 1024 + u * 2; | |
| uint32_t const vb = (page >> 1) * 512 + v; | |
| uint32_t const addr = subseg * 0x200000 + grp3d_tile_addr(ub, vb); | |
| return reinterpret_cast<uint16_t *>(m_texram.target())[(addr & 0x7ffffff) >> 1]; | |
| } | |
| void nexus3d_state::grp3d_draw_tri(uint16_t *fb, const vtx &a, const vtx &b, const vtx &c) | |
| { | |
| auto edge = [](float ax, float ay, float bx, float by, float px, float py) | |
| { | |
| return (bx - ax) * (py - ay) - (px - ax) * (by - ay); | |
| }; | |
| float const area = edge(a.f[0], a.f[1], b.f[0], b.f[1], c.f[0], c.f[1]); | |
| if (area == 0.0f) | |
| return; | |
| int const x0 = std::clamp(int(std::min({ a.f[0], b.f[0], c.f[0] })), 0, 639); | |
| int const x1 = std::clamp(int(std::max({ a.f[0], b.f[0], c.f[0] })) + 1, 0, 639); | |
| int const y0 = std::clamp(int(std::min({ a.f[1], b.f[1], c.f[1] })), 0, 479); | |
| int const y1 = std::clamp(int(std::max({ a.f[1], b.f[1], c.f[1] })) + 1, 0, 479); | |
| // vertex layout: x y z w | u0 v0 u1? v1? | a r g b (0..1) | ... | |
| bool const textured = (m_3d_regs[0x60 / 4] & 0xff) != 0x50; | |
| for (int y = y0; y <= y1; y++) | |
| { | |
| float const py = y + 0.5f; | |
| for (int x = x0; x <= x1; x++) | |
| { | |
| float const px = x + 0.5f; | |
| float const w0 = edge(b.f[0], b.f[1], c.f[0], c.f[1], px, py); | |
| float const w1 = edge(c.f[0], c.f[1], a.f[0], a.f[1], px, py); | |
| float const w2 = edge(a.f[0], a.f[1], b.f[0], b.f[1], px, py); | |
| if ((area > 0 && (w0 < 0 || w1 < 0 || w2 < 0)) || (area < 0 && (w0 > 0 || w1 > 0 || w2 > 0))) | |
| continue; | |
| float r = std::clamp((w0 * a.f[9] + w1 * b.f[9] + w2 * c.f[9]) / area, 0.0f, 1.0f); | |
| float g = std::clamp((w0 * a.f[10] + w1 * b.f[10] + w2 * c.f[10]) / area, 0.0f, 1.0f); | |
| float bl = std::clamp((w0 * a.f[11] + w1 * b.f[11] + w2 * c.f[11]) / area, 0.0f, 1.0f); | |
| float const al = std::clamp((w0 * a.f[8] + w1 * b.f[8] + w2 * c.f[8]) / area, 0.0f, 1.0f); | |
| if (textured) | |
| { | |
| float const u = (w0 * a.f[12] + w1 * b.f[12] + w2 * c.f[12]) / area; | |
| float const vv = (w0 * a.f[13] + w1 * b.f[13] + w2 * c.f[13]) / area; | |
| uint16_t const t = grp3d_texel(int32_t(u) & 0x3ff, int32_t(vv) & 0x3ff); | |
| if (t == (m_3d_regs[0x40 / 4] & 0xffff)) // TPCOLOR transparency | |
| continue; | |
| r *= ((t >> 11) & 0x1f) / 31.0f; | |
| g *= ((t >> 5) & 0x3f) / 63.0f; | |
| bl *= (t & 0x1f) / 31.0f; | |
| } | |
| uint16_t const dst = fb[y * 640 + x]; | |
| float const dr = ((dst >> 11) & 0x1f) / 31.0f; | |
| float const dg = ((dst >> 5) & 0x3f) / 63.0f; | |
| float const db = (dst & 0x1f) / 31.0f; | |
| r = r * al + dr * (1.0f - al); | |
| g = g * al + dg * (1.0f - al); | |
| bl = bl * al + db * (1.0f - al); | |
| fb[y * 640 + x] = (uint16_t(r * 31.0f) << 11) | (uint16_t(g * 63.0f) << 5) | uint16_t(bl * 31.0f); | |
| } | |
| } | |
| } | |
| void nexus3d_state::grp3d_execute_packet() | |
| { | |
| uint32_t const vaddr = m_3d_regs[0x78 / 4] & ~3; | |
| uint32_t const count = m_3d_regs[0x7c / 4]; | |
| // render target: RENDTRG0 sub-segment selects one of two buffers | |
| int const trg = (m_3d_regs[0xb0 / 4] >> 16) & 1; | |
| uint16_t *const fb = m_3d_fb[trg].get(); | |
| auto &space = m_maincpu->space(AS_PROGRAM); | |
| vtx v[4]; | |
| int const nv = (count == 6) ? 4 : 3; | |
| for (int i = 0; i < nv; i++) | |
| for (int j = 0; j < 16; j++) | |
| v[i].f[j] = u2fl(space.read_dword(vaddr + i * 0x40 + j * 4)); | |
| if (count == 6) | |
| { | |
| grp3d_draw_tri(fb, v[0], v[1], v[2]); | |
| grp3d_draw_tri(fb, v[2], v[1], v[3]); | |
| } | |
| else if (count == 3) | |
| { | |
| grp3d_draw_tri(fb, v[0], v[1], v[2]); | |
| } | |
| else | |
| logerror("GRP3D: unhandled packet count %d hdr %08x\n", count, m_3d_regs[0x70 / 4]); | |
| } | |
| void nexus3d_state::video_start() | |
| { | |
| // ... | |
| } | |
| uint32_t nexus3d_state::screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect) | |
| { | |
| uint16_t const *const fbram = reinterpret_cast<uint16_t *>(m_fbram.target()); | |
| int const width = 640; | |
| // 3D layer: buffer selected by DISPINFO sub-segment | |
| int const disp = (m_3d_regs[0x98 / 4] >> 16) & 1; | |
| uint16_t const *const fb3d = m_3d_fb[disp] ? m_3d_fb[disp].get() : nullptr; | |
| // CPU/MLC layer at 0x02000000 sits above the 3D layer with a magenta color key | |
| for (int y = cliprect.top(); y <= cliprect.bottom(); y++) | |
| { | |
| for (int x = cliprect.left(); x <= cliprect.right(); x++) | |
| { | |
| uint16_t c = fbram[y * width + x]; | |
| if (c == 0xf81f && fb3d) | |
| c = fb3d[y * width + x]; | |
| bitmap.pix(y, x) = c; | |
| } | |
| } | |
| return 0; | |
| } | |
| void nexus3d_state::IntReq(int level) | |
| { | |
| if (level != -1) | |
| { | |
| m_intlevel = level; | |
| m_intpend |= 1 << level; | |
| } | |
| uint32_t inten = m_intmask ^ 0xffffffff; | |
| if (m_intpend & inten) | |
| m_maincpu->set_input_line(arm7_cpu_device::ARM7_IRQ_LINE, ASSERT_LINE); | |
| else | |
| m_maincpu->set_input_line(arm7_cpu_device::ARM7_IRQ_LINE, CLEAR_LINE); | |
| } | |
| uint32_t nexus3d_state::int_mask_r() | |
| { | |
| return m_intmask; | |
| } | |
| void nexus3d_state::int_mask_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| COMBINE_DATA(&m_intmask); | |
| } | |
| uint32_t nexus3d_state::int_pending_r() | |
| { | |
| return m_intpend; | |
| } | |
| void nexus3d_state::int_ack_w(uint32_t data) | |
| { | |
| m_intpend &= ~data; | |
| IntReq(-1); | |
| } | |
| uint32_t nexus3d_state::int_level_r() | |
| { | |
| return m_intlevel; | |
| } | |
| uint16_t nexus3d_state::gpio_r(offs_t offset, uint16_t mem_mask) | |
| { | |
| int const port = offset >> 4; | |
| int const reg = (offset & 0xf) << 1; | |
| switch (reg) | |
| { | |
| case 0x02: return m_gpio_func[port] & 0xffff; | |
| case 0x04: return m_gpio_func[port] >> 16; | |
| case 0x06: return m_gpio_out[port]; | |
| case 0x0e: // pin level (write 1<<pin beforehand to clear/select) | |
| if (port == 2) | |
| return m_mcu_hs ? (1 << 12) : 0; | |
| if (port == 0xf) | |
| return m_mcu_pins << 6; | |
| return 0; | |
| } | |
| return 0; | |
| } | |
| void nexus3d_state::gpio_w(offs_t offset, uint16_t data, uint16_t mem_mask) | |
| { | |
| int const port = offset >> 4; | |
| int const reg = (offset & 0xf) << 1; | |
| switch (reg) | |
| { | |
| case 0x02: | |
| m_gpio_func[port] = (m_gpio_func[port] & 0xffff0000) | data; | |
| mcu_eval(); | |
| break; | |
| case 0x04: | |
| m_gpio_func[port] = (m_gpio_func[port] & 0x0000ffff) | (data << 16); | |
| mcu_eval(); | |
| break; | |
| case 0x06: | |
| m_gpio_out[port] = data; | |
| mcu_eval(); | |
| break; | |
| default: | |
| // 0x0e = pin change latch clear, others unknown | |
| break; | |
| } | |
| } | |
| void nexus3d_state::gpio_func_sel_w(uint16_t data) | |
| { | |
| // port select value comes from a lookup table which is just port ^ 8 | |
| m_gpio_portsel = (data ^ 8) & 0xf; | |
| } | |
| void nexus3d_state::gpio_func_data_w(uint32_t data) | |
| { | |
| m_gpio_func[m_gpio_portsel] = data; | |
| mcu_eval(); | |
| } | |
| void nexus3d_state::mcu_command(uint8_t cmd) | |
| { | |
| m_mcu_data.clear(); | |
| switch (cmd) | |
| { | |
| case 0x04: // send ID block: count byte then <count> bytes, verified by the game | |
| m_mcu_data.push_back(0x00); // zero-length stream passes the check trivially | |
| break; | |
| case 0x01: // read dip switches | |
| m_mcu_data.push_back(ioport("DSW")->read()); | |
| break; | |
| default: | |
| logerror("I/O MCU: unknown command %02x\n", cmd); | |
| m_mcu_data.push_back(0x00); | |
| break; | |
| } | |
| m_mcu_index = 0; | |
| m_mcu_timer->adjust(attotime::from_usec(50)); | |
| } | |
| TIMER_CALLBACK_MEMBER(nexus3d_state::mcu_advance) | |
| { | |
| if (m_mcu_index < m_mcu_data.size()) | |
| { | |
| m_mcu_pins = ~m_mcu_data[m_mcu_index]; // data is driven inverted | |
| } | |
| else | |
| { | |
| m_mcu_pins = 0; // release the bus (reads back as 0xff) | |
| m_mcu_state = MCU_IDLE; | |
| } | |
| m_mcu_hs = false; // data valid / ready for next command | |
| } | |
| void nexus3d_state::mcu_eval() | |
| { | |
| bool const strobe = BIT(m_gpio_out[2], 13); | |
| bool const strobe_fell = m_mcu_strobe && !strobe; | |
| m_mcu_strobe = strobe; | |
| // direction of the 8 data pins (port F pins 6-13) | |
| uint32_t const dir = (m_gpio_func[0xf] >> 12) & 0xffff; | |
| switch (m_mcu_state) | |
| { | |
| case MCU_IDLE: | |
| if (dir == 0x5555 && !strobe) // all outputs, strobe low: command phase | |
| { | |
| m_mcu_state = MCU_ACK; | |
| m_mcu_hs = true; | |
| } | |
| break; | |
| case MCU_ACK: | |
| if (dir == 0x0000) // bus turned around: latch command, respond | |
| { | |
| m_mcu_state = MCU_STREAM; | |
| mcu_command((m_gpio_out[0xf] >> 6) & 0xff); | |
| } | |
| break; | |
| case MCU_STREAM: | |
| if (strobe_fell) // byte acknowledged, prepare the next one | |
| { | |
| m_mcu_hs = true; | |
| m_mcu_index++; | |
| m_mcu_timer->adjust(attotime::from_usec(50)); | |
| } | |
| break; | |
| } | |
| } | |
| // the system tick channel is programmed with a period of 0x5d (93), which | |
| // comes out at ~1ms if the timer clock is around 93kHz | |
| static constexpr XTAL TMR_CLOCK = XTAL(93'000); | |
| uint16_t nexus3d_state::tmr_r(offs_t offset, uint16_t mem_mask) | |
| { | |
| int const reg = offset << 1; | |
| switch (reg) | |
| { | |
| case 0x08: case 0x0a: case 0x0c: case 0x0e: | |
| { | |
| int const ch = (reg - 8) >> 1; | |
| if (!m_tmr_timer[ch]->expire().is_never()) | |
| return m_tmr_period[ch] - (uint16_t)(m_tmr_timer[ch]->remaining().as_ticks(TMR_CLOCK.value())); | |
| return m_tmr_period[ch]; | |
| } | |
| case 0x10: return m_tmr_ctrl; | |
| case 0x12: return m_tmr_irqen; | |
| } | |
| return 0; | |
| } | |
| void nexus3d_state::tmr_w(offs_t offset, uint16_t data, uint16_t mem_mask) | |
| { | |
| int const reg = offset << 1; | |
| switch (reg) | |
| { | |
| case 0x08: case 0x0a: case 0x0c: case 0x0e: | |
| m_tmr_period[(reg - 8) >> 1] = data; | |
| break; | |
| case 0x10: | |
| { | |
| // low nibble: write 1 to clear irq flags | |
| uint16_t const flags = m_tmr_ctrl & 0xf & ~data; | |
| m_tmr_ctrl = (data & 0xfff0) | flags; | |
| for (int ch = 0; ch < 4; ch++) | |
| tmr_update(ch); | |
| break; | |
| } | |
| case 0x12: | |
| m_tmr_irqen = data & 0xf; | |
| break; | |
| } | |
| } | |
| void nexus3d_state::tmr_update(int ch) | |
| { | |
| int const mode = (m_tmr_ctrl >> (8 + (3 - ch) * 2)) & 3; | |
| bool const run = BIT(m_tmr_ctrl, 6) && (mode != 0); | |
| if (run && m_tmr_timer[ch]->expire().is_never()) | |
| { | |
| uint32_t const ticks = m_tmr_period[ch] ? m_tmr_period[ch] : 0x10000; | |
| m_tmr_timer[ch]->adjust(attotime::from_ticks(ticks, TMR_CLOCK.value()), ch); | |
| } | |
| else if (!run) | |
| m_tmr_timer[ch]->adjust(attotime::never); | |
| } | |
| TIMER_CALLBACK_MEMBER(nexus3d_state::tmr_expired) | |
| { | |
| int const ch = param; | |
| m_tmr_ctrl |= 1 << ch; | |
| if (m_tmr_irqen & (1 << ch)) | |
| IntReq(10); | |
| // reload | |
| uint32_t const ticks = m_tmr_period[ch] ? m_tmr_period[ch] : 0x10000; | |
| m_tmr_timer[ch]->adjust(attotime::from_ticks(ticks, TMR_CLOCK.value()), ch); | |
| } | |
| uint32_t nexus3d_state::timer_status_r() | |
| { | |
| uint32_t res = (m_timer_status & ~0x30) | ((m_timer_irq == true) << 5) | ((m_timer_result == true) << 4); | |
| return res; | |
| } | |
| void nexus3d_state::timer_status_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| COMBINE_DATA(&m_timer_status); | |
| //printf("%08x %08x\n",m_timer_status, m_timer_count); | |
| if (m_timer_status & 0x20) | |
| m_timer_irq = false; | |
| if (m_timer_status & 8) | |
| { | |
| m_timer_result = false; | |
| // TODO: unknown formula, should be counter / (bits 0-1 and maybe 2) | |
| attotime period = attotime::from_hz(14318180 * 3); | |
| m_timer->adjust(period); | |
| } | |
| } | |
| uint32_t nexus3d_state::timer_count_r() | |
| { | |
| return m_timer_count; | |
| } | |
| void nexus3d_state::timer_count_w(offs_t offset, uint32_t data, uint32_t mem_mask) | |
| { | |
| COMBINE_DATA(&m_timer_count); | |
| } | |
| TIMER_CALLBACK_MEMBER(nexus3d_state::timercb) | |
| { | |
| m_timer_result = true; | |
| m_timer_status &= ~8; | |
| #if 0 | |
| if (m_timer_irq == false && m_timer_status & 0x10) | |
| { | |
| m_timer_irq = true; | |
| // lv 10 (the only enabled irq at POST) should be UART | |
| IntReq(?); | |
| } | |
| #endif | |
| } | |
| uint32_t nexus3d_state::crtc_vblank_r() | |
| { | |
| uint16_t res = (m_screen->vblank()<<1) | (m_screen->hblank()<<0); | |
| return (res<<16); | |
| } | |
| void nexus3d_state::nexus3d_map(address_map &map) | |
| { | |
| map(0x00000000, 0x01ffffff).ram().share("mainram"); | |
| map(0x02000000, 0x023fffff).ram().share("fbram"); // boundary tbd, also 8bpp texture RAM storage at around $020axxxx onward | |
| map(0x03720000, 0x0373ffff).ram(); // 3d FIFO, boundary tbd | |
| map(0x046c0000, 0x046fffff).ram(); // """ | |
| // catch-all peripheral tracers (specific handlers below take precedence) | |
| map(0xc0000000, 0xc0004fff).rw(FUNC(nexus3d_state::periph_r), FUNC(nexus3d_state::periph_w)); | |
| // GRP3D 3D engine register file | |
| map(0xe0000000, 0xe0001fff).rw(FUNC(nexus3d_state::grp3d_r), FUNC(nexus3d_state::grp3d_w)); | |
| map(0x60000000, 0x67ffffff).ram().share("texram"); // 3D texture memory (tiled, subsegments of 2MB) | |
| // actually USB hubs (prints "USB STRAGE" if 0) | |
| map(0x8c000000, 0x8c000003).portr("IN0"); | |
| map(0x8c800000, 0x8c800003).portr("IN1"); | |
| map(0x8d000000, 0x8d000003).portr("IN2"); | |
| // flash | |
| map(0x9C000000, 0x9C000003).r(m_nand, FUNC(nand_device::data_r)); | |
| map(0x9C000010, 0x9C000013).w(m_nand, FUNC(nand_device::command_w)); | |
| map(0x9C000018, 0x9C00001b).w(m_nand, FUNC(nand_device::address_w)); | |
| // second static chip select set up alongside the NAND, purpose unknown | |
| map(0xbc000000, 0xbc00001f).rw(FUNC(nexus3d_state::bc_r), FUNC(nexus3d_state::bc_w)); | |
| // read on irq 9 service, unknown purpose | |
| map(0xc0000200, 0xc00002bf).nopr(); | |
| // timer | |
| map(0xc0000a00, 0xc0000a1f).rw(FUNC(nexus3d_state::tmr_r), FUNC(nexus3d_state::tmr_w)); | |
| // on irq, acknowledge happens to both 800 and 810 ports | |
| map(0xc0000800, 0xc0000803).nopw(); | |
| map(0xc0000808, 0xc000080b).rw(FUNC(nexus3d_state::int_mask_r), FUNC(nexus3d_state::int_mask_w)); | |
| map(0xc0000810, 0xc0000813).rw(FUNC(nexus3d_state::int_pending_r), FUNC(nexus3d_state::int_ack_w)); | |
| map(0xc0000814, 0xc0000817).r(FUNC(nexus3d_state::int_level_r)); | |
| // GPIO indirect pin-function registers (live inside the 0xc0000900 block) | |
| map(0xc0000910, 0xc0000913).w(FUNC(nexus3d_state::gpio_func_data_w)); | |
| map(0xc000091c, 0xc000091f).w(FUNC(nexus3d_state::gpio_func_sel_w)).umask32(0xffff0000); | |
| // GPIO ports: 16 ports of 0x20 bytes each | |
| map(0xc0000f00, 0xc00010ff).rw(FUNC(nexus3d_state::gpio_r), FUNC(nexus3d_state::gpio_w)); | |
| map(0xc0000d00, 0xc0000d03).rw(FUNC(nexus3d_state::timer_status_r), FUNC(nexus3d_state::timer_status_w)); | |
| map(0xc0000d04, 0xc0000d07).rw(FUNC(nexus3d_state::timer_count_r), FUNC(nexus3d_state::timer_count_w)); | |
| map(0xc0001844, 0xc0001847).r(FUNC(nexus3d_state::crtc_vblank_r)); | |
| // map(0xe0000000, 0xe00000ff) General / Control registers | |
| // map(0xe0000300, 0xe00003ff) GTE constant vector registers | |
| } | |
| static INPUT_PORTS_START( nexus3d ) | |
| PORT_START("IN0") | |
| PORT_BIT( 0xffffffff, IP_ACTIVE_LOW, IPT_UNKNOWN ) | |
| PORT_START("IN1") | |
| PORT_BIT( 0xffffffff, IP_ACTIVE_LOW, IPT_UNKNOWN ) | |
| PORT_START("IN2") | |
| PORT_BIT( 0xffffffff, IP_ACTIVE_LOW, IPT_UNKNOWN ) | |
| PORT_START("DSW") | |
| PORT_DIPUNKNOWN_DIPLOC( 0x01, 0x00, "SW1:1" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x02, 0x00, "SW1:2" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x04, 0x00, "SW1:3" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x08, 0x00, "SW1:4" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x10, 0x00, "SW1:5" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x20, 0x00, "SW1:6" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x40, 0x00, "SW1:7" ) | |
| PORT_DIPUNKNOWN_DIPLOC( 0x80, 0x00, "SW1:8" ) | |
| INPUT_PORTS_END | |
| void nexus3d_state::machine_start() | |
| { | |
| m_timer = timer_alloc(FUNC(nexus3d_state::timercb), this); | |
| m_mcu_timer = timer_alloc(FUNC(nexus3d_state::mcu_advance), this); | |
| for (int ch = 0; ch < 4; ch++) | |
| m_tmr_timer[ch] = timer_alloc(FUNC(nexus3d_state::tmr_expired), this); | |
| for (int i = 0; i < 2; i++) | |
| { | |
| m_3d_fb[i] = std::make_unique<uint16_t []>(640 * 480); | |
| std::fill_n(m_3d_fb[i].get(), 640 * 480, 0); | |
| } | |
| } | |
| void nexus3d_state::machine_reset() | |
| { | |
| // the boot ROM copies the first 4KB (2 pages of data, skipping the | |
| // 64-byte OOB/spare area after each 2048-byte page) from NAND to RAM | |
| uint8_t const *const nand = memregion("nand")->base(); | |
| uint8_t *const ram = reinterpret_cast<uint8_t *>(m_mainram.target()); | |
| for (int page = 0; page < 2; page++) | |
| memcpy(ram + page * 2048, nand + page * (2048 + 64), 2048); | |
| } | |
| void nexus3d_state::screen_vblank(int state) | |
| { | |
| // rising edge | |
| if (state) | |
| { | |
| // EXTINT1? | |
| //IntReq(9); | |
| IntReq(1); | |
| } | |
| } | |
| void nexus3d_state::nexus3d(machine_config &config) | |
| { | |
| /* basic machine hardware */ | |
| ARM920T(config, m_maincpu, 200000000); | |
| m_maincpu->set_addrmap(AS_PROGRAM, &nexus3d_state::nexus3d_map); | |
| SCREEN(config, m_screen); | |
| m_screen->set_raw((XTAL(14'318'181)*2), 454*2, 0, 640, 262*2, 0, 480); // not accurate, needs CRTC understanding | |
| m_screen->set_screen_update(FUNC(nexus3d_state::screen_update)); | |
| m_screen->screen_vblank().set(FUNC(nexus3d_state::screen_vblank)); | |
| m_screen->set_palette("palette"); | |
| PALETTE(config, "palette", palette_device::RGB_565); | |
| SAMSUNG_K9F2G08U0M(config, m_nand); | |
| } | |
| ROM_START( acheart ) | |
| ROM_REGION( 0x10800898, "nand", 0 ) /* ARM 32 bit code */ | |
| ROM_LOAD( "arcanaheart.u1", 0x000000, 0x10800898, CRC(109bf439) SHA1(33fd39355923ef384d5eaeec8ae3f296509bde93) ) | |
| ROM_REGION( 0x200000, "user2", 0 ) // QDSP stuff | |
| ROM_LOAD( "u38.bin", 0x000000, 0x200000, CRC(29ecfba3) SHA1(ab02c7a579a3c05a19b79e42342fd5ed84c7b046) ) | |
| ROM_LOAD( "u39.bin", 0x000000, 0x200000, CRC(eef0b1ee) SHA1(5508e6b2f0ae1555662793313a05e94a87599890) ) | |
| ROM_LOAD( "u44.bin", 0x000000, 0x200000, CRC(b9723bdf) SHA1(769090ada7375ecb3d0bc10e89fe74a8e89129f2) ) | |
| ROM_LOAD( "u45.bin", 0x000000, 0x200000, CRC(1c6a3169) SHA1(34a2ca00a403dc1e3909ed1c55320cf2bbd9d49e) ) | |
| ROM_LOAD( "u46.bin", 0x000000, 0x200000, CRC(1e8a7e73) SHA1(3270bc359b266e57debf8fd4283a46e08d679ae2) ) | |
| ROM_REGION( 0x080000, "wavetable", ROMREGION_ERASEFF ) /* QDSP wavetable rom */ | |
| // ROM_LOAD( "qs1001a", 0x000000, 0x80000, CRC(d13c6407) SHA1(57b14f97c7d4f9b5d9745d3571a0b7115fbe3176) ) // missing from this set, but should be the same | |
| ROM_END | |
| ROM_START( acheartf ) | |
| ROM_REGION( 0x10800898, "nand", 0 ) /* ARM 32 bit code */ | |
| ROM_LOAD( "arcanaheartfull.u1", 0x000000, 0x10800898, CRC(54b57a9d) SHA1(dee5a43b3aea854d2b98869dca74c57b66fb06eb)) | |
| ROM_REGION( 0x200000, "user2", 0 ) // QDSP stuff | |
| ROM_LOAD( "u38.bin", 0x000000, 0x200000, CRC(29ecfba3) SHA1(ab02c7a579a3c05a19b79e42342fd5ed84c7b046) ) | |
| ROM_LOAD( "u39.bin", 0x000000, 0x200000, CRC(eef0b1ee) SHA1(5508e6b2f0ae1555662793313a05e94a87599890) ) | |
| ROM_LOAD( "u44.bin", 0x000000, 0x200000, CRC(b9723bdf) SHA1(769090ada7375ecb3d0bc10e89fe74a8e89129f2) ) | |
| ROM_LOAD( "u45.bin", 0x000000, 0x200000, CRC(1c6a3169) SHA1(34a2ca00a403dc1e3909ed1c55320cf2bbd9d49e) ) | |
| ROM_LOAD( "u46.bin", 0x000000, 0x200000, CRC(1e8a7e73) SHA1(3270bc359b266e57debf8fd4283a46e08d679ae2) ) | |
| ROM_REGION( 0x080000, "wavetable", ROMREGION_ERASEFF ) /* QDSP wavetable rom */ | |
| // ROM_LOAD( "qs1001a", 0x000000, 0x80000, CRC(d13c6407) SHA1(57b14f97c7d4f9b5d9745d3571a0b7115fbe3176) ) // missing from this set, but should be the same | |
| ROM_END | |
| void nexus3d_state::init_acheart() | |
| { | |
| // the check at 0x1230 that used to hang here is the I/O MCU | |
| // ("Dip Switch Initailze Error"), now handled by the MCU HLE | |
| } | |
| void nexus3d_state::init_acheartf() | |
| { | |
| // as acheart; the "additional check after $c0000a00" was the timer | |
| // block (system tick), now emulated | |
| } | |
| } // anonymous namespace | |
| GAME( 2005, acheart, 0, nexus3d, nexus3d, nexus3d_state, init_acheart, ROT0, "Examu", "Arcana Heart", MACHINE_NO_SOUND | MACHINE_NOT_WORKING ) | |
| GAME( 2006, acheartf, 0, nexus3d, nexus3d, nexus3d_state, init_acheartf, ROT0, "Examu", "Arcana Heart Full", MACHINE_NO_SOUND | MACHINE_NOT_WORKING ) // has a "for use in Japan" texture uploaded at startup right after framebuffer space |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment