advision: emulate the display at a lower level

This commit is contained in:
hap 2022-10-11 03:05:28 +02:00
parent b09ea40089
commit fecf31406e
2 changed files with 133 additions and 68 deletions

View File

@ -62,7 +62,7 @@ license:CC0
<!-- Homebrew -->
<software name="codered" supported="partial">
<software name="codered">
<!-- ref: https://www.m-e-g-a.org/research-education/research/adventure-vision/ -->
<description>Code Red</description>
<year>2013</year>

View File

@ -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<filter_volume_device> m_volume;
required_device<screen_device> m_screen;
required_device<timer_device> m_mirror_sync;
required_device<timer_device> m_led_update;
required_device<timer_device> m_led_off;
required_device<generic_slot_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<u8> 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<u8 []> m_display;
memory_region *m_cart_rom = nullptr;
std::vector<u8> 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<u8 []>(256 * 40);
std::fill_n(m_display.get(), 256 * 40, 0);
save_pointer(NAME(m_display), 256 * 40);
m_display = std::make_unique<u8 []>(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();