1 // license:BSD-3-Clause
2 // copyright-holders:Vas Crabb
3 /*
4     Laser Battle / Lazarian (c) 1981 Zaccaria
5     Cat and Mouse           (c) 1982 Zaccaria
6 
7     video emulation by Vas Crabb
8 
9     This is an absolutely insane arrangement of three Signetics S2623
10     PVIs and custom TTL logic.  The PVIs can each render up to four
11     (potentially duplicated) sprites.  The TTL logic renders a single
12     32x32 pixel 4-colour sprite and an 8-colour background tilemap.
13     There are also two symmetrical area effects where the game only
14     needs to program the horizontal distance from the screen edge for
15     each line, and a per-line single-pixel shell effect.  The shell
16     effect replaces one of the area effects if used.
17 
18     In order to get 30% more horizontal resolution than Signetics
19     intended, this board divides the master clock by 4 to drive the
20     S2621 sync generator, but has a separate set of filp-flops to do
21     a symmetric divide by 3 to drive the rest of the video hardware.
22     There's some extra logic to align the first pixel to the end of the
23     horizontal blanking period period because the line isn't a whole
24     number of pixels.  The visible portion isn't a whole number of
25     pixels for that matter, either.
26 
27     There's some fancy logic to stretch the vertical blanking period for
28     eight additional lines after the USG deasserts VRST and then to
29     start the next vertical blanking period after 247 visible lines.
30 
31     The first visible line is line 8 from the point of view of the PVIs,
32     background generator and sprite generator.
33 
34     The first visible column of the display is pixel 8 from the point of
35     of view of the sprite and background generation hardware, but it's
36     pixel 0 from the point of view of the PVIs.
37 
38     The hardware has 8-bit RRRGGGBBB output converted to analog levels
39     with a simple resistor network driven by open-collector gates.
40     However video is actually generated in a 16-bit internal colour
41     space and mapped onto the 8-bit output colour space using a PLA.
42 
43     The equations in the PLA for Laser Battle/Lazarian give the
44     following graphics priorities, from highest to lowest:
45     * TTL-generated sprite
46     * PVIs (colours ORed, object/score output ignored)
47     * Shell/area effect 2
48     * Background tilemap
49     * Area effect 1
50 
51     The Cat and Mouse PLA program gives completely different priorities,
52     once again from highest to lowest:
53     * Background tilemap
54     * PVIs (colours ORed, object/score output ignored)
55     * TTL-generated sprite
56     * Shell
57 
58     Cat and Mouse uses some signals completely differently.  LUM affects
59     the background palette rather than the sprite palette, area effect 1
60     affects the background palette, and area effect 2 is completely
61     unused.
62 
63     The game board has no logic for flipping the screen in cocktail
64     mode.  It just provides an active-low open collector out with pull-
65     up indicating when player 2 is playing.  In a cocktail cabinet this
66     goes to an "image commutation board".  It's not connected to
67     anything in an upright cabinet.  The "image commutation board" must
68     flip the image somehow, presumably by reversing the deflection coil
69     connections.
70 
71     There are still issues with horizontal alignment between layers.  I
72     have the schematic, yet I really can't understand where these issues
73     are coming from.  I'm pretty sure alignment between TTL background
74     and sprites is right, judging from gameplay.  I'm not sure about
75     alignment with the effect layers.
76 
77     There are definitely alignment problems with the PVI objects, but
78     that may be a bug in the S2636 implementation.  I need to check it
79     in more detail.
80 */
81 
82 #include "emu.h"
83 #include "includes/laserbat.h"
84 
85 
videoram_w(offs_t offset,uint8_t data)86 void laserbat_state_base::videoram_w(offs_t offset, uint8_t data)
87 {
88 	if (!m_mpx_bkeff)
89 		m_bg_ram[offset] = data;
90 	else
91 		m_eff_ram[offset & 0x1ff] = data; // A9 is not connected, only half the chip is used
92 }
93 
wcoh_w(uint8_t data)94 void laserbat_state_base::wcoh_w(uint8_t data)
95 {
96 	// sprite horizontal offset
97 	m_wcoh = data;
98 }
99 
wcov_w(uint8_t data)100 void laserbat_state_base::wcov_w(uint8_t data)
101 {
102 	// sprite vertical offset
103 	m_wcov = data;
104 }
105 
cnt_eff_w(uint8_t data)106 void laserbat_state_base::cnt_eff_w(uint8_t data)
107 {
108 	/*
109 	    +-----+-------------+-----------------------------------------------+
110 	    | bit |    name     | description                                   |
111 	    +-----+-------------+-----------------------------------------------+
112 	    |  0  | /ABEFF1     | effect 1 enable                               |
113 	    |  1  | /ABEFF2     | effect 2/shell enable                         |
114 	    |  2  | MPX EFF2 SH | select SHELL point or EFF2 area for effect 2  |
115 	    |  3  | COLEFF 0    | area effect colour bit 0                      |
116 	    |  4  | COLEFF 1    | area effect colour bit 1                      |
117 	    |  5  | /NEG 1      | select inside/outside area for effect 1       |
118 	    |  6  | /NEG 2      | select inside/outside area for effect 2       |
119 	    |  7  | MPX P_1/2   | selects input row 2                           |
120 	    +-----+-------------+-----------------------------------------------+
121 	*/
122 
123 	m_abeff1 = !bool(data & 0x01);
124 	m_abeff2 = !bool(data & 0x02);
125 	m_mpx_eff2_sh = bool(data & 0x04);
126 	m_coleff = (data >> 3) & 0x03;
127 	m_neg1 = !bool(data & 0x20);
128 	m_neg2 = !bool(data & 0x40);
129 	m_mpx_p_1_2 = bool(data & 0x80);
130 
131 //  popmessage("effect: 0x%02X", data);
132 }
133 
cnt_nav_w(uint8_t data)134 void laserbat_state_base::cnt_nav_w(uint8_t data)
135 {
136 	/*
137 	    +-----+-----------+--------------------------------------+
138 	    | bit |   name    | description                          |
139 	    +-----+-----------+--------------------------------------+
140 	    |  0  | /NAVE     | sprite enable                        |
141 	    |  1  | CLR0      | sprite colour bit 0                  |
142 	    |  2  | CLR1      | sprite colour bit 1                  |
143 	    |  3  | LUM       | sprite luminance                     |
144 	    |  4  | MPX BKEFF | access background RAM or effect RAM  |
145 	    |  5  | SHPA      | sprite select bit 0                  |
146 	    |  6  | SHPB      | sprite select bit 1                  |
147 	    |  7  | SHPC      | sprite select bit 2                  |
148 	    +-----+-----------+--------------------------------------+
149 	*/
150 
151 	m_nave = !bool(data & 0x01);
152 	m_clr_lum = (data >> 1) & 0x07;
153 	m_mpx_bkeff = bool(data & 0x10);
154 	m_shp = (data >> 5) & 0x07;
155 
156 //  popmessage("nav: 0x%02X", data);
157 }
158 
159 
video_start()160 void laserbat_state_base::video_start()
161 {
162 	// we render straight from ROM
163 	m_gfx1 = memregion("gfx1")->base();
164 	m_gfx2 = memregion("gfx2")->base();
165 
166 	// start rendering scanlines
167 	m_screen->register_screen_bitmap(m_bitmap);
168 	m_scanline_timer->adjust(m_screen->time_until_pos(1, 0));
169 }
170 
171 
screen_update_laserbat(screen_device & screen,bitmap_ind16 & bitmap,const rectangle & cliprect)172 uint32_t laserbat_state_base::screen_update_laserbat(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect)
173 {
174 	bool const flip_y = flip_screen_y(), flip_x = flip_screen_x();
175 	int const offs_y = m_screen->visible_area().max_y + m_screen->visible_area().min_y;
176 	int const offs_x = m_screen->visible_area().max_x + m_screen->visible_area().min_x;
177 
178 	for (int y = cliprect.min_y; cliprect.max_y >= y; y++)
179 	{
180 		uint16_t const *const src = &m_bitmap.pix(flip_y ? (offs_y - y) : y);
181 		uint16_t *dst = &bitmap.pix(y);
182 		for (int x = cliprect.min_x; cliprect.max_x >= x; x++)
183 		{
184 			dst[x] = uint16_t(m_gfxmix->read(src[flip_x ? (offs_x - x) : x]));
185 		}
186 	}
187 
188 	return 0;
189 }
190 
191 
TIMER_CALLBACK_MEMBER(laserbat_state_base::video_line)192 TIMER_CALLBACK_MEMBER(laserbat_state_base::video_line)
193 {
194 	/*
195 	    +-----+---------+----------------------------------+-------------------------------------+
196 	    | bit |  name   | laserbat/lazarian                | catnmous                            |
197 	    +-----+---------+----------------------------------+-------------------------------------+
198 	    |  0  | NAV0    | sprite bit 0                     | sprite bit 0                        |
199 	    |  1  | NAV1    | sprite bit 1                     | sprite bit 1                        |
200 	    |  2  | CLR0    | sprite palette bit 0             | sprite palette bit 0                |
201 	    |  3  | CLR1    | sprite palette bit 1             | sprite palette bit 1                |
202 	    |  4  | LUM     | sprite luminance                 | background tilemap palette control  |
203 	    |  5  | C1*     | combined PVI red (active low)    | combined PVI red (active low)       |
204 	    |  6  | C2*     | combined PVI green (active low)  | combined PVI green (active low)     |
205 	    |  7  | C3*     | combined PVI blue (active low)   | combined PVI blue (active low)      |
206 	    |  8  | BKR     | background tilemap red           | background tilemap bit 0            |
207 	    |  9  | BKG     | background tilemap green         | background tilemap bit 1            |
208 	    | 10  | BKB     | background tilemap blue          | background tilemap bit 2            |
209 	    | 11  | SHELL   | shell point                      | shell point                         |
210 	    | 12  | EFF1    | effect 1 area                    | background tilemap palette control  |
211 	    | 13  | EFF2    | effect 2 area                    | unused                              |
212 	    | 14  | COLEFF0 | area effect colour bit 0         | background tilemap palette control  |
213 	    | 15  | COLEFF1 | area effect colour bit 1         | background tilemap palette control  |
214 	    +-----+---------+----------------------------------+-------------------------------------+
215 	*/
216 
217 	assert(m_bitmap.width() > m_screen->visible_area().max_x);
218 	assert(m_bitmap.height() > m_screen->visible_area().max_y);
219 
220 	// prep some useful values
221 	int const y = m_screen->vpos();
222 	int const min_x = m_screen->visible_area().min_x;
223 	int const max_x = m_screen->visible_area().max_x;
224 	int const x_offset = min_x - (8 * 3);
225 	int const y_offset = m_screen->visible_area().min_y - 8;
226 	uint16_t *const row = &m_bitmap.pix(y);
227 
228 	// wait for next scanline
229 	m_scanline_timer->adjust(m_screen->time_until_pos(y + 1, 0));
230 
231 	// update the PVIs
232 	if (!y)
233 	{
234 		m_pvi[0]->render_first_line();
235 		m_pvi[1]->render_first_line();
236 		m_pvi[2]->render_first_line();
237 	}
238 	else
239 	{
240 		m_pvi[0]->render_next_line();
241 		m_pvi[1]->render_next_line();
242 		m_pvi[2]->render_next_line();
243 	}
244 	uint16_t const *const pvi1_row = &m_pvi[0]->bitmap().pix(y);
245 	uint16_t const *const pvi2_row = &m_pvi[1]->bitmap().pix(y);
246 	uint16_t const *const pvi3_row = &m_pvi[2]->bitmap().pix(y);
247 
248 	// don't draw outside the visible area
249 	m_bitmap.plot_box(0, y, m_bitmap.width(), 1, 0);
250 	if ((m_screen->visible_area().min_y > y) || (m_screen->visible_area().max_y < y))
251 		return;
252 
253 	// render static effect bits
254 	uint16_t const static_bits = ((uint16_t(m_coleff) << 14) & 0xc000) | ((uint16_t(m_clr_lum) << 2) & 0x001c);
255 	m_bitmap.plot_box(min_x, y, max_x - min_x + 1, 1, static_bits);
256 
257 	// render the TTL-generated background tilemap
258 	unsigned const bg_row = (y - y_offset) & 0x07;
259 	uint8_t const *const bg_src = &m_bg_ram[((y - y_offset) << 2) & 0x3e0];
260 	for (unsigned byte = 0, px = x_offset + (9 * 3); max_x >= px; byte++)
261 	{
262 		uint16_t const tile = (uint16_t(bg_src[byte & 0x1f]) << 3) & 0x7f8;
263 		uint8_t red   = m_gfx1[0x0000 | tile | bg_row];
264 		uint8_t green = m_gfx1[0x0800 | tile | bg_row];
265 		uint8_t blue  = m_gfx1[0x1000 | tile | bg_row];
266 		for (unsigned pixel = 0; 8 > pixel; pixel++, red <<= 1, green <<= 1, blue <<= 1)
267 		{
268 			uint16_t const bg = ((red & 0x80) ? 0x0100 : 0x0000) | ((green & 0x80) ? 0x0200 : 0x0000) | ((blue & 0x80) ? 0x0400 : 0x0000);
269 			if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
270 			px++;
271 			if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
272 			px++;
273 			if ((min_x <= px) && (max_x >= px)) row[px] |= bg;
274 			px++;
275 		}
276 	}
277 
278 	// render shell/effect graphics
279 	uint8_t const eff1_val = m_eff_ram[((y - y_offset) & 0xff) | 0x100];
280 	uint8_t const eff2_val = m_eff_ram[((y - y_offset) & 0xff) | 0x000];
281 	for (int x = 0, px = x_offset; max_x >= px; x++)
282 	{
283 		// calculate area effects
284 		// I have no idea where the magical x offset comes from but it's necessary
285 		bool const right_half = bool((x + 0) & 0x80);
286 		bool const eff1_cmp = right_half ? (uint8_t((x + 0) & 0x7f) < (eff1_val & 0x7f)) : (uint8_t((x + 0) & 0x7f) > (~eff1_val & 0x7f));
287 		bool const eff2_cmp = right_half ? (uint8_t((x + 0) & 0x7f) < (eff2_val & 0x7f)) : (uint8_t((x + 0) & 0x7f) > (~eff2_val & 0x7f));
288 		bool const eff1 = m_abeff1 && (m_neg1 ? !eff1_cmp : eff1_cmp);
289 		bool const eff2 = m_abeff2 && (m_neg2 ? !eff2_cmp : eff2_cmp) && m_mpx_eff2_sh;
290 
291 		// calculate shell point effect
292 		// using the same magical offset as the area effects
293 		bool const shell = m_abeff2 && (uint8_t((x + 0) & 0xff) == (eff2_val & 0xff)) && !m_mpx_eff2_sh;
294 
295 		// set effect bits, and mix in PVI graphics while we're here
296 		uint16_t const effect_bits = (shell ? 0x0800 : 0x0000) | (eff1 ? 0x1000 : 0x0000) | (eff2 ? 0x2000 : 0x0000);
297 		uint16_t pvi_bits = ~(pvi1_row[px] | pvi2_row[px] | pvi3_row[px]);
298 		pvi_bits = ((pvi_bits & 0x01) << 7) | ((pvi_bits & 0x02) << 5) | ((pvi_bits & 0x04) << 3);
299 		if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
300 		px++;
301 		if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
302 		px++;
303 		if ((min_x <= px) && (max_x >= px)) row[px] |= effect_bits | pvi_bits;
304 		px++;
305 	}
306 
307 	// render the TTL-generated sprite
308 	// more magic offsets here I don't understand the source of
309 	if (m_nave)
310 	{
311 		int const sprite_row = y + y_offset - ((256 - m_wcov) & 0x0ff);
312 		if ((0 <= sprite_row) && (32 > sprite_row))
313 		{
314 			for (unsigned byte = 0, x = x_offset + (3 * ((256 - m_wcoh + 5) & 0x0ff)); 8 > byte; byte++)
315 			{
316 				uint8_t bits = m_gfx2[((m_shp << 8) & 0x700) | ((sprite_row << 3) & 0x0f8) | (byte & 0x07)];
317 				for (unsigned pixel = 0; 4 > pixel; pixel++, bits <<= 2)
318 				{
319 					if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
320 					if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
321 					if (max_x >= x) row[x++] |= (bits >> 6) & 0x03;
322 				}
323 			}
324 		}
325 	}
326 }
327 
328 
laserbat_palette(palette_device & palette) const329 void laserbat_state::laserbat_palette(palette_device &palette) const
330 {
331 	/*
332 	    Uses GRBGRBGR pixel format.  The two topmost bist are the LSBs
333 	    for red and green.  LSB for blue is always effectively 1.  The
334 	    middle group is the MSB.  Yet another crazy thing they did.
335 
336 	    Each colour channel has an emitter follower buffer amlpifier
337 	    biased with a 1k resistor to +5V and a 3k3 resistor to ground.
338 	    Output is adjusted by connecting additional resistors across the
339 	    leg to ground using an open collector buffer - 270R, 820R and
340 	    1k0 for unset MSB to LSB, respectively (blue has no LSB so it
341 	    has no 1k0 resistor).
342 
343 	    Assuming 0.7V drop across the emitter follower and no drop
344 	    across the open collector buffer, these are the approximate
345 	    output voltages:
346 
347 	    0.0000, 0.1031, 0.1324, 0.2987, 0.7194, 1.2821, 1.4711, 3.1372
348 
349 	    The game never sets the colour to any value above 4, effectively
350 	    treating it as 5-level red and green, and 3-level blue, for a
351 	    total of 75 usable colours.
352 
353 	    From the fact that there's no DC offset on red and green, and
354 	    the highest value used is just over 0.7V, I'm guessing the game
355 	    expects to drive a standard 0.7V RGB monitor, and higher colour
356 	    values would simply saturate the input.  To make it not look
357 	    like the inside of a coal mine, I've applied gamma decoding at
358 	    2.2
359 
360 	    However there's that nasty DC offset on the blue caused by the
361 	    fact that it has no LSB, but it's eliminated at the AC-coupling
362 	    of the input and output of the buffer amplifier on the monitor
363 	    interface board.  I'm treating it as though it has the same gain
364 	    as the other channels.  After gamma adjustment, medium red and
365 	    medium blue as used by the game have almost the same intensity.
366 	*/
367 
368 	int const weights[] = { 0, 107, 120, 173, 255, 255, 255, 255 };
369 	int const blue_weights[] = { 0, 0, 60, 121, 241, 255, 255, 255 };
370 	for (int entry = 0; palette.entries() > entry; entry++)
371 	{
372 		uint8_t const bits(entry & 0xff);
373 		uint8_t const r(((bits & 0x01) << 1) | ((bits & 0x08) >> 1) | ((bits & 0x40) >> 6));
374 		uint8_t const g(((bits & 0x02) >> 0) | ((bits & 0x10) >> 2) | ((bits & 0x80) >> 7));
375 		uint8_t const b(((bits & 0x04) >> 1) | ((bits & 0x20) >> 3) | 0x01);
376 		palette.set_pen_color(entry, rgb_t(weights[r], weights[g], blue_weights[b]));
377 	}
378 }
379 
380 
catnmous_palette(palette_device & palette) const381 void catnmous_state::catnmous_palette(palette_device &palette) const
382 {
383 	/*
384 	    Uses GRBGRBGR pixel format.  The two topmost bist are the LSBs
385 	    for red and green.  The middle group is the MSB.  Yet another
386 	    crazy thing they did.
387 
388 	    Each colour channel has an emitter follower buffer amlpifier
389 	    biased with a 1k resistor to +5V and a 3k3 resistor to ground.
390 	    Output is adjusted by connecting additional resistors across the
391 	    leg to ground using an open collector buffer.  Red and green use
392 	    560R, 820R and 1k0 for unset MSB to LSB, respectively.  Blue
393 	    uses 47R and 820R on the PCB we have a photo of, although the
394 	    47R resistor looks like it could be a bad repair (opposite
395 	    orientation and burn marks on PCB).
396 
397 	    Assuming 0.7V drop across the emitter follower and no drop
398 	    across the open collector buffer, these are the approximate
399 	    output voltages for red and green:
400 
401 	    0.2419, 0.4606, 0.5229, 0.7194, 0.9188, 1.2821, 1.4711, 3.1372
402 
403 	    The game uses all colour values except 4.  The DC offset will be
404 	    eliminated by the AC coupling on the monitor interface board.
405 	    The differences steps aren't very linear, they vary from 0.06V
406 	    to 0.36V with no particular order.  The input would be expected
407 	    to saturate somewhere inside the big jump to the highest level.
408 
409 	    Let's assume the 47R resistor is a bad repair and it's supposed
410 	    to be 470R.  That gives us these output voltages for blue:
411 
412 	    0.3752, 0.7574, 1.2821, 3.1372
413 
414 	    To make life easier, I'll assume the monitor is expected to have
415 	    half the gain of a standard monitor and no gamma decoding is
416 	    necessary.
417 	*/
418 
419 	int const weights[] = { 0, 40, 51, 87, 123, 189, 224, 255 };
420 	int const blue_weights[] = { 0, 70, 165, 255 };
421 	for (int entry = 0; palette.entries() > entry; entry++)
422 	{
423 		uint8_t const bits(entry & 0xff);
424 		uint8_t const r(((bits & 0x01) << 1) | ((bits & 0x08) >> 1) | ((bits & 0x40) >> 6));
425 		uint8_t const g(((bits & 0x02) >> 0) | ((bits & 0x10) >> 2) | ((bits & 0x80) >> 7));
426 		uint8_t const b(((bits & 0x04) >> 2) | ((bits & 0x20) >> 4));
427 		palette.set_pen_color(entry, rgb_t(weights[r], weights[g], blue_weights[b]));
428 	}
429 }
430