// license:BSD-3-Clause
// copyright-holders:Vas Crabb
/*
Laser Battle / Lazarian (c) 1981 Zaccaria
Cat and Mouse (c) 1982 Zaccaria
video emulation by Vas Crabb
This is an absolutely insane arrangement of three Signetics S2623
PVIs and custom TTL logic. The PVIs can each render up to four
(potentially duplicated) sprites. The TTL logic renders a single
32x32 pixel 4-colour sprite and an 8-colour background tilemap.
There are also two symmetrical area effects where the game only
needs to program the horizontal distance from the screen edge for
each line, and a per-line single-pixel shell effect. The shell
effect replaces one of the area effects if used.
In order to get 30% more horizontal resolution than Signetics
intended, this board divides the master clock by 4 to drive the
S2621 sync generator, but has a separate set of filp-flops to do
a symmetric divide by 3 to drive the rest of the video hardware.
There's some extra logic to align the first pixel to the end of the
horizontal blanking period period because the line isn't a whole
number of pixels. The visible portion isn't a whole number of
pixels for that matter, either.
There's some fancy logic to stretch the vertical blanking period for
eight additional lines after the USG deasserts VRST and then to
start the next vertical blanking period after 247 visible lines.
The first visible line is line 8 from the point of view of the PVIs,
background generator and sprite generator.
The first visible column of the display is pixel 8 from the point of
of view of the sprite and background generation hardware, but it's
pixel 0 from the point of view of the PVIs.
The hardware has 8-bit RRRGGGBBB output converted to analog levels
with a simple resistor network driven by open-collector gates.
However video is actually generated in a 16-bit internal colour
space and mapped onto the 8-bit output colour space using a PLA.
The equations in the PLA for Laser Battle/Lazarian give the
following graphics priorities, from highest to lowest:
* TTL-generated sprite
* PVIs (colours ORed, object/score output ignored)
* Shell/area effect 2
* Background tilemap
* Area effect 1
The Cat and Mouse PLA program gives completely different priorities,
once again from highest to lowest:
* Background tilemap
* PVIs (colours ORed, object/score output ignored)
* TTL-generated sprite
* Shell
Cat and Mouse uses some signals completely differently. LUM affects
the background palette rather than the sprite palette, area effect 1
affects the background palette, and area effect 2 is completely
unused.
The game board has no logic for flipping the screen in cocktail
mode. It just provides an active-low open collector out with pull-
up indicating when player 2 is playing. In a cocktail cabinet this
goes to an "image commutation board". It's not connected to
anything in an upright cabinet. The "image commutation board" must
flip the image somehow, presumably by reversing the deflection coil
connections.
There are still issues with horizontal alignment between layers. I
have the schematic, yet I really can't understand where these issues
are coming from. I'm pretty sure alignment between TTL background
and sprites is right, judging from gameplay. I'm not sure about
alignment with the effect layers.
There are definitely alignment problems with the PVI opjects, but
that may be a bug in the S2636 implementation. I need to check it
more detail
*/
#include "emu.h"
#include "includes/laserbat.h"
WRITE8_MEMBER(laserbat_state_base::videoram_w)
{
if (!m_mpx_bkeff)
m_bg_ram[offset] = data;
else
m_eff_ram[offset & 0x1ff] = data; // A9 is not connected, only half the chip is used
}
WRITE8_MEMBER(laserbat_state_base::wcoh_w)
{
// sprite horizontal offset
m_wcoh = data;
}
WRITE8_MEMBER(laserbat_state_base::wcov_w)
{
// sprite vertical offset
m_wcov = data;
}
WRITE8_MEMBER(laserbat_state_base::cnt_eff_w)
{
/*
+-----+-------------+-----------------------------------------------+
| bit | name | description |
+-----+-------------+-----------------------------------------------+
| 0 | /ABEFF1 | effect 1 enable |
| 1 | /ABEFF2 | effect 2/shell enable |
| 2 | MPX EFF2 SH | select SHELL point or EFF2 area for effect 2 |
| 3 | COLEFF 0 | area effect colour bit 0 |
| 4 | COLEFF 1 | area effect colour bit 1 |
| 5 | /NEG 1 | select inside/outside area for effect 1 |
| 6 | /NEG 2 | select inside/outside area for effect 2 |
| 7 | MPX P_1/2 | selects input row 2 |
+-----+-------------+-----------------------------------------------+
*/
m_abeff1 = !bool(data & 0x01);
m_abeff2 = !bool(data & 0x02);
m_mpx_eff2_sh = bool(data & 0x04);
m_coleff = (data >> 3) & 0x03;
m_neg1 = !bool(data & 0x20);
m_neg2 = !bool(data & 0x40);
m_mpx_p_1_2 = bool(data & 0x80);
// popmessage("effect: 0x%02X", data);
}
WRITE8_MEMBER(laserbat_state_base::cnt_nav_w)
{
/*
+-----+-----------+--------------------------------------+
| bit | name | description |
+-----+-----------+--------------------------------------+
| 0 | /NAVE | sprite enable |
| 1 | CLR0 | sprite colour bit 0 |
| 2 | CLR1 | sprite colour bit 1 |
| 3 | LUM | sprite luminance |
| 4 | MPX BKEFF | access background RAM or effect RAM |
| 5 | SHPA | sprite select bit 0 |
| 6 | SHPB | sprite select bit 1 |
| 7 | SHPC | sprite select bit 2 |
+-----+-----------+--------------------------------------+
*/
m_nave = !bool(data & 0x01);
m_clr_lum = (data >> 1) & 0x07;
m_mpx_bkeff = bool(data & 0x10);
m_shp = (data >> 5) & 0x07;
// popmessage("nav: 0x%02X", data);
}
void laserbat_state_base::video_start()
{
// we render straight from ROM
m_gfx1 = memregion("gfx1")->base();
m_gfx2 = memregion("gfx2")->base();
// start rendering scanlines
m_screen->register_screen_bitmap(m_bitmap);
m_scanline_timer->adjust(m_screen->time_until_pos(1, 0));
}
uint32_t laserbat_state_base::screen_update_laserbat(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect)
{
bool const flip_y = flip_screen_y(), flip_x = flip_screen_x();
int const offs_y = m_screen->visible_area().max_y + m_screen->visible_area().min_y;
int const offs_x = m_screen->visible_area().max_x + m_screen->visible_area().min_x;
for (int y = cliprect.min_y; cliprect.max_y >= y; y++)
{
uint16_t const *const src = &m_bitmap.pix16(flip_y ? (offs_y - y) : y);
uint16_t *dst = &bitmap.pix16(y);
for (int x = cliprect.min_x; cliprect.max_x >= x; x++)
{
dst[x] = uint16_t(m_gfxmix->read(src[flip_x ? (offs_x - x) : x]));
}
}
return 0;
}
TIMER_CALLBACK_MEMBER(laserbat_state_base::video_line)
{
/*
+-----+---------+----------------------------------+-------------------------------------+
| bit | name | laserbat/lazarian | catnmous |
+-----+---------+----------------------------------+-------------------------------------+
| 0 | NAV0 | sprite bit 0 | sprite bit 0 |
| 1 | NAV1 | sprite bit 1 | sprite bit 1 |
| 2 | CLR0 | sprite palette bit 0 | sprite palette bit 0 |
| 3 | CLR1 | sprite palette bit 1 | sprite palette bit 1 |
| 4 | LUM | sprite luminance | background tilemap palette control |
| 5 | C1* | combined PVI red (active low) | combined PVI red (active low) |
| 6 | C2* | combined PVI green (active low) | combined PVI green (active low) |
| 7 | C3* | combined PVI blue (active low) | combined PVI blue (active low) |
| 8 | BKR | background tilemap red | background tilemap bit 0 |
| 9 | BKG | background tilemap green | background tilemap bit 1 |
| 10 | BKB | background tilemap blue | background tilemap bit 2 |
| 11 | SHELL | shell point | shell point |
| 12 | EFF1 | effect 1 area | background tilemap palette control |
| 13 | EFF2 | effect 2 area | unused |
| 14 | COLEFF0 | area effect colour bit 0 | background tilemap palette control |
| 15 | COLEFF1 | area effect colour bit 1 | background tilemap palette control |
+-----+---------+----------------------------------+-------------------------------------+
*/
assert(m_bitmap.width() > m_screen->visible_area().max_x);
assert(m_bitmap.height() > m_screen->visible_area().max_y);
// prep some useful values
int const y = m_screen->vpos();
int const min_x = m_screen->visible_area().min_x;
int const max_x = m_screen->visible_area().max_x;
int const x_offset = min_x - (8 * 3);
int const y_offset = m_screen->visible_area().min_y - 8;
uint16_t *const row = &m_bitmap.pix16(y);
// wait for next scanline
m_scanline_timer->adjust(m_screen->time_until_pos(y + 1, 0));
// update the PVIs
if (!y)
{
m_pvi1->render_first_line();
m_pvi2->render_first_line();
m_pvi3->render_first_line();
}
else
{
m_pvi1->render_next_line();
m_pvi2->render_next_line();
m_pvi3->render_next_line();
}
uint16_t const *const pvi1_row = &m_pvi1->bitmap().pix16(y);
uint16_t const *const pvi2_row = &m_pvi2->bitmap().pix16(y);
uint16_t const *const pvi3_row = &m_pvi3->bitmap().pix16(y);
// don't draw outside the visible area
m_bitmap.plot_box(0, y, m_bitmap.width(), 1, 0);
if ((m_screen->visible_area().min_y > y) || (m_screen->visible_area().max_y < y))
return;
// render static effect bits
uint16_t const static_bits = ((uint16_t(m_coleff) << 14) & 0xc000) | ((uint16_t(m_clr_lum) << 2) & 0x001c);
m_bitmap.plot_box(min_x, y, max_x - min_x + 1, 1, static_bits);
// render the TTL-generated background tilemap
unsigned const bg_row = (y - y_offset) & 0x07;
uint8_t const *const bg_src = &m_bg_ram[((y - y_offset) << 2) & 0x3e0];
for (unsigned byte = 0, px = x_offset + (9 * 3); max_x >= px; byte++)
{
uint16_t const tile = (uint16_t(bg_src[byte & 0x1f]) << 3) & 0x7f8;
uint8_t red = m_gfx1[0x0000 | tile | bg_row];
uint8_t green = m_gfx1[0x0800 | tile | bg_row];
uint8_t blue = m_gfx1[0x1000 | tile | bg_row];
for (unsigned pixel = 0; 8 > pixel; pixel++, red <<= 1, green <<= 1, blue <<= 1)
{
uint16_t const bg = ((red & 0x80) ? 0x0100 : 0x0000) | ((green & 0x80) ? 0x0200 : 0x0000) | ((blue & 0x80) ? 0x0400 : 0x0000);
if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
px++;
if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
px++;
if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
px++;
}
}
// render shell/effect graphics
uint8_t const eff1_val = m_eff_ram[((y - y_offset) & 0xff) | 0x100];
uint8_t const eff2_val = m_eff_ram[((y - y_offset) & 0xff) | 0x000];
for (int x = 0, px = x_offset; max_x >= px; x++)
{
// calculate area effects
// I have no idea where the magical x offset comes from but it's necessary
bool const right_half = bool((x + 0) & 0x80);
bool const eff1_cmp = right_half ? (uint8_t((x + 0) & 0x7f) < (eff1_val & 0x7f)) : (uint8_t((x + 0) & 0x7f) > (~eff1_val & 0x7f));
bool const eff2_cmp = right_half ? (uint8_t((x + 0) & 0x7f) < (eff2_val & 0x7f)) : (uint8_t((x + 0) & 0x7f) > (~eff2_val & 0x7f));
bool const eff1 = m_abeff1 && (m_neg1 ? !eff1_cmp : eff1_cmp);
bool const eff2 = m_abeff2 && (m_neg2 ? !eff2_cmp : eff2_cmp) && m_mpx_eff2_sh;
// calculate shell point effect
// using the same magical offset as the area effects
bool const shell = m_abeff2 && (uint8_t((x + 0) & 0xff) == (eff2_val & 0xff)) && !m_mpx_eff2_sh;
// set effect bits, and mix in PVI graphics while we're here
uint16_t const effect_bits = (shell ? 0x0800 : 0x0000) | (eff1 ? 0x1000 : 0x0000) | (eff2 ? 0x2000 : 0x0000);
uint16_t pvi_bits = ~(pvi1_row[px] | pvi2_row[px] | pvi3_row[px]);
pvi_bits = ((pvi_bits & 0x01) << 7) | ((pvi_bits & 0x02) << 5) | ((pvi_bits & 0x04) << 3);
if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
px++;
if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
px++;
if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
px++;
}
// render the TTL-generated sprite
// more magic offsets here I don't understand the source of
if (m_nave)
{
int const sprite_row = y + y_offset - ((256 - m_wcov) & 0x0ff);
if ((0 <= sprite_row) && (32 > sprite_row))
{
for (unsigned byte = 0, x = x_offset + (3 * ((256 - m_wcoh + 5) & 0x0ff)); 8 > byte; byte++)
{
uint8_t bits = m_gfx2[((m_shp << 8) & 0x700) | ((sprite_row << 3) & 0x0f8) | (byte & 0x07)];
for (unsigned pixel = 0; 4 > pixel; pixel++, bits <<= 2)
{
if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
}
}
}
}
}
PALETTE_INIT_MEMBER(laserbat_state, laserbat)
{
/*
Uses GRBGRBGR pixel format. The two topmost bist are the LSBs
for red and green. LSB for blue is always effectively 1. The
middle group is the MSB. Yet another crazy thing they did.
Each colour channel has an emitter follower buffer amlpifier
biased with a 1k resistor to +5V and a 3k3 resistor to ground.
Output is adjusted by connecting additional resistors across the
leg to ground using an open collector buffer - 270R, 820R and
1k0 for unset MSB to LSB, respectively (blue has no LSB so it
has no 1k0 resistor).
Assuming 0.7V drop across the emitter follower and no drop
across the open collector buffer, these are the approximate
output voltages:
0.0000, 0.1031, 0.1324, 0.2987, 0.7194, 1.2821, 1.4711, 3.1372
The game never sets the colour to any value above 4, effectively
treating it as 5-level red and green, and 3-level blue, for a
total of 75 usable colours.
From the fact that there's no DC offset on red and green, and
the highest value used is just over 0.7V, I'm guessing the game
expects to drive a standard 0.7V RGB monitor, and higher colour
values would simply saturate the input. To make it not look
like the inside of a coal mine, I've applied gamma decoding at
2.2
However there's that nasty DC offset on the blue caused by the
fact that it has no LSB, but it's eliminated at the AC-coupling
of the input and output of the buffer amplifier on the monitor
interface board. I'm treating it as though it has the same gain
as the other channels. After gamma adjustment, medium red and
medium blue as used by the game have almost the same intensity.
*/
int const weights[] = { 0, 107, 120, 173, 255, 255, 255, 255 };
int const blue_weights[] = { 0, 0, 60, 121, 241, 255, 255, 255 };
for (int entry = 0; palette.entries() > entry; entry++)
{
uint8_t const bits(entry & 0xff);
uint8_t const r(((bits & 0x01) << 1) | ((bits & 0x08) >> 1) | ((bits & 0x40) >> 6));
uint8_t const g(((bits & 0x02) >> 0) | ((bits & 0x10) >> 2) | ((bits & 0x80) >> 7));
uint8_t const b(((bits & 0x04) >> 1) | ((bits & 0x20) >> 3) | 0x01);
palette.set_pen_color(entry, rgb_t(weights[r], weights[g], blue_weights[b]));
}
}
PALETTE_INIT_MEMBER(catnmous_state, catnmous)
{
/*
Uses GRBGRBGR pixel format. The two topmost bist are the LSBs
for red and green. The middle group is the MSB. Yet another
crazy thing they did.
Each colour channel has an emitter follower buffer amlpifier
biased with a 1k resistor to +5V and a 3k3 resistor to ground.
Output is adjusted by connecting additional resistors across the
leg to ground using an open collector buffer. Red and green use
560R, 820R and 1k0 for unset MSB to LSB, respectively. Blue
uses 47R and 820R on the PCB we have a photo of, although the
47R resistor looks like it could be a bad repair (opposite
orientation and burn marks on PCB).
Assuming 0.7V drop across the emitter follower and no drop
across the open collector buffer, these are the approximate
output voltages for red and green:
0.2419, 0.4606, 0.5229, 0.7194, 0.9188, 1.2821, 1.4711, 3.1372
The game uses all colour values except 4. The DC offset will be
eliminated by the AC coupling on the monitor interface board.
The differences steps aren't very linear, they vary from 0.06V
to 0.36V with no particular order. The input would be expected
to saturate somewhere inside the big jump to the highest level.
Let's assume the 47R resistor is a bad repair and it's supposed
to be 470R. That gives us these output voltages for blue:
0.3752, 0.7574, 1.2821, 3.1372
To make life easier, I'll assume the monitor is expected to have
half the gain of a standard monitor and no gamma decoding is
necessary.
*/
int const weights[] = { 0, 40, 51, 87, 123, 189, 224, 255 };
int const blue_weights[] = { 0, 70, 165, 255 };
for (int entry = 0; palette.entries() > entry; entry++)
{
uint8_t const bits(entry & 0xff);
uint8_t const r(((bits & 0x01) << 1) | ((bits & 0x08) >> 1) | ((bits & 0x40) >> 6));
uint8_t const g(((bits & 0x02) >> 0) | ((bits & 0x10) >> 2) | ((bits & 0x80) >> 7));
uint8_t const b(((bits & 0x04) >> 2) | ((bits & 0x20) >> 4));
palette.set_pen_color(entry, rgb_t(weights[r], weights[g], blue_weights[b]));
}
}