From 972e35e4519d72b04981fe8eaad978427e9e2750 Mon Sep 17 00:00:00 2001 From: arbee Date: Mon, 14 Aug 2023 21:10:59 -0400 Subject: apple/f108.cpp: New device for the Apple F108 memory controller / northbridge. [R. Belmont] apple/valkyrie.cpp: New device for Apple Valkyrie framebuffer ASIC. [R. Belmont] apple/iosb.cpp: Added support for the PrimeTime II I/O ASIC. [R. Belmont] New machines added as working ----------------------------- Apple Computer Macintosh Quadra 630 [R. Belmont] New working clones added ------------------------ Apple Computer Macintosh LC/Performa 580 [R. Belmont] --- src/mame/apple/f108.cpp | 211 ++++++++++++++++++ src/mame/apple/f108.h | 72 ++++++ src/mame/apple/iosb.cpp | 78 +++++-- src/mame/apple/iosb.h | 61 ++++-- src/mame/apple/macquadra630.cpp | 209 ++++++++++++++++++ src/mame/apple/valkyrie.cpp | 473 ++++++++++++++++++++++++++++++++++++++++ src/mame/apple/valkyrie.h | 67 ++++++ src/mame/mame.lst | 4 + 8 files changed, 1144 insertions(+), 31 deletions(-) create mode 100644 src/mame/apple/f108.cpp create mode 100644 src/mame/apple/f108.h create mode 100755 src/mame/apple/macquadra630.cpp create mode 100644 src/mame/apple/valkyrie.cpp create mode 100644 src/mame/apple/valkyrie.h mode change 100644 => 100755 src/mame/mame.lst diff --git a/src/mame/apple/f108.cpp b/src/mame/apple/f108.cpp new file mode 100644 index 00000000000..e89fb4ee31f --- /dev/null +++ b/src/mame/apple/f108.cpp @@ -0,0 +1,211 @@ +// license:BSD-3-Clause +// copyright-holders:R. Belmont +/* + Apple "F108" memory controller + Emulation by R. Belmont + + F108 contains: + - A memory controller + - The usual Mac ROM/RAM switch so at boot the processor has vectors at 0 + - An ATA bus interface + - An SCC interface + - A SCSI controller which is claimed to be "just like a 53C96". A real 53C96 seems to work fine. +*/ + +#include "emu.h" + +#include "f108.h" + +#include "softlist_dev.h" + +#include "bus/nscsi/devices.h" +#include "bus/rs232/rs232.h" + +//************************************************************************** +// DEVICE DEFINITIONS +//************************************************************************** + +DEFINE_DEVICE_TYPE(F108, f108_device, "macf108", "Apple F108 memory controller") + +//------------------------------------------------- +// ADDRESS_MAP +//------------------------------------------------- + +void f108_device::map(address_map &map) +{ + map(0x40000000, 0x400fffff).r(FUNC(f108_device::rom_switch_r)).mirror(0x0ff00000).nopw(); + map(0x5000c000, 0x5000dfff).rw(FUNC(f108_device::scc_r), FUNC(f108_device::scc_w)).mirror(0x00fc0000); + map(0x5001a000, 0x5001a01f).rw(m_ata, FUNC(ata_interface_device::cs0_swap_r), FUNC(ata_interface_device::cs0_swap_w)).umask32(0xffff0000).mirror(0x00fc0000); + map(0x5001a000, 0x5001a003).rw(FUNC(f108_device::ata_data_r), FUNC(f108_device::ata_data_w)).mirror(0x00fc0000); + map(0x5001a020, 0x5001a03f).rw(m_ata, FUNC(ata_interface_device::cs1_swap_r), FUNC(ata_interface_device::cs1_swap_w)).umask32(0xffff0000).mirror(0x00fc0000); + // a040 is probably a configuration register based on later Apple ATA implementations, but the values written don't make sense + map(0xf9000000, 0xf90fffff).ram(); // VRAM +} + +//------------------------------------------------- +// device_add_mconfig - add device configuration +//------------------------------------------------- + +void f108_device::device_add_mconfig(machine_config &config) +{ + ATA_INTERFACE(config, m_ata).options(ata_devices, "hdd", nullptr, false); + m_ata->irq_handler().set(FUNC(f108_device::ata_irq_w)); + + NSCSI_BUS(config, m_scsibus); + NSCSI_CONNECTOR(config, "scsi:0", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:1", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:2", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:3", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:4", mac_scsi_devices, "cdrom"); + NSCSI_CONNECTOR(config, "scsi:5", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:6", mac_scsi_devices, nullptr); + NSCSI_CONNECTOR(config, "scsi:7").option_set("ncr53c96", NCR53C96).clock(40_MHz_XTAL).machine_config( + [this] (device_t *device) + { + ncr53c96_device &adapter = downcast(*device); + + adapter.set_busmd(ncr53c96_device::BUSMD_1); + adapter.irq_handler_cb().set(m_primetimeii, FUNC(primetime_device::scsi_irq_w)); + adapter.drq_handler_cb().set(m_primetimeii, FUNC(primetime_device::scsi_drq_w)); + }); + + SOFTWARE_LIST(config, "hdd_list").set_original("mac_hdd"); + + SCC85C30(config, m_scc, 31.3344_MHz_XTAL/4); + m_scc->configure_channels(3'686'400, 3'686'400, 3'686'400, 3'686'400); + m_scc->out_int_callback().set(FUNC(f108_device::scc_irq_w)); + m_scc->out_txda_callback().set("printer", FUNC(rs232_port_device::write_txd)); + m_scc->out_txdb_callback().set("modem", FUNC(rs232_port_device::write_txd)); + + rs232_port_device &rs232a(RS232_PORT(config, "printer", default_rs232_devices, nullptr)); + rs232a.rxd_handler().set(m_scc, FUNC(z80scc_device::rxa_w)); + rs232a.dcd_handler().set(m_scc, FUNC(z80scc_device::dcda_w)); + rs232a.cts_handler().set(m_scc, FUNC(z80scc_device::ctsa_w)); + + rs232_port_device &rs232b(RS232_PORT(config, "modem", default_rs232_devices, nullptr)); + rs232b.rxd_handler().set(m_scc, FUNC(z80scc_device::rxb_w)); + rs232b.dcd_handler().set(m_scc, FUNC(z80scc_device::dcdb_w)); + rs232b.cts_handler().set(m_scc, FUNC(z80scc_device::ctsb_w)); +} + +//------------------------------------------------- +// f108_device - constructor +//------------------------------------------------- + +f108_device::f108_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : + device_t(mconfig, F108, tag, owner, clock), + m_maincpu(*this, finder_base::DUMMY_TAG), + m_primetimeii(*this, finder_base::DUMMY_TAG), + m_ata(*this, "ata"), + m_scsibus(*this, "scsi"), + m_ncr1(*this, "scsi:7:ncr53c96"), + m_scc(*this, "scc"), + m_rom(*this, finder_base::DUMMY_TAG), + m_ata_irq(*this), + m_overlay(false) +{ +} + +//------------------------------------------------- +// device_start - device-specific startup +//------------------------------------------------- + +void f108_device::device_start() +{ + m_rom_ptr = &m_rom[0]; + m_rom_size = m_rom.length() << 2; +} + +//------------------------------------------------- +// device_reset - device-specific reset +//------------------------------------------------- + +void f108_device::device_reset() +{ + m_overlay = true; + + // put ROM mirror at 0 + address_space &space = m_maincpu->space(AS_PROGRAM); + const u32 memory_size = std::min((u32)0x3fffff, m_rom_size); + const u32 memory_end = memory_size - 1; + offs_t memory_mirror = memory_end & ~(memory_size - 1); + + space.unmap_write(0x00000000, memory_end); + space.install_rom(0x00000000, memory_end & ~memory_mirror, memory_mirror, m_rom_ptr); +} + +u32 f108_device::rom_switch_r(offs_t offset) +{ + // disable the overlay + if (m_overlay && !machine().side_effects_disabled()) + { + address_space &space = m_maincpu->space(AS_PROGRAM); + const u32 memory_end = m_ram_size - 1; + void *memory_data = m_ram_ptr; + offs_t memory_mirror = memory_end & ~memory_end; + + space.install_ram(0x00000000, memory_end & ~memory_mirror, memory_mirror, memory_data); + m_overlay = false; + } + + return m_rom_ptr[offset & ((m_rom_size - 1) >> 2)]; +} + +void f108_device::set_ram_info(u32 *ram, u32 size) +{ + m_ram_ptr = ram; + m_ram_size = size; +} + +void f108_device::ata_irq_w(int state) +{ + m_ata_irq(state); +} + +u32 f108_device::ata_data_r(offs_t offset, u32 mem_mask) +{ + u32 retval = 0; + + if (mem_mask == 0xffffffff) + { + retval = m_ata->cs0_swap_r(0, 0xffff) << 16; + retval |= m_ata->cs0_swap_r(0, 0xffff); + } + else if ((mem_mask & 0xffff0000) != 0) + { + retval = m_ata->cs0_swap_r(0, mem_mask >> 16) << 16; + } + + return retval; +} + +void f108_device::ata_data_w(offs_t offset, u32 data, u32 mem_mask) +{ + if (mem_mask == 0xffffffff) + { + m_ata->cs0_swap_w(0, data >> 16, 0xffff); + m_ata->cs0_swap_w(0, data & 0xffff, 0xffff); + } + else if ((mem_mask & 0xffff0000) != 0) + { + m_ata->cs0_swap_w(0, data >> 16, mem_mask >> 16); + } +} + +u16 f108_device::scc_r(offs_t offset) +{ + m_primetimeii->via_sync(); + u16 result = m_scc->dc_ab_r(offset); + return (result << 8) | result; +} + +void f108_device::scc_w(offs_t offset, u16 data) +{ + m_primetimeii->via_sync(); + m_scc->dc_ab_w(offset, data >> 8); +} + +void f108_device::scc_irq_w(int state) +{ + m_primetimeii->scc_irq_w(state); +} diff --git a/src/mame/apple/f108.h b/src/mame/apple/f108.h new file mode 100644 index 00000000000..0ca62a8d44d --- /dev/null +++ b/src/mame/apple/f108.h @@ -0,0 +1,72 @@ +// license:BSD-3-Clause +// copyright-holders:R. Belmont + +#ifndef MAME_APPLE_F108_H +#define MAME_APPLE_F108_H + +#pragma once + +#include "iosb.h" + +#include "bus/ata/ataintf.h" +#include "machine/ncr53c90.h" +#include "machine/nscsi_bus.h" +#include "machine/z80scc.h" + +// ======================> f108_device + +/// \brief Device class for Apple F108 system controller ASIC. +/// +/// F108 includes a memory controller, NCR 53C96 compatible SCSI, +/// and a single-drive ATA-1 interface. +class f108_device : public device_t +{ +public: + // construction/destruction + f108_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock); + + // interface routines + virtual void map(address_map &map); + + template void set_maincpu_tag(T &&... args) { m_maincpu.set_tag(std::forward(args)...); } + template void set_primetimeii_tag(T &&... args) { m_primetimeii.set_tag(std::forward(args)...); } + template void set_rom_tag(T &&... args) { m_rom.set_tag(std::forward(args)...); } + void set_ram_info(u32 *ram, u32 size); + + auto write_ata_irq() { return m_ata_irq.bind(); } + +protected: + // device-level overrides + virtual void device_start() override; + virtual void device_reset() override; + virtual void device_add_mconfig(machine_config &config) override; + + u32 rom_switch_r(offs_t offset); + +private: + void ata_irq_w(int state); + u32 ata_data_r(offs_t offset, u32 mem_mask); + void ata_data_w(offs_t offset, u32 data, u32 mem_mask); + u16 scc_r(offs_t offset); + void scc_w(offs_t offset, u16 data); + void scc_irq_w(int state); + + required_device m_maincpu; + required_device m_primetimeii; + required_device m_ata; + required_device m_scsibus; + required_device m_ncr1; + required_device m_scc; + required_region_ptr m_rom; + + devcb_write_line m_ata_irq; + + bool m_overlay; + u32 *m_ram_ptr, *m_rom_ptr; + u32 m_ram_size, m_rom_size; +}; + +// device type definition +DECLARE_DEVICE_TYPE(F108, f108_device) + +#endif // MAME_APPLE_F108_H diff --git a/src/mame/apple/iosb.cpp b/src/mame/apple/iosb.cpp index bbee1cff200..6747f5fe149 100644 --- a/src/mame/apple/iosb.cpp +++ b/src/mame/apple/iosb.cpp @@ -15,12 +15,12 @@ - The "Turbo SCSI" logic from the standalone versions of DAFB and DAFB II - Support logic for various external subsystems (ADB, SCC, SONIC Ethernet) - IOSB and PrimeTime are similar to Sonora, but replace the RBV/V8/VASP/Sonora pseudo-VIA with a - real VIA core that has the timers disabled and some IER and PCR bits hard-wired to specific values. - The pseudo-VIA returns in AMIC on the PDM-class PowerMacs and goes away forever in the TNT-class. + IOSB and PrimeTime are similar to Sonora, but replace the RBV/V8/VASP/Sonora pseudo-VIA with a + real VIA core that has the timers disabled and some IER and PCR bits hard-wired to specific values. + The pseudo-VIA returns in AMIC on the PDM-class PowerMacs and goes away forever in the TNT-class. - PrimeTime swaps the GI/Microchip ADB modem interface for the standard Cuda hookup found in Sonora, - and doesn't bring out the 4 VIA ID pins. PrimeTime II is similar. + PrimeTime swaps the GI/Microchip ADB modem interface for the standard Cuda hookup found in Sonora, + and doesn't bring out the 4 VIA ID pins. PrimeTime II is similar. */ #include "emu.h" @@ -44,6 +44,7 @@ static constexpr u32 C15M = (C7M * 2); DEFINE_DEVICE_TYPE(IOSB, iosb_device, "iosb", "Apple IOSB I/O ASIC") DEFINE_DEVICE_TYPE(PRIMETIME, primetime_device, "primetime", "Apple PrimeTime I/O ASIC") +DEFINE_DEVICE_TYPE(PRIMETIMEII, primetimeii_device, "primetime2", "Apple PrimeTime II I/O ASIC") //------------------------------------------------- // ADDRESS_MAP @@ -125,6 +126,11 @@ iosb_base::iosb_base(const machine_config &mconfig, device_type type, const char m_asc(*this, "asc"), m_fdc(*this, "fdc"), m_floppy(*this, "fdc:%d", 0U), + m_nubus_irqs(0xff), + m_via_interrupt(0), + m_via2_interrupt(0), + m_scc_interrupt(0), + m_last_taken_interrupt(-1), m_cur_floppy(nullptr), m_hdsel(0), m_adb_interrupt(0), @@ -147,14 +153,26 @@ iosb_device::iosb_device(const machine_config &mconfig, const char *tag, device_ { } -primetime_device::primetime_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : - iosb_base(mconfig, PRIMETIME, tag, owner, clock), +primetime_device::primetime_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, uint32_t clock) : + iosb_base(mconfig, type, tag, owner, clock), write_pb4(*this), write_pb5(*this), read_pb3(*this, 0) { } +primetime_device::primetime_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : + primetime_device(mconfig, PRIMETIME, tag, owner, clock) +{ +} + +primetimeii_device::primetimeii_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : + primetime_device(mconfig, PRIMETIMEII, tag, owner, clock), + m_ata_irq(0) +{ + std::fill(std::begin(m_primetimeii_regs), std::end(m_primetimeii_regs), 0); +} + //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- @@ -185,11 +203,6 @@ void iosb_base::device_reset() // start 60.15 Hz timer m_6015_timer->adjust(attotime::from_hz(60.15), 0, attotime::from_hz(60.15)); - m_via_interrupt = m_via2_interrupt = m_scc_interrupt = 0; - m_last_taken_interrupt = -1; - m_hdsel = 0; - m_nubus_irqs = 0xff; - m_via2_ca1_hack = 1; m_via2->write_ca1(1); m_via2->write_cb1(1); @@ -293,7 +306,7 @@ void iosb_base::field_interrupts() void iosb_base::scc_irq_w(int state) { - m_scc_interrupt = (state == ASSERT_LINE); + m_scc_interrupt = (state == ASSERT_LINE) ? 1 : 0; field_interrupts(); } @@ -545,6 +558,8 @@ u32 iosb_base::turboscsi_dma_r(offs_t offset, u32 mem_mask) return 0xffff; } + LOGMASKED(LOG_TURBOSCSI, "dma_r mask %08x (%s)\n", mem_mask, machine().describe_context()); + if (mem_mask == 0xffffffff) { if (!m_scsi_second_half) @@ -627,7 +642,6 @@ void iosb_base::scsi_drq_w(int state) u16 iosb_base::iosb_regs_r(offs_t offset) { - LOGMASKED(LOG_IOSBREGS, "iosb_regs_r: @ %x\n", offset>>7); return m_iosb_regs[offset>>7]; } @@ -671,3 +685,39 @@ void primetime_device::via_out_b(uint8_t data) write_pb4(BIT(data, 4)); write_pb5(BIT(data, 5)); } + +// ------------------------------------------- PrimeTime II device +void primetimeii_device::device_start() +{ + primetime_device::device_start(); + save_item(NAME(m_ata_irq)); + save_item(NAME(m_primetimeii_regs)); +} + +void primetimeii_device::map(address_map &map) +{ + iosb_base::map(map); + map(0x0001a100, 0x0001a10f).r(FUNC(primetimeii_device::ata_regs_r)).mirror(0x00f00000); +} + +// This may actually be in F108, the boundaries between the two chips aren't completely clear +u16 primetimeii_device::ata_regs_r(offs_t offset) +{ + switch (offset) + { + case 0: // special interrupt status: bit 6 = VBL IRQ, bit 5 = ATA IRQ + return ((m_nubus_irqs ^ 0xff) & 0x40) | (m_ata_irq << 5); + + default: + LOGMASKED(LOG_IOSBREGS, "%s: Unhandled ata_regs_r @ %x\n", tag(), offset); + break; + } + + return 0; +} + +void primetimeii_device::ata_irq_w(int state) +{ + via2_irq_w<0x10>(state); + m_ata_irq = state; +} diff --git a/src/mame/apple/iosb.h b/src/mame/apple/iosb.h index 1483bc0910b..7742729d8da 100644 --- a/src/mame/apple/iosb.h +++ b/src/mame/apple/iosb.h @@ -22,7 +22,7 @@ class iosb_base : public device_t { public: // construction/destruction - iosb_base(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, uint32_t clock); + iosb_base(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, u32 clock); // interface routines auto write_adb_st() { return m_adb_st.bind(); } // ADB state @@ -34,7 +34,7 @@ public: auto read_pa4() { return m_pa4.bind(); } auto read_pa6() { return m_pa6.bind(); } - void map(address_map &map); + virtual void map(address_map &map); template void set_maincpu_tag(T &&... args) { m_maincpu.set_tag(std::forward(args)...); } template void set_scsi_tag(T &&... args) { m_ncr.set_tag(std::forward(args)...); } @@ -65,6 +65,9 @@ protected: virtual uint8_t via_in_b(); virtual void via_out_b(uint8_t data); + virtual u16 iosb_regs_r(offs_t offset); + virtual void iosb_regs_w(offs_t offset, u16 data, u16 mem_mask); + devcb_write8 m_adb_st; devcb_write_line m_cb1, m_cb2; devcb_read_line m_pa1, m_pa2, m_pa4, m_pa6; @@ -76,6 +79,10 @@ protected: required_device m_fdc; required_device_array m_floppy; + u16 m_iosb_regs[0x20]; + + u8 m_nubus_irqs; + private: emu_timer *m_6015_timer; int m_via_interrupt, m_via2_interrupt, m_scc_interrupt, m_last_taken_interrupt; @@ -83,22 +90,16 @@ private: int m_hdsel; int m_adb_interrupt; int m_via2_ca1_hack; - u8 m_nubus_irqs; - int m_drq, m_scsi_irq, m_asc_irq; - int m_scsi_read_cycles, m_scsi_write_cycles, m_scsi_dma_read_cycles, m_scsi_dma_write_cycles; + s32 m_drq, m_scsi_irq, m_asc_irq; + u32 m_scsi_read_cycles, m_scsi_write_cycles, m_scsi_dma_read_cycles, m_scsi_dma_write_cycles; u32 m_scsi_dma_result; bool m_scsi_second_half; - u16 m_iosb_regs[0x20]; - - u16 iosb_regs_r(offs_t offset); - void iosb_regs_w(offs_t offset, u16 data, u16 mem_mask); - - uint16_t mac_via_r(offs_t offset); - void mac_via_w(offs_t offset, uint16_t data, uint16_t mem_mask); - uint16_t mac_via2_r(offs_t offset); - void mac_via2_w(offs_t offset, uint16_t data, uint16_t mem_mask); + u16 mac_via_r(offs_t offset); + void mac_via_w(offs_t offset, u16 data, u16 mem_mask); + u16 mac_via2_r(offs_t offset); + void mac_via2_w(offs_t offset, u16 data, u16 mem_mask); uint8_t via_in_a(); uint8_t via2_in_a(); @@ -113,15 +114,19 @@ private: void phases_w(uint8_t phases); void devsel_w(uint8_t devsel); - uint16_t swim_r(offs_t offset, u16 mem_mask); + u16 swim_r(offs_t offset, u16 mem_mask); void swim_w(offs_t offset, u16 data, u16 mem_mask); }; +/// \brief Device class for Apple IOSB I/O controller ASIC. +/// +/// IOSB includes 2 VIAs, TurboSCSI logic, a SWIM 2 floppy controller, +/// and more. class iosb_device : public iosb_base { public: // construction/destruction - iosb_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock); + iosb_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock); protected: virtual void device_add_mconfig(machine_config &config) override; @@ -138,7 +143,8 @@ class primetime_device : public iosb_base { public: // construction/destruction - primetime_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock); + primetime_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, u32 clock); + primetime_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock); auto pb4_callback() { return write_pb4.bind(); } auto pb5_callback() { return write_pb5.bind(); } @@ -154,8 +160,29 @@ protected: private: }; +class primetimeii_device : public primetime_device +{ +public: + // construction/destruction + primetimeii_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock); + + virtual void map(address_map &map) override; + + void ata_irq_w(int state); + +protected: + virtual void device_start() override; + +private: + u16 ata_regs_r(offs_t offset); + + s32 m_ata_irq; + u16 m_primetimeii_regs[0x20]; +}; + // device type definition DECLARE_DEVICE_TYPE(IOSB, iosb_device) DECLARE_DEVICE_TYPE(PRIMETIME, primetime_device) +DECLARE_DEVICE_TYPE(PRIMETIMEII, primetimeii_device) #endif // MAME_APPLE_IOSB_H diff --git a/src/mame/apple/macquadra630.cpp b/src/mame/apple/macquadra630.cpp new file mode 100755 index 00000000000..e6daf656d8d --- /dev/null +++ b/src/mame/apple/macquadra630.cpp @@ -0,0 +1,209 @@ +// license:BSD-3-Clause +// copyright-holders:R. Belmont +/**************************************************************************** + + drivers/macquadra630.cpp + Mac Quadra 630 ("Show and Tell") + Mac LC 580 ("Dragonkid") + + By R. Belmont + + These machines took the cost-reduced but still decent Quadra 605/LC 575 + and made them even cheaper by replacing the full-featured DAFB video chip + with "Valkyrie". Which appears to offer only a few pre-programmed video + mode timings. + + Further cost reduction occured by replacing the hard disk with an ATA/IDE + model instead of the by-then traditional Mac SCSI drive. The ATA interface + was wedged into the chipset in a somewhat odd manner that impacts VIA2 + interrupt handling (including video VBL IRQs). + + Known problems: + - If you don't boot an OS, the mouse pointer will stop updating when the + question-mark disk appears. If you do boot an OS, everything's fine. + + - The later version boot ROM for the LC 580 can't boot a SCSI CD-ROM. It + reads 512 bytes of a 2048 byte sector and expects CyclePhase_96 to read + and discard the rest of the sector from the drive. But it sees a (pseudo) + DMA command was active and waits for DRQ, which doesn't happen because + the 53C96's transfer count is zero. The earlier ROM has the same logic as + previous (and later!) 53C96 machines and works fine. + +****************************************************************************/ + +#include "emu.h" +#include "softlist_dev.h" + +#include "cuda.h" +#include "f108.h" +#include "iosb.h" +#include "macadb.h" +#include "mactoolbox.h" +#include "valkyrie.h" + +#include "cpu/m68000/m68040.h" +#include "machine/ram.h" +#include "machine/timer.h" + +#define C32M 31.3344_MHz_XTAL +#define C15M (C32M/2) +#define C7M (C32M/4) + +namespace { + +class quadra630_state : public driver_device +{ +public: + quadra630_state(const machine_config &mconfig, device_type type, const char *tag) : + driver_device(mconfig, type, tag), + m_maincpu(*this, "maincpu"), + m_f108(*this, "f108"), + m_primetimeii(*this, "primetimeii"), + m_video(*this, "valkyrie"), + m_macadb(*this, "macadb"), + m_cuda(*this, "cuda"), + m_ram(*this, RAM_TAG) + { + } + + void macqd630(machine_config &config); + void maclc580(machine_config &config); + + void quadra630_map(address_map &map); + void lc580_map(address_map &map); + + void init_macqd630(); + +private: + required_device m_maincpu; + required_device m_f108; + required_device m_primetimeii; + required_device m_video; + required_device m_macadb; + required_device m_cuda; + required_device m_ram; + + virtual void machine_start() override; + virtual void machine_reset() override; + + void cuda_reset_w(int state) + { + m_maincpu->set_input_line(INPUT_LINE_HALT, state); + m_maincpu->set_input_line(INPUT_LINE_RESET, state); + } +}; + +void quadra630_state::machine_start() +{ + m_f108->set_ram_info((u32 *) m_ram->pointer(), m_ram->size()); +} + +void quadra630_state::machine_reset() +{ + m_maincpu->set_input_line(INPUT_LINE_HALT, ASSERT_LINE); +} + +void quadra630_state::init_macqd630() +{ +} + +/*************************************************************************** + ADDRESS MAPS +***************************************************************************/ +void quadra630_state::quadra630_map(address_map &map) +{ + map(0x00000000, 0xffffffff).m(m_f108, FUNC(f108_device::map)); + map(0x00000000, 0xffffffff).m(m_video, FUNC(valkyrie_device::map)); + map(0x50000000, 0x5fffffff).m(m_primetimeii, FUNC(primetime_device::map)); + + // 5000a000 = SONIC if comm slot card is installed + map(0x5000a000, 0x5000bfff).noprw().mirror(0x00fc0000); + + // 2252 = Q630, 225a = LC580 + map(0x5ffffffc, 0x5fffffff).lr32(NAME([](offs_t offset) { return 0xa55a2252; })); +} + +void quadra630_state::lc580_map(address_map &map) +{ + quadra630_map(map); + + map(0x5ffffffc, 0x5fffffff).lr32(NAME([](offs_t offset) { return 0xa55a225a; })); +} + +/*************************************************************************** + DEVICE CONFIG +***************************************************************************/ + +static INPUT_PORTS_START( macadb ) +INPUT_PORTS_END + +/*************************************************************************** + MACHINE DRIVERS +***************************************************************************/ + +void quadra630_state::macqd630(machine_config &config) +{ + M68040(config, m_maincpu, 33_MHz_XTAL); + m_maincpu->set_addrmap(AS_PROGRAM, &quadra630_state::quadra630_map); + m_maincpu->set_dasm_override(std::function(&mac68k_dasm_override), "mac68k_dasm_override"); + + F108(config, m_f108, 33_MHz_XTAL); + m_f108->set_maincpu_tag("maincpu"); + m_f108->set_primetimeii_tag("primetimeii"); + m_f108->set_rom_tag("bootrom"); + m_f108->write_ata_irq().set(m_primetimeii, FUNC(primetimeii_device::ata_irq_w)); + + PRIMETIMEII(config, m_primetimeii, 33_MHz_XTAL); + m_primetimeii->set_maincpu_tag("maincpu"); + m_primetimeii->set_scsi_tag("f108:scsi:7:ncr53c96"); + + VALKYRIE(config, m_video, C32M); + m_video->write_irq().set(m_primetimeii, FUNC(primetime_device::via2_irq_w<0x40>)); + + MACADB(config, m_macadb, C15M); + + // TODO: recapamac.com.au's logic board photos show Cuda 2.40 for both Q630 and LC580, + // but both ROM versions have issues syncing with 2.38 and 2.40 while 2.37 works. + CUDA_V237(config, m_cuda, XTAL(32'768)); + m_cuda->reset_callback().set(FUNC(quadra630_state::cuda_reset_w)); + m_cuda->linechange_callback().set(m_macadb, FUNC(macadb_device::adb_linechange_w)); + m_cuda->via_clock_callback().set(m_primetimeii, FUNC(primetime_device::cb1_w)); + m_cuda->via_data_callback().set(m_primetimeii, FUNC(primetime_device::cb2_w)); + m_macadb->adb_data_callback().set(m_cuda, FUNC(cuda_device::set_adb_line)); + config.set_perfect_quantum(m_maincpu); + + m_primetimeii->pb3_callback().set(m_cuda, FUNC(cuda_device::get_treq)); + m_primetimeii->pb4_callback().set(m_cuda, FUNC(cuda_device::set_byteack)); + m_primetimeii->pb5_callback().set(m_cuda, FUNC(cuda_device::set_tip)); + m_primetimeii->write_cb2().set(m_cuda, FUNC(cuda_device::set_via_data)); + + /* internal ram */ + RAM(config, m_ram); + m_ram->set_default_size("4M"); + m_ram->set_extra_options("8M,16M,32M"); +} + +void quadra630_state::maclc580(machine_config &config) +{ + macqd630(config); + + m_maincpu->set_addrmap(AS_PROGRAM, &quadra630_state::lc580_map); +} + +ROM_START( macqd630 ) + ROM_REGION32_BE(0x100000, "bootrom", 0) + ROM_LOAD( "06684214.bin", 0x000000, 0x100000, CRC(1735e7a5) SHA1(47cd505b6a7c46e5c0ffa29f0d5037c83e94a02f) ) +ROM_END + +ROM_START( maclc580 ) + ROM_REGION32_BE(0x100000, "bootrom", 0) + ROM_SYSTEM_BIOS(0, "older", "Version 32F1") + ROMX_LOAD("06684214.bin", 0x000000, 0x100000, CRC(1735e7a5) SHA1(47cd505b6a7c46e5c0ffa29f0d5037c83e94a02f), ROM_BIOS(0)) + ROM_SYSTEM_BIOS(1, "later", "Version 32F2 (bug: can't boot CD-ROM)") + ROMX_LOAD( "064dc91d.bin", 0x000000, 0x100000, CRC(59e6960f) SHA1(f48a8adf06bce50beee033d0d814da0e5e916d08), ROM_BIOS(1)) +ROM_END + +} // anonymous namespace + +COMP( 1994, macqd630, 0, 0, macqd630, macadb, quadra630_state, init_macqd630, "Apple Computer", "Macintosh Quadra 630", MACHINE_SUPPORTS_SAVE) +COMP( 1995, maclc580, macqd630, 0, maclc580, macadb, quadra630_state, init_macqd630, "Apple Computer", "Macintosh LC/Performa 580", MACHINE_SUPPORTS_SAVE) diff --git a/src/mame/apple/valkyrie.cpp b/src/mame/apple/valkyrie.cpp new file mode 100644 index 00000000000..2c3dd4ededb --- /dev/null +++ b/src/mame/apple/valkyrie.cpp @@ -0,0 +1,473 @@ +// license:BSD-3-Clause +// copyright-holders:R. Belmont +/* + Apple "Valkyrie" - very low-cost video framebuffer + Emulation by R. Belmont + + This was the bonus awfulness in the infamous Quadra 630/LC 580 machines. Only + a few monitor types are supported and the video mode timings appear to be hardcoded + into the chip. +*/ + +#include "emu.h" +#include "valkyrie.h" + +#define LOG_MODE (1U << 1) +#define LOG_MONSENSE (1U << 2) +#define LOG_RAMDAC (1U << 3) + +#define VERBOSE (0) +#include "logmacro.h" + +DEFINE_DEVICE_TYPE(VALKYRIE, valkyrie_device, "valkyrie", "Apple Valkyrie video") + +//------------------------------------------------- +// ADDRESS_MAP +//------------------------------------------------- + +void valkyrie_device::map(address_map &map) +{ + map(0x50f2a000, 0x50f2bfff).rw(FUNC(valkyrie_device::regs_r), FUNC(valkyrie_device::regs_w)); + map(0x50f24000, 0x50f25fff).rw(FUNC(valkyrie_device::ramdac_r), FUNC(valkyrie_device::ramdac_w)); + + map(0xf9000000, 0xf90fffff).rw(FUNC(valkyrie_device::vram_r), FUNC(valkyrie_device::vram_w)); +} + +valkyrie_device::valkyrie_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock) : + device_t(mconfig, VALKYRIE, tag, owner, clock), + m_vram_size(0x100000), + m_pixel_clock(31334400), + m_pal_address(0), m_pal_idx(0), m_mode(0), + m_screen(*this, "screen"), + m_palette(*this, "palette"), + m_monitor_config(*this, "monitor"), + m_irq(*this), + m_vram_offset(0), m_monitor_id(0), + m_base(0), m_stride(1024), m_int_status(0), m_hres(0), m_vres(0), m_htotal(0), m_vtotal(0), + m_config(0) +{ +} + +void valkyrie_device::device_start() +{ + m_vram = std::make_unique(m_vram_size); + + m_vbl_timer = timer_alloc(FUNC(valkyrie_device::vbl_tick), this); + m_vbl_timer->adjust(attotime::never); + + save_item(NAME(m_vram_offset)); + save_item(NAME(m_mode)); + save_item(NAME(m_monitor_id)); + save_item(NAME(m_base)); + save_item(NAME(m_stride)); + save_item(NAME(m_pal_address)); + save_item(NAME(m_pal_idx)); + save_item(NAME(m_hres)); + save_item(NAME(m_vres)); + save_item(NAME(m_htotal)); + save_item(NAME(m_vtotal)); + save_item(NAME(m_pixel_clock)); + save_item(NAME(m_config)); + save_item(NAME(m_int_status)); + save_pointer(NAME(m_vram), m_vram_size); + + machine().save().register_postload(save_prepost_delegate(FUNC(valkyrie_device::recalc_mode), this)); +} + +void valkyrie_device::device_reset() +{ + // zero out the palette on start, I'm not sure where the video enable is, or if there is one +/* for (int i = 0; i < 256; i++) + { + m_palette->set_pen_red_level(i, 0); + m_palette->set_pen_green_level(i, 0); + m_palette->set_pen_blue_level(i, 0); + }*/ + m_enable = false; +} + +void valkyrie_device::device_add_mconfig(machine_config &config) +{ + SCREEN(config, m_screen, SCREEN_TYPE_RASTER); + // dot clock, htotal, hstart, hend, vtotal, vstart, vend + m_screen->set_raw(31334400, 896, 0, 640, 525, 0, 480); + m_screen->set_screen_update(FUNC(valkyrie_device::screen_update)); + + PALETTE(config, m_palette).set_entries(256); +} + +static constexpr u8 ext(u8 bc, u8 ac, u8 ab) +{ + return 0x40 | (bc << 4) | (ac << 2) | ab; +} + +static INPUT_PORTS_START(monitor_config) + PORT_START("monitor") + PORT_CONFNAME(0x7f, 6, "Monitor type") + PORT_CONFSETTING(0x02, u8"Mac RGB Display (12\" 512\u00d7384)") // "Rubik" (modified IIgs AppleColor RGB) + PORT_CONFSETTING(0x06, u8"Mac Hi-Res Display (12-14\" 640\u00d7480)") // "High Res" + PORT_CONFSETTING(ext(1, 1, 3), "640x480 VGA") + PORT_CONFSETTING(ext(2, 3, 1), "832x624 16\" RGB") // "Goldfish" or "16 inch RGB" +INPUT_PORTS_END + +ioport_constructor valkyrie_device::device_input_ports() const +{ + return INPUT_PORTS_NAME(monitor_config); +} + +u32 valkyrie_device::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect) +{ + auto const vram8 = util::big_endian_cast(&m_vram[0]) + 0x1000; + const pen_t *pens = m_palette->pens(); + + if (!m_enable) + { + return 0; + } + + const u32 stride = (m_stride << m_mode); + switch (m_mode) + { + case 0: // 1bpp + { + for (int y = 0; y < m_vres; y++) + { + u32 *scanline = &bitmap.pix(y); + for (int x = 0; x < m_hres/8; x++) + { + u8 const pixels = vram8[(y * stride) + x]; + + *scanline++ = pens[(pixels>>7)&1]; + *scanline++ = pens[(pixels>>6)&1]; + *scanline++ = pens[(pixels>>5)&1]; + *scanline++ = pens[(pixels>>4)&1]; + *scanline++ = pens[(pixels>>3)&1]; + *scanline++ = pens[(pixels>>2)&1]; + *scanline++ = pens[(pixels>>1)&1]; + *scanline++ = pens[(pixels&1)]; + } + } + } + break; + + case 1: // 2bpp + { + for (int y = 0; y < m_vres; y++) + { + u32 *scanline = &bitmap.pix(y); + for (int x = 0; x < m_hres/4; x++) + { + u8 const pixels = vram8[(y * stride) + x]; + + *scanline++ = pens[((pixels>>6)&3)]; + *scanline++ = pens[((pixels>>4)&3)]; + *scanline++ = pens[((pixels>>2)&3)]; + *scanline++ = pens[(pixels&3)]; + } + } + } + break; + + case 2: // 4bpp + { + for (int y = 0; y < m_vres; y++) + { + u32 *scanline = &bitmap.pix(y); + for (int x = 0; x < m_hres/2; x++) + { + u8 const pixels = vram8[(y * stride) + x]; + + *scanline++ = pens[(pixels>>4)]; + *scanline++ = pens[(pixels&0xf)]; + } + } + } + break; + + case 3: // 8bpp + { + for (int y = 0; y < m_vres; y++) + { + u32 *scanline = &bitmap.pix(y); + for (int x = 0; x < m_hres; x++) + { + u8 const pixels = vram8[(y * stride) + x]; + *scanline++ = pens[pixels]; + } + } + } + break; + + case 4: // 16bpp x555 + for (int y = 0; y < m_vres; y++) + { + u32 *scanline = &bitmap.pix(y); + for (int x = 0; x < m_hres; x++) + { + u16 const pixels = (vram8[(y * stride) + (x<<1)] << 8) | vram8[(y * stride) + (x<<1) + 1]; + *scanline++ = rgb_t(((pixels >> 10) & 0x1f) << 3, ((pixels >> 5) & 0x1f) << 3, (pixels & 0x1f) << 3); + } + } + break; + } + + return 0; +} + +u32 valkyrie_device::regs_r(offs_t offset) +{ +// printf("Read regs @ %x\n", offset<<2); + switch (offset<<2) + { + case 0: + return m_video_timing; + + case 4: + return m_mode; + + case 0x10: // config + return m_config; + + case 0x14: + return (m_screen->vblank() << 24); + + case 0x1c: // monitor sense in upper nibble, write monitor sense in lower nibble + { + u8 mon = m_monitor_config->read(); + u8 res; + LOGMASKED(LOG_MONSENSE, "mon = %02x, m_monitor_id = %02x\n", mon, m_monitor_id); + if (mon & 0x40) + { + res = 7; + if (m_monitor_id == 0x4) + { + res &= 4 | (BIT(mon, 5) << 1) | BIT(mon, 4); + } + if (m_monitor_id == 0x2) + { + res &= (BIT(mon, 3) << 2) | 2 | BIT(mon, 2); + } + if (m_monitor_id == 0x1) + { + res &= (BIT(mon, 1) << 2) | (BIT(mon, 0) << 1) | 1; + } + } + else + { + res = mon; + } + + LOGMASKED(LOG_MONSENSE, "sense result = %x\n", res); + return res<<28; + } + break; + } + + return 0; +} + +void valkyrie_device::regs_w(offs_t offset, u32 data) +{ + data &= 0xfff; + switch (offset << 2) + { + case 0: // video timing select (apparently from hardcoded values!) + m_video_timing = data; + break; + + case 4: // video depth: 0=1bpp, 1=2bpp, 2=4bpp, 3=8bpp, 4=16bpp + LOG("Mode set to %d\n", data & 7); + m_mode = data & 7; + break; + + case 0xc: // written to lock in the video timing from register 0 + if (data == 0x101) + { + recalc_mode(); + } + break; + + case 0x10: + m_config = data; + + m_int_status &= ~1; + recalc_ints(); + + if (data & 1) // VBL enable + { + m_vbl_timer->adjust(m_screen->time_until_pos(m_vres, 0), 0); + } + else + { + m_vbl_timer->adjust(attotime::never); + } + break; + + case 0x18: // screen enable + m_enable = (data & 0x80) ? true : false; + break; + + case 0x1c: // drive monitor sense lines. 1 = drive, 0 = tri-state + m_monitor_id = (data & 0x7); + LOGMASKED(LOG_MONSENSE, "%x to sense drive\n", data & 0xf); + break; + + default: + LOG("Valkyrie: Unk write %08x @ %x\n", data, offset<<2); + break; + } +} + +u32 valkyrie_device::ramdac_r(offs_t offset) +{ + switch (offset) + { + case 0: + if (!machine().side_effects_disabled()) + { + m_pal_idx = 0; + } + return m_pal_address<<24; + + case 1: + { + pen_t const entry = m_palette->pen(m_pal_address); + u8 const idx = m_pal_idx; + if (!machine().side_effects_disabled()) + { + m_pal_idx++; + } + switch (idx) + { + case 0: + return ((entry >> 16) & 0xff) << 24; + case 1: + return ((entry >> 8) & 0xff) << 24; + case 2: + return (entry & 0xff) << 24; + } + } + break; + } + return 0; +} + +void valkyrie_device::ramdac_w(offs_t offset, u32 data) +{ + data >>= 24; + switch (offset) + { + case 0: + m_pal_address = data & 0xff; + m_pal_idx = 0; + break; + + case 1: + switch (m_pal_idx) + { + case 0: + m_palette->set_pen_red_level(m_pal_address, data & 0xff); + break; + case 1: + m_palette->set_pen_green_level(m_pal_address, data & 0xff); + break; + case 2: + m_palette->set_pen_blue_level(m_pal_address, data & 0xff); + break; + } + + m_pal_idx++; + if (m_pal_idx == 3) + { + m_pal_idx = 0; + m_pal_address++; + } + break; + + case 2: + LOGMASKED(LOG_RAMDAC, "%02x to DAC @ %x\n", data, offset); + break; + } +} + +void valkyrie_device::recalc_mode() +{ + // mode parameters taken from the Quadra 630 Developer Note + switch (m_video_timing) + { + case 0x101: // default + case 0x686: // 13" 640x480 + m_hres = 640; + m_vres = 480; + m_htotal = 864; + m_vtotal = 525; + m_pixel_clock = 30240000; + m_stride = 80; + break; + + case 0x282: // Rubik 512x384 + m_hres = 512; + m_vres = 384; + m_htotal = 640; + m_vtotal = 407; + m_pixel_clock = 15670000; + m_stride = 64; + break; + + case 0xb8b: // VGA 640x480 + m_hres = 640; + m_vres = 480; + m_htotal = 800; + m_vtotal = 525; + m_pixel_clock = 25180000; + m_stride = 80; + break; + + case 0x989: // 16" RGB 832x624? + m_hres = 832; + m_vres = 624; + m_htotal = 1072; + m_vtotal = 690; + m_pixel_clock = 50000000; + m_stride = 104; + break; + } + + const double refresh = (double)m_pixel_clock / (double)(m_htotal * m_vtotal); + LOGMASKED(LOG_MODE, "hres %d vres %d htotal %d vtotal %d refresh %f stride %d mode %d\n", m_hres, m_vres, m_htotal, m_vtotal, refresh, m_stride, m_mode); + if ((m_hres != 0) && (m_vres != 0)) + { + rectangle visarea(0, m_hres - 1, 0, m_vres - 1); + m_screen->configure(m_htotal, m_vtotal, visarea, attotime::from_ticks(m_htotal * m_vtotal, m_pixel_clock).as_attoseconds()); + } +} + +u32 valkyrie_device::vram_r(offs_t offset) +{ + return m_vram[offset & (m_vram_size - 1)]; +} + +void valkyrie_device::vram_w(offs_t offset, u32 data, u32 mem_mask) +{ + COMBINE_DATA(&m_vram[offset & (m_vram_size - 1)]); +} + +void valkyrie_device::recalc_ints() +{ + if (m_int_status != 0) + { + m_irq(ASSERT_LINE); + } + else + { + m_irq(CLEAR_LINE); + } +} + +TIMER_CALLBACK_MEMBER(valkyrie_device::vbl_tick) +{ + m_int_status |= 1; + recalc_ints(); + + m_vbl_timer->adjust(m_screen->time_until_pos(480, 0), 0); +} diff --git a/src/mame/apple/valkyrie.h b/src/mame/apple/valkyrie.h new file mode 100644 index 00000000000..288b7e866b2 --- /dev/null +++ b/src/mame/apple/valkyrie.h @@ -0,0 +1,67 @@ +// license:BSD-3-Clause +// copyright-holders:R. Belmont + +#ifndef MAME_APPLE_VALKYRIE_H +#define MAME_APPLE_VALKYRIE_H + +#pragma once + +#include "cpu/m68000/m68040.h" + +#include "emupal.h" +#include "screen.h" + +class valkyrie_device : public device_t +{ +public: + valkyrie_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock); + virtual ~valkyrie_device() = default; + + void map(address_map &map); + + auto write_irq() { return m_irq.bind(); } + +protected: + virtual void device_start() override; + virtual void device_reset() override; + virtual void device_add_mconfig(machine_config &config) override; + virtual ioport_constructor device_input_ports() const override; + + void recalc_ints(); + void recalc_mode(); + + u32 m_vram_size; + u32 m_pixel_clock; + + u8 m_pal_address, m_pal_idx, m_mode; + +private: + required_device m_screen; + required_device m_palette; + required_ioport m_monitor_config; + devcb_write_line m_irq; + + std::unique_ptr m_vram; + emu_timer *m_vbl_timer; + u32 m_vram_offset; + u8 m_monitor_id; + u32 m_base, m_stride, m_video_timing; + s32 m_int_status; + u32 m_hres, m_vres, m_htotal, m_vtotal, m_config; + bool m_enable; + + u32 screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect); + + u32 regs_r(offs_t offset); + void regs_w(offs_t offset, u32 data); + u32 ramdac_r(offs_t offset); + void ramdac_w(offs_t offset, u32 data); + u32 vram_r(offs_t offset); + void vram_w(offs_t offset, u32 data, u32 mem_mask); + + TIMER_CALLBACK_MEMBER(vbl_tick); +}; + +DECLARE_DEVICE_TYPE(VALKYRIE, valkyrie_device) + +#endif /* MAME_APPLE_VALKYRIE_H */ diff --git a/src/mame/mame.lst b/src/mame/mame.lst old mode 100644 new mode 100755 index b467e37879f..d0c171bc34b --- a/src/mame/mame.lst +++ b/src/mame/mame.lst @@ -904,6 +904,10 @@ macqd605 // 1993 Apple Macintosh Quadra 605 maclc475 // 1993 Apple Macintosh LC/Performa 475 maclc575 // 1994 Apple Macintosh LC/Performa 575 +@source:apple/macquadra630.cpp +macqd630 // July 18, 1994 Apple Macintosh Quadra 630 +maclc580 // April 3, 1995 Apple Macintosh LC/Performa 580 + @source:apple/macquadra700.cpp macqd700 // 1991 Apple Macintosh Quadra 700 -- cgit v1.2.3