// license:BSD-3-Clause // copyright-holders:Vas Crabb /********************************************************************** Signetics 2636 Programmable Video Interface This emulation is pretty low-level. For the most part it models the kinds of counters and flags you'd use if you were actually implementing it in programmable logic or on silicon. I'm sure there are potential performance improvements in the drawing and collision detection code. At present the entire internal space of 256 bytes can be read and written. This isn't accurate as some registers are read- or write- only, and there are several unused blocks in the address range. We should be returning some fixed value on attempt to read unreadable locations. This implementation really needs proper display timing information information to work properly. Audio pitch will be wrong if the screen's scan time is inaccurate. Positioning objects depends on the screen's visible area representing the pixel clock periods periods between blanking pulses. You also need to call the line rendering function at appropriate times if something expects to see collision or completion flags. The crude "offset" system for adjusting the position of the image has been maintained, but it's clearly inadequate for what games are doing with it, and it will inevitably lead to bugs. If an object is positioned outside the visible area using the offset mechanism, it won't be displayed and neither will its duplicates. If what's happening in the games right now is correct, the hardware must be feeding something other than the actual vertical and horizontal blanking signals to the S2623(s) so they start drawing inside the blanking region and have advanced to the desired location on reaching the visible area. Implementing this properly would require decoupling the S2623 from the screen and giving it some other means to determine when it believes the blanking periods begin and end. Sorry, analog input isn't currently supported, and neither is interrupt request/acknowledge. I have't got to them yet, and I'm still not sure whether reading a status register clears a pending interrupt. The address decoding features still aren't implemented but they'd be pretty hard to fit in the MAME framework. ADDRESS MAP ----------- start end description F00 F0D object descriptor 1 F0E F0F 2 bytes scratch F10 F1D object descriptor 2 F1E F1F 2 bytes scratch F20 F2D object descriptor 3 F2E F3F - F40 F4D object descriptor 4 F4E F6D 32 bytes scratch F6E F7F - F80 FA7 background vertical bar definition FA8 FAC background horizontal bar definition FAD FAD 1 byte scratch FAE FBF - FC0 FCF I/O and control FD0 FDF I/O and control (mirror) FE0 FEF I/O and control (mirror) FF0 FFF I/O and control (mirror) OBJECT DESCRIPTOR ----------------- 0 9 basic shape of object (10 lines of 8 horizontal clocks) A A horizontal coordinate of object (HC) B B horizontal coordinate of duplicates (HCB) C C vertical coordinate of object (VC) D D vertical offset of duplicates (VCB) I/O AND CONTROL --------------- | bit |R/W| description byte | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | | | | | FC0 | size 4| size 3| size 2| size 1| W | size of 4 objects (scale 2^x) | | | FC1 | |C1 |C2 |C3 |C1 |C2 |C3 | W | colors of 4 objects | | color 1 | color 2 | | FC2 | |C1 |C2 |C3 |C1 |C2 |C3 | W | | | color 3 | color 4 | | | | | FC3 | |frm|pos| W | scroe format and position | | | FC4 | | - | FC5 | | - | | | | FC6 | |C1 |C2 |C3 |BG | scrn colr | W | background enable and color | |backg colr |enb|C1 |C2 |C3 | | | | | FC7 | sound | W | sound square wave period | | | FC8 | N1 | N2 | W | values of the four score digits FC9 | N3 | N4 | W | | | | | obj/backgrnd |object complete| R | object/background and inter-object FCA | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | | collision status and object display | | | completion status FCB | |VR-| object collisions | R | VRLE set at leading edge of VRST, | |LE |1/2|1/3|1/3|1/4|2/4|3/4| | all bits reset when read or at | | | trailing edge of VRST | | | FCC | A/D POT 1 | R | POT1 and POT2 A/D conversion value FCD | A/D POT 2 | R | valid during VRST only | | | FCE | | - | FCF | | - | *************************************************************/ #include "emu.h" #include "machine/s2636.h" int const s2636_device::OFFS_OBJ[s2636_device::OBJ_COUNT] = { 0x000, 0x010, 0x020, 0x040 }; int const s2636_device::SCORE_START_X[2][s2636_device::SCORE_DIGITS] { { 48, 64, 96, 112 }, { 48, 64, 80, 96 } }; int const s2636_device::SCORE_START_Y[2] = { 20, 200 }; uint16_t const s2636_device::SCORE_FONT[16][5] = { { 0x0fff, 0x0f0f, 0x0f0f, 0x0f0f, 0x0fff }, { 0x0f00, 0x0f00, 0x0f00, 0x0f00, 0x0f00 }, { 0x0fff, 0x0f00, 0x0fff, 0x000f, 0x0fff }, { 0x0fff, 0x0f00, 0x0fff, 0x0f00, 0x0fff }, { 0x0f0f, 0x0f0f, 0x0fff, 0x0f00, 0x0f00 }, { 0x0fff, 0x000f, 0x0fff, 0x0f00, 0x0fff }, { 0x0fff, 0x000f, 0x0fff, 0x0f0f, 0x0fff }, { 0x0fff, 0x0f00, 0x0f00, 0x0f00, 0x0f00 }, { 0x0fff, 0x0f0f, 0x0fff, 0x0f0f, 0x0fff }, { 0x0fff, 0x0f0f, 0x0fff, 0x0f00, 0x0fff }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }, { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 } }; /************************************* * * Device interface * *************************************/ const device_type S2636 = &device_creator; s2636_device::s2636_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, S2636, "Signetics 2636", tag, owner, clock, "s2636", __FILE__) , device_video_interface(mconfig, *this) , device_sound_interface(mconfig, *this) , m_divider(1) , m_y_offset(0) , m_x_offset(0) , m_intreq_cb(*this) , m_vrst(false) , m_screen_line(0) , m_vis_line(0) , m_intreq(CLEAR_LINE) , m_intack(CLEAR_LINE) , m_stream(nullptr) , m_sample_cnt(0) , m_sound_lvl(false) { for (auto &elem : m_registers) elem = 0; for (auto &elem : m_obj_cnt) elem = 0; for (auto &elem : m_obj_disp) elem = false; for (auto &elem : m_obj_dup) elem = false; } //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- void s2636_device::device_start() { m_bitmap.resize(m_screen->width(), m_screen->height()); save_item(NAME(m_bitmap)); save_item(NAME(m_registers)); save_item(NAME(m_vrst)); save_item(NAME(m_screen_line)); save_item(NAME(m_vis_line)); save_item(NAME(m_obj_cnt)); save_item(NAME(m_obj_disp)); save_item(NAME(m_obj_dup)); save_item(NAME(m_intreq)); save_item(NAME(m_intack)); m_stream = machine().sound().stream_alloc(*this, 0, 1, machine().sample_rate()); save_item(NAME(m_sample_cnt)); save_item(NAME(m_sound_lvl)); m_intreq_cb.resolve_safe(); } //------------------------------------------------- // backwards-compatible update method //------------------------------------------------- bitmap_ind16 const &s2636_device::update(const rectangle &cliprect) { m_vrst = true; m_screen_line = m_screen->visible_area().min_y; while (m_screen_line <= m_screen->visible_area().max_y) render_next_line(); return m_bitmap; } //------------------------------------------------- // render the first line into the bitmap //------------------------------------------------- void s2636_device::render_first_line() { m_screen_line = 0; render_next_line(); } //------------------------------------------------- // render next line into the bitmap //------------------------------------------------- void s2636_device::render_next_line() { assert(m_screen_line < m_bitmap.height()); // pre-clear the line for convenience rectangle const &vis_area = m_screen->visible_area(); uint16_t *const row = &m_bitmap.pix16(m_screen_line); m_bitmap.plot_box(0, m_screen_line, m_bitmap.width(), 1, 0); if ((vis_area.min_y > m_screen_line) || (vis_area.max_y < m_screen_line)) { if (!m_vrst) { m_registers[REG_VBL_COL_OBJ] |= 0x40; m_vrst = true; update_intreq(ASSERT_LINE); } } else { // check for first visible line if (m_vrst) { // latch vertical position primary objects for (int i = 0; i < OBJ_COUNT; i++) { m_obj_cnt[i] = m_registers[OFFS_OBJ[i] + OFFS_VC] + m_y_offset; m_obj_disp[i] = false; m_obj_dup[i] = false; } // collision, completion and vertical reset flags are cleared here m_registers[REG_COL_BG_CMPL] = 0x00; m_registers[REG_VBL_COL_OBJ] = 0x00; // set our internal tracking flags and clear interrupt m_vrst = false; m_vis_line = 0; update_intreq(CLEAR_LINE); } // work out what object pixels belong in this line for (int i = 0; i < OBJ_COUNT; i++) { // repurpose counter and set flag when we've skipped enough lines if (!m_obj_cnt[i]) { m_obj_cnt[i] = OBJ_HEIGHT << 3; m_obj_disp[i] = true; } if (m_obj_disp[i]) { int const obj_inc = 1 << (3 - object_scale(i)); m_obj_cnt[i] -= obj_inc; // fetch appropriate line from object uint8_t const obj_bits = m_registers[OFFS_OBJ[i] + OBJ_HEIGHT - 1 - (m_obj_cnt[i] >> 3)]; uint16_t const obj_clr = object_color(i) | 0x08 | (0x10 << i); // blit it to the line ignoring intermediate pixels int const obj_h_cnt = m_registers[OFFS_OBJ[i] + (m_obj_dup[i] ? OFFS_HCB : OFFS_HC)] + m_x_offset; for (int x = 0, screen_col = vis_area.min_x + (obj_h_cnt * m_divider); (OBJ_WIDTH << 3) > x && (vis_area.max_x >= screen_col); ) { bool const bit = bool((obj_bits << (x >> 3)) & 0x80); if (bit && (vis_area.min_x <= screen_col)) row[screen_col] |= obj_clr; x += obj_inc; screen_col += m_divider; } // if that's the last line of the object, flag completion and prepare for duplicates if (!m_obj_cnt[i]) { m_registers[REG_COL_BG_CMPL] |= 0x01 << (3 - i); m_obj_cnt[i] = 1 + m_registers[OFFS_OBJ[i] + OFFS_VCB]; m_obj_disp[i] = false; m_obj_dup[i] = true; update_intreq(ASSERT_LINE); } } else { // count down lines to display object m_obj_cnt[i]--; } } // let's take a look at the score display uint16_t const bg_clr = m_registers[REG_BG_ENB_CLR] & 0x07; int const score_row = m_vis_line - m_y_offset - SCORE_START_Y[m_registers[REG_SCORE_FMT] & 0x01]; if ((0 <= score_row) && (SCORE_HEIGHT > score_row)) { int const (&score_start_x)[SCORE_DIGITS] = SCORE_START_X[(m_registers[REG_SCORE_FMT] >> 1) & 0x01]; for (int i = 0; i < SCORE_DIGITS; i++) { uint16_t score_bits = SCORE_FONT[score_digit(i)][score_row >> 2]; int screen_col = vis_area.min_x + ((score_start_x[i] + m_x_offset) * m_divider); while (score_bits && (vis_area.max_x >= screen_col)) { if (score_bits & 0x0001) row[screen_col] |= bg_clr | 0x08; score_bits >>= 1; screen_col += m_divider; } } } // work out how the background pattern will be drawn bool const bg_enable = bool(m_registers[REG_BG_ENB_CLR] & 0x08); int const bg_row = m_vis_line - m_y_offset - BG_START_Y; bool const bg_draw = bg_enable && (0 <= bg_row) && (BG_HEIGHT > bg_row); int const bg_vbar_offs = OFFS_VBAR_DEF + ((bg_row / 20) << 2) + (((bg_row % 20) >= 2) ? 2 : 0); int const bg_hbar_offs = OFFS_HBAR_DEF + (bg_row / 40); uint16_t const bg_vbar_bits = (uint16_t(m_registers[bg_vbar_offs]) << 8) | uint16_t(m_registers[bg_vbar_offs + 1]); uint8_t const bg_hbar_bits = m_registers[bg_hbar_offs]; bool const bg_hbar_stretch = bool(bg_hbar_bits & (1 << ((((bg_row % 40) >= 20) ? 3 : 0) + (((bg_row % 20) >= 11) ? 2 : ((bg_row % 20) >= 2) ? 1 : 0)))); int const bg_hbar_width = bg_hbar_stretch ? 8 : (0xc0 == (bg_hbar_bits & 0xc0)) ? 4 : (0x40 == (bg_hbar_bits & 0xc0)) ? 2 : 1; uint16_t const scrn_clr = bg_enable ? ((m_registers[REG_BG_ENB_CLR] >> 4) & 0x07) : 0x00; for (int screen_col = vis_area.min_x, x = 0; vis_area.max_x >= screen_col; x++) { // check object-object collisions if ((row[screen_col] & 0x10) && (row[screen_col] & 0x20)) m_registers[REG_VBL_COL_OBJ] |= 0x20; if ((row[screen_col] & 0x10) && (row[screen_col] & 0x40)) m_registers[REG_VBL_COL_OBJ] |= 0x10; if ((row[screen_col] & 0x10) && (row[screen_col] & 0x80)) m_registers[REG_VBL_COL_OBJ] |= 0x08; if ((row[screen_col] & 0x20) && (row[screen_col] & 0x40)) m_registers[REG_VBL_COL_OBJ] |= 0x04; if ((row[screen_col] & 0x20) && (row[screen_col] & 0x80)) m_registers[REG_VBL_COL_OBJ] |= 0x02; if ((row[screen_col] & 0x40) && (row[screen_col] & 0x80)) m_registers[REG_VBL_COL_OBJ] |= 0x01; // work out if the background hits this pixel int const bg_col = x - m_x_offset - BG_START_X; bool const bg = bool(bg_vbar_bits & (1U << (15 - (bg_col >> 3)))); if (bg_draw && (0 <= bg_col) && (BG_WIDTH > bg_col) && bg && (bg_hbar_width > (bg_col & 0x07))) { // do object-background collisions if (row[screen_col] & 0x10) m_registers[REG_COL_BG_CMPL] |= 0x80; if (row[screen_col] & 0x20) m_registers[REG_COL_BG_CMPL] |= 0x40; if (row[screen_col] & 0x40) m_registers[REG_COL_BG_CMPL] |= 0x20; if (row[screen_col] & 0x80) m_registers[REG_COL_BG_CMPL] |= 0x10; if (!(row[screen_col] & 0x08)) row[screen_col] = bg_clr; } else if (!(row[screen_col] & 0x08)) { // draw screen background if not obscured row[screen_col] = scrn_clr; } // clear collision crud and deal with pixel clock divider ratio row[screen_col++] &= 0x0f; for (int i = 1; (i < m_divider) && (vis_area.max_x >= screen_col); i++, screen_col++) { row[screen_col] = row[screen_col - 1]; } } } m_screen_line++; m_vis_line++; } //------------------------------------------------- // bus access handlers //------------------------------------------------- READ8_MEMBER( s2636_device::read_data ) { mask_offset(offset); uint8_t data = m_registers[offset]; // TODO: some locations are not readable, should return some garbage switch (offset) { case REG_COL_BG_CMPL: case REG_VBL_COL_OBJ: if (!space.debugger_access()) m_registers[offset] = 0x00; // collision/completion/VRESET flags reset on read break; } return data; } WRITE8_MEMBER( s2636_device::write_data ) { mask_offset(offset); if (REG_SND_PERIOD == offset) m_stream->update(); m_registers[offset] = data; } WRITE_LINE_MEMBER( s2636_device::write_intack ) { assert((ASSERT_LINE == state) || (HOLD_LINE == state) || (CLEAR_LINE == state) || (PULSE_LINE == state)); // pretend interrupt acknowledge is handled instantaneously m_intack = state; update_intreq(m_intreq); if (ASSERT_LINE != m_intreq) m_intack = CLEAR_LINE; } //------------------------------------------------- // sound_stream_update - generate audio output //------------------------------------------------- void s2636_device::sound_stream_update(sound_stream &stream, stream_sample_t **inputs, stream_sample_t **outputs, int samples) { stream_sample_t *buffer = outputs[0]; while (samples--) { if (!m_sample_cnt) { if (m_registers[REG_SND_PERIOD]) { m_sample_cnt = (machine().sample_rate() * (m_registers[REG_SND_PERIOD] + 1) * m_screen->scan_period()).seconds(); m_sound_lvl = !m_sound_lvl; } else { m_sample_cnt = 1; m_sound_lvl = false; } } *buffer++ = m_sound_lvl ? 0x7fff : 0x0000; m_sample_cnt--; } } void s2636_device::update_intreq(int value) { int const new_value = m_intack ? CLEAR_LINE : value; if (new_value != m_intreq) { m_intreq = new_value; m_intreq_cb(m_intreq); } }