Skip to content

Instantly share code, notes, and snippets.

@rb6502
Created September 1, 2026 11:37
Show Gist options
  • Select an option

  • Save rb6502/0f40655ddd7684026aee6348f474a4c7 to your computer and use it in GitHub Desktop.

Select an option

Save rb6502/0f40655ddd7684026aee6348f474a4c7 to your computer and use it in GitHub Desktop.
Generated nexus3d driver
// 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