-
Notifications
You must be signed in to change notification settings - Fork 4
/
clem_vgc.c
326 lines (294 loc) · 11.7 KB
/
clem_vgc.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#include "clem_vgc.h"
#include "clem_debug.h"
#include "clem_mmio_defs.h"
#include "clem_shared.h"
#include "clem_util.h"
#include <inttypes.h>
/* References:
Vertical/Horizontal Counters and general VBL timings
http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.039.html
VBL particulars:
http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html
*/
static inline void _clem_vgc_set_scanline_int(struct ClemensVGC *vgc, bool enable) {
if (enable) {
vgc->irq_line |= CLEM_IRQ_VGC_SCAN_LINE;
} else {
vgc->irq_line &= ~CLEM_IRQ_VGC_SCAN_LINE;
}
}
static inline unsigned _clem_vgc_calc_h_counter(clem_clocks_duration_t duration,
clem_clocks_duration_t ref_step) {
// TODO: this uses avg ns per cycle over a scanline vs 0-63 = 978 and 64 = 1108ns
// if this is ever needed, a reminder here.
// TODO: Look at CLEM_VGC_SCANLINE_CLOCKS_DURATION and see if we can use that somehow instead
//
return (duration / clem_calc_clocks_step_from_ns(CLEM_PHI0_CYCLE_NS)) & 0x7f; /* 7 bits */
}
static bool _clem_vgc_is_scanline_int_enabled(const uint8_t *mega2_e1, unsigned v_counter) {
if (v_counter >= CLEM_VGC_FIRST_VISIBLE_SCANLINE_CNTR) {
v_counter -= CLEM_VGC_FIRST_VISIBLE_SCANLINE_CNTR;
if (v_counter < CLEM_VGC_SHGR_SCANLINE_COUNT) {
return (mega2_e1[0x9d00 + v_counter] & CLEM_VGC_SCANLINE_CONTROL_INTERRUPT) != 0;
}
}
return false;
}
static void _clem_vgc_scanline_build_rgb_palette(struct ClemensVGC *vgc, unsigned v_counter,
const uint8_t *mega2_e1) {
uint16_t *rgb_dest;
const uint8_t *rgb_src;
unsigned palette, i;
if (v_counter >= CLEM_VGC_FIRST_VISIBLE_SCANLINE_CNTR) {
v_counter -= CLEM_VGC_FIRST_VISIBLE_SCANLINE_CNTR;
if (v_counter >= CLEM_VGC_SHGR_SCANLINE_COUNT) {
return;
}
}
palette = mega2_e1[0x9d00 + v_counter] & CLEM_VGC_SCANLINE_PALETTE_INDEX_MASK;
rgb_src = &mega2_e1[0x9e00 + palette * 32];
rgb_dest = &vgc->shgr_palettes[16 * v_counter];
for (i = 0; i < 16; ++i, rgb_src += 2, rgb_dest++) {
*rgb_dest = ((uint16_t)(rgb_src[1]) << 8) | rgb_src[0];
}
}
void clem_vgc_reset_scanlines(struct ClemensVGC *vgc) {
/* setup scanline maps for all of the different modes */
struct ClemensScanline *line;
unsigned offset;
unsigned row, inner;
/* text page 1 $0400-$07FF, page 2 = $0800-$0BFF
rows (0, 8, 16), (1, 9, 17), (2, 10, 18), etc.. increment by 40, for
every row in the tuple. When advancing to next tuple of rows, account
for 8 byte 'hole' (so row 0 = +0, row 1 = +128, row 2 = +256)
*/
line = &vgc->text_1_scanlines[0];
offset = 0x400;
for (row = 0; row < 8; ++row, ++line) {
line[0].offset = offset;
line[0].control = 0;
line[8].offset = offset + 40;
line[8].control = 0;
line[16].offset = offset + 80;
line[16].control = 0;
offset += 128;
}
line = &vgc->text_2_scanlines[0];
for (row = 0; row < 8; ++row, ++line) {
line[0].offset = offset;
line[0].control = 0;
line[8].offset = offset + 40;
line[8].control = 0;
line[16].offset = offset + 80;
line[16].control = 0;
offset += 128;
}
/* hgr page 1 $2000-$3FFF, page 2 = $4000-$5FFF
line text but each "row" is 8 pixels high and offsets increment by 1024
of course, each byte is 7 pixels + 1 palette bit, which keeps the
familiar +40, +80 increments used in text scanline calculation.
*/
line = &vgc->hgr_1_scanlines[0];
offset = 0x2000;
for (row = 0; row < 8; ++row) {
line[0 + row * 8].offset = offset + row * 128;
line[0 + row * 8].control = 0;
line[64 + row * 8].offset = (offset + 0x28) + row * 128;
line[64 + row * 8].control = 0;
line[128 + row * 8].offset = (offset + 0x50) + row * 128;
line[128 + row * 8].control = 0;
}
for (row = 0; row < 24; ++row) {
for (inner = 1; inner < 8; ++inner) {
line[row * 8 + inner].offset = line[row * 8 + (inner - 1)].offset + 0x400;
line[row * 8 + inner].control = 0;
}
}
line = &vgc->hgr_2_scanlines[0];
offset = 0x4000;
for (row = 0; row < 8; ++row) {
line[0 + row * 8].offset = offset + row * 128;
line[0 + row * 8].control = 0;
line[64 + row * 8].offset = (offset + 0x28) + row * 128;
line[64 + row * 8].control = 0;
line[128 + row * 8].offset = (offset + 0x50) + row * 128;
line[128 + row * 8].control = 0;
}
for (row = 0; row < 24; ++row) {
for (inner = 1; inner < 8; ++inner) {
line[row * 8 + inner].offset = line[row * 8 + (inner - 1)].offset + 0x400;
line[row * 8 + inner].control = 0;
}
}
// linear mapping (otherwise is not supported on the IIgs I believe)
line = &vgc->shgr_scanlines[0];
offset = 0x2000;
for (row = 0; row < CLEM_VGC_SHGR_SCANLINE_COUNT; ++row, ++line) {
line->offset = offset;
line->control = 0;
offset += 160;
}
}
void clem_vgc_reset(struct ClemensVGC *vgc) {
unsigned row;
vgc->mode_flags = CLEM_VGC_INIT;
vgc->text_fg_color = CLEM_VGC_COLOR_WHITE;
vgc->text_bg_color = CLEM_VGC_COLOR_MEDIUM_BLUE;
vgc->scanline_irq_enable = false;
vgc->irq_vbl = false;
vgc->irq_line = 0;
vgc->vbl_counter = 0;
for (row = 0; row < CLEM_VGC_SHGR_SCANLINE_COUNT * 16; ++row) {
vgc->shgr_palettes[row] = 0x0000;
}
clem_vgc_reset_scanlines(vgc);
}
void clem_vgc_set_mode(struct ClemensVGC *vgc, unsigned mode_flags) {
if (mode_flags & CLEM_VGC_RESOLUTION_MASK) {
clem_vgc_clear_mode(vgc, CLEM_VGC_RESOLUTION_MASK);
}
vgc->mode_flags |= mode_flags;
}
void clem_vgc_clear_mode(struct ClemensVGC *vgc, unsigned mode_flags) {
vgc->mode_flags &= ~mode_flags;
if (mode_flags & CLEM_VGC_ENABLE_VBL_IRQ) {
vgc->irq_line &= ~CLEM_IRQ_VGC_BLANK;
}
}
void clem_vgc_set_text_colors(struct ClemensVGC *vgc, unsigned fg_color, unsigned bg_color) {
vgc->text_fg_color = fg_color;
vgc->text_bg_color = bg_color;
}
void clem_vgc_set_region(struct ClemensVGC *vgc, uint8_t c02b_value) {
unsigned last_mode_flags = vgc->mode_flags;
if (c02b_value & 0x08) {
clem_vgc_set_mode(vgc, CLEM_VGC_LANGUAGE);
} else {
clem_vgc_clear_mode(vgc, CLEM_VGC_LANGUAGE);
}
if (c02b_value & 0x10) {
clem_vgc_set_mode(vgc, CLEM_VGC_PAL);
} else {
clem_vgc_clear_mode(vgc, CLEM_VGC_PAL);
}
if ((last_mode_flags ^ vgc->mode_flags) & CLEM_VGC_PAL) {
// since we use vertical blanks as a time counter vs absolute clocks, need
// to reset the counter when changing the refresh rate - not the best
// solution but given how infrequent this use-case might be, this
// oddity shoudn't have any practical effect
vgc->vbl_counter = 0;
}
vgc->text_language = (c02b_value & 0xe0) >> 5;
}
uint8_t clem_vgc_get_region(struct ClemensVGC *vgc) {
uint8_t result = (vgc->mode_flags & CLEM_VGC_LANGUAGE) ? 0x08 : 0x00;
if (vgc->mode_flags & CLEM_VGC_PAL)
result |= 0x10;
result |= (uint8_t)((vgc->text_language << 5) & 0xe0);
return result;
}
void clem_vgc_scanline_enable_int(struct ClemensVGC *vgc, bool enable) {
vgc->scanline_irq_enable = enable;
if (!enable) {
_clem_vgc_set_scanline_int(vgc, false);
}
}
#define CLEM_VGC_HORIZ_SCAN_DURATION(_ref_step_) \
(clem_calc_clocks_step_from_ns(CLEM_VGC_HORIZ_SCAN_TIME_NS))
#define CLEM_VGC_NTSC_FRAMES_PER_SECOND (1e6f / (CLEM_VGC_NTSC_SCAN_TIME_NS / 1000.0f))
void clem_vgc_sync(struct ClemensVGC *vgc, struct ClemensClock *clock, const uint8_t *mega2_bank0,
const uint8_t *mega2_bank1) {
/* TODO: VBL detection depends on NTSC/PAL switch
@ specific time within a scan until end of scan
Calculate v_counter and h_counter on demand given:
NTSC/PAL mode
Timer/cycle
Trigger IRQ (vgc->irq_line) if inside VBL region
And of course, super hi-res
*/
clem_clocks_duration_t scanline_duration = CLEM_VGC_HORIZ_SCAN_DURATION(clock->ref_step);
unsigned scanline_limit = CLEM_VGC_NTSC_SCANLINE_COUNT;
unsigned delta_tmp;
if (vgc->mode_flags & CLEM_VGC_INIT) {
vgc->ts_last_frame = clock->ts;
vgc->dt_scanline = 0;
vgc->mode_flags &= ~CLEM_VGC_INIT;
} else {
vgc->dt_scanline += (clock->ts - vgc->ts_last_frame);
while (vgc->dt_scanline >= scanline_duration) {
if (vgc->scanline_irq_enable && (vgc->mode_flags & CLEM_VGC_SUPER_HIRES)) {
if (_clem_vgc_is_scanline_int_enabled(mega2_bank1, vgc->v_counter)) {
_clem_vgc_set_scanline_int(vgc, true);
}
}
_clem_vgc_scanline_build_rgb_palette(vgc, vgc->v_counter, mega2_bank1);
vgc->dt_scanline -= scanline_duration;
vgc->v_counter++;
}
// TODO: vbl only once per VBL frame?
if (vgc->v_counter >= CLEM_VGC_VBL_NTSC_LOWER_BOUND && !vgc->vbl_started) {
if (vgc->mode_flags & CLEM_VGC_ENABLE_VBL_IRQ) {
vgc->irq_line |= CLEM_IRQ_VGC_BLANK;
vgc->irq_vbl = true;
}
vgc->vbl_started = true;
}
if (vgc->v_counter >= scanline_limit) {
vgc->v_counter -= scanline_limit;
vgc->vbl_started = false;
vgc->vbl_counter++;
}
}
vgc->ts_last_frame = clock->ts;
}
void clem_vgc_calc_counters(struct ClemensVGC *vgc, struct ClemensClock *clock, unsigned *v_counter,
unsigned *h_counter) {
/* 65 cycles per horizontal scanline, 978 ns per horizontal count = 63.57us
and include wraparound since this is called during a read from I/O register switch and some
time might have passed before the last call to sync(). */
clem_clocks_duration_t scanline_duration = vgc->dt_scanline + (clock->ts - vgc->ts_last_frame);
unsigned v_counter_delta = scanline_duration / CLEM_VGC_HORIZ_SCAN_DURATION(clock->ref_step);
scanline_duration -= v_counter_delta * CLEM_VGC_HORIZ_SCAN_DURATION(clock->ref_step);
*v_counter = vgc->v_counter + v_counter_delta;
*h_counter = _clem_vgc_calc_h_counter(scanline_duration, clock->ref_step);
}
uint8_t clem_vgc_read_switch(struct ClemensVGC *vgc, struct ClemensClock *clock, uint8_t ioreg,
uint8_t flags) {
uint8_t result = 0x00;
unsigned v_counter;
unsigned h_counter;
clem_vgc_calc_counters(vgc, clock, &v_counter, &h_counter);
switch (ioreg) {
case CLEM_MMIO_REG_VBLBAR:
/* IIgs sets bit 7 if scanline is within vertical blank region */
if (v_counter >= CLEM_VGC_VBL_NTSC_LOWER_BOUND) {
result |= 0x80;
}
break;
case CLEM_MMIO_REG_VGC_VERTCNT:
result = (uint8_t)(((v_counter + 0xFA) >> 1) & 0xff);
break;
case CLEM_MMIO_REG_VGC_HORIZCNT:
if (h_counter < 1) {
result = 0x00;
} else {
result = 0x3F + h_counter;
}
result |= ((v_counter + 0xFA) & 1) << 7;
break;
}
return result;
}
void clem_vgc_write_switch(struct ClemensVGC *vgc, struct ClemensClock *clock, uint8_t ioreg,
uint8_t value) {
switch (ioreg) {
case CLEM_MMIO_REG_RTC_VGC_SCANINT:
if (!(value & 0x20)) {
_clem_vgc_set_scanline_int(vgc, false);
}
break;
default:
CLEM_UNIMPLEMENTED("vgc: write %02x : %02x ", ioreg, value);
break;
}
}