From fecf31406e79432245ce4d040f8e67da60391bc6 Mon Sep 17 00:00:00 2001 From: hap Date: Tue, 11 Oct 2022 03:05:28 +0200 Subject: [PATCH] advision: emulate the display at a lower level --- hash/advision.xml | 2 +- src/mame/entex/advision.cpp | 199 ++++++++++++++++++++++++------------ 2 files changed, 133 insertions(+), 68 deletions(-) diff --git a/hash/advision.xml b/hash/advision.xml index 0e811f48cd9..98f3b67949f 100644 --- a/hash/advision.xml +++ b/hash/advision.xml @@ -62,7 +62,7 @@ license:CC0 - + Code Red 2013 diff --git a/src/mame/entex/advision.cpp b/src/mame/entex/advision.cpp index 6277d4908d8..d9c1b7c1fc4 100644 --- a/src/mame/entex/advision.cpp +++ b/src/mame/entex/advision.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Dan Boris +// copyright-holders:Dan Boris, hap /******************************************************************************* Entex Adventure Vision, tabletop video game console @@ -15,23 +15,28 @@ Hardware notes: right button panels are electronically the same) - expansion port (unused) -The mirror rotates at around 7.5Hz, the motor speed is not controlled by software. -There's a mirror on both sides so the display refreshes at around 15Hz. A similar -technology was later used in the Nintendo Virtual Boy. - -The display is faked in MAME. On the real thing, the picture is not as stable. - A game cartridge is basically an EPROM chip wearing a jacket, there is no dedicated cartridge slot as is common on other consoles. Only 4 games were released in total. +The mirror rotates at around 7Hz, the motor speed is not controlled by software. +There's a mirror on both sides so the display refreshes at around 14Hz. A similar +technology was later used in the Nintendo Virtual Boy. + +The display is faked in MAME. On the real thing, the picture is not as stable. The +width of 150 is specified by the BIOS, but it's possible to update the leds at a +different rate, hence MAME configures a larger screen. In fact, the homebrew demo +Code Red doesn't use the BIOS for it, and runs at 50*40 to save some RAM. + TODO: - EA banking is ugly, it can be turd-polished but the real issue is in mcs48 -- display refresh is actually ~15Hz, but doing that will make MAME very sluggish -- Do the spinning mirror simulation differently? Right now it relies on the BIOS - specifying a width of 150, and the official games work fine. But it should be - possible to update the leds at a different rate. In fact, the homebrew demo - Code Red doesn't use the BIOS for it, and runs at 50*40. +- display refresh is actually ~14Hz, but doing that will make MAME very sluggish + +BTANB: +- 2 thin vertical seams (do a hyperspace in defender to see them more clearly) +- glitches at the right edge during gameplay in scobra, and also during some + explosion sounds in defender +- scobra screen is shifted to the right when the game scrolls *******************************************************************************/ @@ -62,6 +67,8 @@ public: , m_volume(*this, "volume") , m_screen(*this, "screen") , m_mirror_sync(*this, "mirror_sync") + , m_led_update(*this, "led_update") + , m_led_off(*this, "led_off") , m_cart(*this, "cartslot") , m_ea_bank(*this, "ea_bank") , m_joy(*this, "JOY") @@ -80,19 +87,20 @@ private: required_device m_volume; required_device m_screen; required_device m_mirror_sync; + required_device m_led_update; + required_device m_led_off; required_device m_cart; required_memory_bank m_ea_bank; required_ioport m_joy; - u32 screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect); - DECLARE_WRITE_LINE_MEMBER(vblank); - void vh_update(); + void io_map(address_map &map); + void program_map(address_map &map); - u8 ext_ram_r(offs_t offset); - void ext_ram_w(offs_t offset, u8 data); - u8 controller_r(); - void bankswitch_w(u8 data); + u32 screen_update(screen_device &screen, bitmap_rgb32 &bitmap, const rectangle &cliprect); void av_control_w(u8 data); + DECLARE_WRITE_LINE_MEMBER(vblank); + TIMER_DEVICE_CALLBACK_MEMBER(led_update); + TIMER_DEVICE_CALLBACK_MEMBER(led_off); DECLARE_READ_LINE_MEMBER(vsync_r); TIMER_CALLBACK_MEMBER(sound_cmd_sync); @@ -100,20 +108,25 @@ private: void sound_g_w(u8 data); void sound_d_w(u8 data); - memory_region *m_cart_rom = nullptr; - std::vector m_ext_ram; - u16 m_rambank = 0; + void bankswitch_w(u8 data); + u8 ext_ram_r(offs_t offset); + void ext_ram_w(offs_t offset, u8 data); + u8 controller_r(); - u8 m_video_enable = 0; + static constexpr u32 DISPLAY_WIDTH = 0x400; + + bool m_video_strobe = false; + bool m_video_enable = false; u8 m_video_bank = 0; - u8 m_video_hpos = 0; + u32 m_video_hpos = 0; + u8 m_led_output[5] = { }; u8 m_led_latch[5] = { }; std::unique_ptr m_display; + memory_region *m_cart_rom = nullptr; + std::vector m_ext_ram; + u16 m_rambank = 0; u8 m_sound_cmd = 0; - - void io_map(address_map &map); - void program_map(address_map &map); }; @@ -126,45 +139,55 @@ u32 advision_state::screen_update(screen_device &screen, bitmap_rgb32 &bitmap, c { for (int y = 0; y < 40; y++) { - u8 *src = &m_display[y * 256]; + u8 *src = &m_display[y * DISPLAY_WIDTH]; - for (int x = 0; x < 150; x++) + for (int x = 0; x < DISPLAY_WIDTH; x++) { - int dx = x + 1; + int dx = x; int dy = y * 2 + 1; if (cliprect.contains(dx, dy)) - bitmap.pix(dy, dx) = src[x] ? (0xff << 16) : 0; + { + u8 red = 0xff; + + if (src[x] == 0) + { + // do some horizontal interpolation + int prev = 0; + for (int i = 1; i < 5; i++) + prev += ((x - i) < 0) ? 0 : src[x - i]; + + red = (prev * 0xff) / 4; + } + + u8 green = red / 16; + u8 blue = red / 12; + + bitmap.pix(dy, dx) = red << 16 | green << 8 | blue; + } } } return 0; } -void advision_state::vh_update() -{ - u8 *dst = &m_display[m_video_hpos]; - - for (int y = 4; y >= 0; y--) - { - for (int i = 0; i < 8; i++) - dst[i * 256] = BIT(m_led_latch[y], 7 - i) ? 0 : 1; - - m_led_latch[y] = 0xff; - dst += 8 * 256; - } - - if (++m_video_hpos == 0) - logerror("HPOS OVERFLOW\n"); -} - void advision_state::av_control_w(u8 data) { - if ((m_video_enable == 0x00) && (data & 0x10)) - vh_update(); + // P25-P27: led latch select + m_video_bank = data >> 5 & 7; - m_video_enable = data & 0x10; - m_video_bank = (data & 0xe0) >> 5; + // disable led outputs (there is some delay before it takes effect) + if (m_video_bank == 0) + m_led_off->adjust(attotime::from_usec(50)); + + // P24 rising edge: transfer led latches to outputs + if (!m_video_strobe && bool(data & 0x10)) + { + std::copy_n(m_led_latch, std::size(m_led_output), m_led_output); + m_led_off->adjust(attotime::never); + m_video_enable = true; + } + m_video_strobe = bool(data & 0x10); // sync soundlatch (using gen_latch device here would give lots of logerror) machine().scheduler().synchronize(timer_expired_delegate(FUNC(advision_state::sound_cmd_sync), this), data >> 4); @@ -174,15 +197,45 @@ WRITE_LINE_MEMBER(advision_state::vblank) { if (!state && (m_screen->frame_number() & 3) == 0) { - std::fill_n(m_display.get(), 256 * 40, 0); + // starting a new frame + std::fill_n(m_display.get(), DISPLAY_WIDTH * 40, 0); + m_mirror_sync->adjust(attotime::from_usec(100)); + m_video_hpos = 0; + m_led_update->adjust(attotime::zero); } } +TIMER_DEVICE_CALLBACK_MEMBER(advision_state::led_update) +{ + // write current leds to display buffer + for (int y = 0; y < 8; y++) + { + for (int b = 0; b < 5; b++) + { + int pixel = m_video_enable ? BIT(m_led_output[b], y ^ 7) : 0; + m_display[(b * 8 + y) * DISPLAY_WIDTH + m_video_hpos] = pixel; + } + } + + if (m_video_hpos < DISPLAY_WIDTH - 1) + { + // for official games, 1 'pixel' is 60us, but there are two spots that have + // a longer duration: at x=50 and x=100 (see BTANB note about seams) + m_led_update->adjust(attotime::from_usec(10)); + m_video_hpos++; + } +} + +TIMER_DEVICE_CALLBACK_MEMBER(advision_state::led_off) +{ + m_video_enable = false; +} + READ_LINE_MEMBER(advision_state::vsync_r) { - // mirror sync pulse (half rotation) + // T1: mirror sync pulse (half rotation) return (m_mirror_sync->enabled()) ? 0 : 1; } @@ -199,16 +252,19 @@ TIMER_CALLBACK_MEMBER(advision_state::sound_cmd_sync) u8 advision_state::sound_cmd_r() { + // L0-L3: sound command return m_sound_cmd; } void advision_state::sound_g_w(u8 data) { + // G0: speaker out m_dac->write(data & 1); } void advision_state::sound_d_w(u8 data) { + // D0: speaker volume m_volume->flt_volume_set_volume((data & 1) ? 0.5 : 1.0); } @@ -220,8 +276,10 @@ void advision_state::sound_d_w(u8 data) void advision_state::bankswitch_w(u8 data) { - m_rambank = (data & 0x03) << 8; + // P10,P11: RAM bank + m_rambank = data & 3; + // P12: 8048 EA pin m_maincpu->set_input_line(MCS48_INPUT_EA, BIT(data, 2) ? ASSERT_LINE : CLEAR_LINE); if (m_cart_rom) m_ea_bank->set_entry(BIT(data, 2)); @@ -229,23 +287,26 @@ void advision_state::bankswitch_w(u8 data) u8 advision_state::ext_ram_r(offs_t offset) { - u8 data = m_ext_ram[m_rambank + offset]; + // read from external RAM + u8 data = m_ext_ram[m_rambank << 8 | offset]; if (machine().side_effects_disabled()) return data; - // the video hardware interprets reads as writes - if (m_video_bank >= 1 && m_video_bank <= 5) - m_led_latch[m_video_bank - 1] = data; + // transfer data to led latch + if (m_video_bank > 0 && m_video_bank < 6) + m_led_latch[5 - m_video_bank] = data ^ 0xff; + // reset sound cpu else if (m_video_bank == 6) - m_soundcpu->set_input_line(INPUT_LINE_RESET, (data & 0x01) ? CLEAR_LINE : ASSERT_LINE); + m_soundcpu->set_input_line(INPUT_LINE_RESET, (data & 1) ? CLEAR_LINE : ASSERT_LINE); return data; } void advision_state::ext_ram_w(offs_t offset, u8 data) { - m_ext_ram[m_rambank + offset] = data; + // write to external RAM + m_ext_ram[m_rambank << 8 | offset] = data; } void advision_state::program_map(address_map &map) @@ -308,18 +369,19 @@ void advision_state::machine_start() m_ea_bank->set_entry(0); // allocate display buffer - m_display = std::make_unique(256 * 40); - std::fill_n(m_display.get(), 256 * 40, 0); - save_pointer(NAME(m_display), 256 * 40); + m_display = std::make_unique(DISPLAY_WIDTH * 40); + std::fill_n(m_display.get(), DISPLAY_WIDTH * 40, 0); + save_pointer(NAME(m_display), DISPLAY_WIDTH * 40); // allocate external RAM m_ext_ram.resize(0x400); save_item(NAME(m_ext_ram)); - std::fill_n(m_led_latch, std::size(m_led_latch), 0xff); + save_item(NAME(m_led_output)); save_item(NAME(m_led_latch)); save_item(NAME(m_rambank)); + save_item(NAME(m_video_strobe)); save_item(NAME(m_video_enable)); save_item(NAME(m_video_bank)); save_item(NAME(m_sound_cmd)); @@ -329,6 +391,7 @@ void advision_state::machine_start() void advision_state::machine_reset() { m_video_hpos = 0; + m_led_update->adjust(attotime::never); // reset sound CPU m_soundcpu->set_input_line(INPUT_LINE_RESET, ASSERT_LINE); @@ -359,14 +422,16 @@ void advision_state::advision(machine_config &config) // video hardware SCREEN(config, m_screen, SCREEN_TYPE_RASTER); - m_screen->set_refresh_hz(4*15); // actually 15Hz + m_screen->set_refresh_hz(4*14); // see notes m_screen->set_vblank_time(0); - m_screen->set_size(150 + 2, 40 * 2 + 1); + m_screen->set_size(960, 40 * 2 + 1); m_screen->set_visarea_full(); m_screen->set_screen_update(FUNC(advision_state::screen_update)); m_screen->screen_vblank().set(FUNC(advision_state::vblank)); TIMER(config, "mirror_sync").configure_generic(nullptr); + TIMER(config, "led_update").configure_generic(FUNC(advision_state::led_update)); + TIMER(config, "led_off").configure_generic(FUNC(advision_state::led_off)); // sound hardware SPEAKER(config, "speaker").front_center();