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