1//! Sega CD's graphics ASIC, which can perform hardware-accelerated image scaling and rotation
2
3mod fixedpoint;
4
5use crate::graphics::fixedpoint::FixedPointDecimal;
6use crate::wordram;
7use crate::wordram::{Nibble, WordRam};
8use bincode::{Decode, Encode};
9use jgenesis_common::num::GetBit;
10use std::array;
11
12const SUB_CPU_DIVIDER: u32 = crate::api::DEFAULT_SUB_CPU_DIVIDER as u32;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
15enum StampSizeDots {
16    #[default]
17    Sixteen,
18    ThirtyTwo,
19}
20
21impl StampSizeDots {
22    fn to_bit(self) -> bool {
23        self == Self::ThirtyTwo
24    }
25
26    fn from_bit(bit: bool) -> Self {
27        if bit { Self::ThirtyTwo } else { Self::Sixteen }
28    }
29
30    fn one_dimension_in_pixels(self) -> u32 {
31        match self {
32            Self::Sixteen => 16,
33            Self::ThirtyTwo => 32,
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
39enum StampMapSizeScreens {
40    #[default]
41    One,
42    Sixteen,
43}
44
45impl StampMapSizeScreens {
46    fn to_bit(self) -> bool {
47        self == Self::Sixteen
48    }
49
50    fn from_bit(bit: bool) -> Self {
51        if bit { Self::Sixteen } else { Self::One }
52    }
53
54    fn one_dimension_in_pixels(self) -> u32 {
55        // One "screen" is 256x256 pixels
56        match self {
57            Self::One => 256,
58            Self::Sixteen => 4096,
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum StampRotation {
65    Zero,
66    Ninety,
67    OneEighty,
68    TwoSeventy,
69}
70
71#[derive(Debug, Clone, Copy)]
72struct StampData {
73    stamp_number: u16,
74    rotation: StampRotation,
75    horizontal_flip: bool,
76}
77
78impl StampData {
79    fn from_word(word: u16) -> Self {
80        let horizontal_flip = word.bit(15);
81        let rotation = match word & 0x6000 {
82            0x0000 => StampRotation::Zero,
83            0x2000 => StampRotation::Ninety,
84            0x4000 => StampRotation::OneEighty,
85            0x6000 => StampRotation::TwoSeventy,
86            _ => unreachable!("value & 0x6000 is always 0x0000/0x2000/0x4000/0x6000"),
87        };
88        let stamp_number = word & 0x07FF;
89
90        Self { stamp_number, rotation, horizontal_flip }
91    }
92}
93
94#[derive(Debug, Clone)]
95struct TraceVectorData {
96    start_x: FixedPointDecimal,
97    start_y: FixedPointDecimal,
98    delta_x: FixedPointDecimal,
99    delta_y: FixedPointDecimal,
100}
101
102impl TraceVectorData {
103    fn from_bytes(bytes: [u8; 8]) -> Self {
104        let start_x_word = u16::from_be_bytes([bytes[0], bytes[1]]);
105        let start_y_word = u16::from_be_bytes([bytes[2], bytes[3]]);
106        let delta_x_word = u16::from_be_bytes([bytes[4], bytes[5]]);
107        let delta_y_word = u16::from_be_bytes([bytes[6], bytes[7]]);
108
109        Self {
110            start_x: FixedPointDecimal::from_position(start_x_word),
111            start_y: FixedPointDecimal::from_position(start_y_word),
112            delta_x: FixedPointDecimal::from_delta(delta_x_word),
113            delta_y: FixedPointDecimal::from_delta(delta_y_word),
114        }
115    }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
119enum State {
120    Idle,
121    Processing { mclk_cycles_remaining: u64, operation_performed: bool },
122}
123
124#[derive(Debug, Clone, Encode, Decode)]
125pub struct GraphicsCoprocessor {
126    stamp_size: StampSizeDots,
127    stamp_map_size: StampMapSizeScreens,
128    stamp_map_repeats: bool,
129    stamp_map_base_address: u32,
130    image_buffer_v_cell_size: u32,
131    image_buffer_start_address: u32,
132    image_buffer_v_offset: u32,
133    image_buffer_h_offset: u32,
134    image_buffer_v_dot_size: u32,
135    image_buffer_h_dot_size: u32,
136    trace_vector_base_address: u32,
137    state: State,
138    interrupt_pending: bool,
139}
140
141impl GraphicsCoprocessor {
142    pub fn new() -> Self {
143        Self {
144            stamp_size: StampSizeDots::default(),
145            stamp_map_size: StampMapSizeScreens::default(),
146            stamp_map_repeats: false,
147            stamp_map_base_address: 0,
148            image_buffer_v_cell_size: 1,
149            image_buffer_start_address: 0,
150            image_buffer_v_offset: 0,
151            image_buffer_h_offset: 0,
152            image_buffer_v_dot_size: 0,
153            image_buffer_h_dot_size: 0,
154            trace_vector_base_address: 0,
155            state: State::Idle,
156            interrupt_pending: false,
157        }
158    }
159
160    pub fn read_register(&self, address: u32) -> u16 {
161        match address & 0x1FF {
162            0x058 | 0x059 => {
163                // Stamp data size
164                let in_progress = matches!(self.state, State::Processing { .. });
165
166                (u16::from(in_progress) << 15)
167                    | (u16::from(self.stamp_map_size.to_bit()) << 2)
168                    | (u16::from(self.stamp_size.to_bit()) << 1)
169                    | u16::from(self.stamp_map_repeats)
170            }
171            0x05A | 0x05B => {
172                // Stamp map base address
173                (self.stamp_map_base_address >> 2) as u16
174            }
175            0x05C | 0x05D => {
176                // Image buffer V cell size (minus one)
177                (self.image_buffer_v_cell_size - 1) as u16
178            }
179            0x05E | 0x05F => {
180                // Image buffer start address
181                (self.image_buffer_start_address >> 2) as u16
182            }
183            0x060 | 0x061 => {
184                // Image buffer offset
185                (((self.image_buffer_v_offset) << 3) | self.image_buffer_h_offset) as u16
186            }
187            0x062 | 0x063 => {
188                // Image buffer H dot size
189                self.image_buffer_h_dot_size as u16
190            }
191            0x064 | 0x065 => {
192                // Image buffer V dot size
193                self.image_buffer_v_dot_size as u16
194            }
195            _ => 0,
196        }
197    }
198
199    #[allow(clippy::match_same_arms)]
200    pub fn write_register_byte(&mut self, address: u32, value: u8) {
201        match address & 0x1FF {
202            0x059 => {
203                // Stamp data size
204                self.stamp_map_size = StampMapSizeScreens::from_bit(value.bit(2));
205                self.stamp_size = StampSizeDots::from_bit(value.bit(1));
206                self.stamp_map_repeats = value.bit(0);
207            }
208            0x05A..=0x05B => {
209                // Stamp map base address (word access only)
210                self.write_register_word(address & !1, u16::from_le_bytes([value, value]));
211            }
212            0x05D => {
213                // Image buffer V cell size (minus one)
214                self.image_buffer_v_cell_size = ((value & 0x1F) + 1).into();
215            }
216            0x05E..=0x05F => {
217                // Image buffer start address (word access only)
218                self.write_register_word(address & !1, u16::from_le_bytes([value, value]));
219            }
220            0x061 => {
221                // Image buffer offset
222                self.image_buffer_v_offset = u32::from(value >> 3) & 0x07;
223                self.image_buffer_h_offset = (value & 0x07).into();
224            }
225            0x062..=0x063 => {
226                // Image buffer H dot size (word access only)
227                self.write_register_word(address & !1, u16::from_le_bytes([value, value]));
228            }
229            0x064..=0x065 => {
230                // Image buffer V dot size (word access only)
231                self.write_register_word(address & !1, u16::from_le_bytes([value, value]));
232            }
233            0x066..=0x067 => {
234                // Trace vector base address (word access only)
235                self.write_register_word(address & !1, u16::from_le_bytes([value, value]));
236            }
237            _ => {}
238        }
239    }
240
241    #[allow(clippy::match_same_arms)]
242    pub fn write_register_word(&mut self, address: u32, value: u16) {
243        match address & 0x1FF {
244            0x058 => {
245                // Stamp data size (only low byte is writable)
246                self.write_register_byte(address | 1, value as u8);
247            }
248            0x05A => {
249                // Stamp map base address (bits 17-7)
250                self.stamp_map_base_address = u32::from(value & 0xFFE0) << 2;
251            }
252            0x05C => {
253                // Image buffer V cell size (only low byte is writable)
254                self.write_register_byte(address | 1, value as u8);
255            }
256            0x05E => {
257                // Image buffer start address (bits 17-5)
258                self.image_buffer_start_address = u32::from(value & 0xFFF8) << 2;
259            }
260            0x060 => {
261                // Image buffer offset (only low byte is writable)
262                self.write_register_byte(address | 1, value as u8);
263            }
264            0x062 => {
265                // Image buffer H dot size
266                self.image_buffer_h_dot_size = (value & 0x01FF).into();
267            }
268            0x064 => {
269                // Image buffer V dot size
270                self.image_buffer_v_dot_size = (value & 0x00FF).into();
271            }
272            0x066 => {
273                // Trace vector base address / begin graphics operation
274                self.trace_vector_base_address = u32::from(value & 0xFFFE) << 2;
275
276                // Mostly a guess at timing, based on this:
277                // https://gendev.spritesmind.net/forum/viewtopic.php?t=908
278                //
279                // Each word RAM access takes 3 sub CPU cycles / 12 MCLK cycles, and the ASIC has
280                // to perform the following accesses per image buffer line:
281                // - Read trace vector (4 words)
282                // - Read stamp map entry per pixel (1 word * H size)
283                // - Read stamp generator per pixel (1 word * H size)
284                // - Write to the image buffer (1 word * H size / 4)
285                //   - Divide by 4 because there are 4 pixels per image buffer word
286                let h_dot_size = self.image_buffer_h_dot_size;
287                let v_dot_size = self.image_buffer_v_dot_size;
288                let estimated_words_per_line = 4 + 2 * h_dot_size + h_dot_size / 4;
289                let estimated_mclk_cycles =
290                    SUB_CPU_DIVIDER * 3 * v_dot_size * estimated_words_per_line;
291                self.state = State::Processing {
292                    mclk_cycles_remaining: estimated_mclk_cycles.into(),
293                    operation_performed: false,
294                };
295            }
296            _ => {}
297        }
298    }
299
300    pub fn tick(
301        &mut self,
302        mclk_cycles: u64,
303        word_ram: &mut WordRam,
304        graphics_interrupt_enabled: bool,
305    ) {
306        let State::Processing { mclk_cycles_remaining, operation_performed } = self.state else {
307            return;
308        };
309
310        if !operation_performed {
311            self.perform_graphics_operation(word_ram);
312        }
313
314        if mclk_cycles >= mclk_cycles_remaining {
315            log::trace!("Graphics operation completed");
316
317            self.state = State::Idle;
318            // In actual hardware V dot size is decremented as the operation goes; here, we're just
319            // clearing at the end
320            self.image_buffer_v_dot_size = 0;
321
322            if graphics_interrupt_enabled {
323                self.interrupt_pending = true;
324            }
325        } else {
326            self.state = State::Processing {
327                mclk_cycles_remaining: mclk_cycles_remaining - mclk_cycles,
328                operation_performed: true,
329            };
330        }
331    }
332
333    pub fn interrupt_pending(&self) -> bool {
334        self.interrupt_pending
335    }
336
337    pub fn acknowledge_interrupt(&mut self) {
338        self.interrupt_pending = false;
339    }
340
341    fn perform_graphics_operation(&self, word_ram: &mut WordRam) {
342        log::trace!("Beginning graphics operation with current state:\n{self:#X?}");
343
344        let stamp_map_size = self.stamp_map_size;
345        let stamp_map_dimension_pixels = stamp_map_size.one_dimension_in_pixels();
346        let stamp_map_repeats = self.stamp_map_repeats;
347        let stamp_size = self.stamp_size;
348
349        let stamp_map_base_address = self.stamp_map_base_address_masked();
350        let trace_vector_base_address = self.trace_vector_base_address;
351
352        // 8 lines per cell
353        let image_buffer_v_cell_size = self.image_buffer_v_cell_size;
354        let image_buffer_line_size = 8 * image_buffer_v_cell_size;
355        let image_buffer_v_dot_size = self.image_buffer_v_dot_size;
356        let image_buffer_h_dot_size = self.image_buffer_h_dot_size;
357        let image_buffer_h_offset = self.image_buffer_h_offset;
358
359        let mut image_buffer_start_address = self.image_buffer_start_address;
360        let mut image_buffer_line = self.image_buffer_v_offset;
361        for line in 0..image_buffer_v_dot_size {
362            // One trace vector per line
363            let trace_vector_address =
364                (trace_vector_base_address + 8 * line) & wordram::ADDRESS_MASK;
365            let trace_vector = TraceVectorData::from_bytes(array::from_fn(|i| {
366                read_word_ram(word_ram, trace_vector_address + i as u32)
367            }));
368
369            let mut trace_x_position = trace_vector.start_x;
370            let mut trace_y_position = trace_vector.start_y;
371            for dot in 0..image_buffer_h_dot_size {
372                let x = trace_x_position.integer_part();
373                let y = trace_y_position.integer_part();
374                let position_out_of_bounds =
375                    x >= stamp_map_dimension_pixels || y >= stamp_map_dimension_pixels;
376
377                let sample = if !stamp_map_repeats && position_out_of_bounds {
378                    // Sampling outside of a non-repeating stamp map is always 0
379                    0
380                } else {
381                    let stamp_map_addr = compute_stamp_map_address(
382                        stamp_map_base_address,
383                        stamp_size,
384                        stamp_map_size,
385                        x,
386                        y,
387                    );
388                    let stamp = StampData::from_word(u16::from_be_bytes([
389                        read_word_ram(word_ram, stamp_map_addr),
390                        read_word_ram(word_ram, stamp_map_addr + 1),
391                    ]));
392
393                    sample_stamp(word_ram, stamp, stamp_size, x, y)
394                };
395
396                let image_buffer_dot = image_buffer_h_offset + dot;
397                let image_buffer_addr = image_buffer_start_address
398                    + compute_relative_addr_v_then_h(
399                        image_buffer_line_size,
400                        image_buffer_dot,
401                        image_buffer_line,
402                    );
403
404                let nibble = if image_buffer_dot.bit(0) { Nibble::Low } else { Nibble::High };
405                write_word_ram(word_ram, image_buffer_addr, nibble, sample);
406
407                trace_x_position += trace_vector.delta_x;
408                trace_y_position += trace_vector.delta_y;
409            }
410
411            image_buffer_line += 1;
412            if image_buffer_line == image_buffer_line_size {
413                image_buffer_line = 0;
414
415                // "Wrap" by shifting the image buffer start address right 1 cell
416                let image_buffer_size_pixels = image_buffer_line_size * 8;
417                image_buffer_start_address = (image_buffer_start_address
418                    + image_buffer_size_pixels / 2)
419                    & wordram::ADDRESS_MASK;
420            }
421        }
422    }
423
424    fn stamp_map_base_address_masked(&self) -> u32 {
425        use StampMapSizeScreens as Screens;
426        use StampSizeDots as Dots;
427
428        let stamp_map_base_address_mask = match (self.stamp_map_size, self.stamp_size) {
429            (Screens::One, Dots::Sixteen) => {
430                // Bits 17-9
431                0x03FE00
432            }
433            (Screens::One, Dots::ThirtyTwo) => {
434                // Bits 17-7
435                0x03FF80
436            }
437            (Screens::Sixteen, Dots::Sixteen) => {
438                // Bit 17 only
439                0x020000
440            }
441            (Screens::Sixteen, Dots::ThirtyTwo) => {
442                // Bits 17-15
443                0x038000
444            }
445        };
446
447        self.stamp_map_base_address & stamp_map_base_address_mask
448    }
449}
450
451fn read_word_ram(word_ram: &mut WordRam, address: u32) -> u8 {
452    word_ram.sub_cpu_read_ram(wordram::SUB_BASE_ADDRESS | address)
453}
454
455fn write_word_ram(word_ram: &mut WordRam, address: u32, nibble: Nibble, value: u8) {
456    word_ram.graphics_write_ram(wordram::SUB_BASE_ADDRESS | address, nibble, value);
457}
458
459fn compute_stamp_map_address(
460    stamp_map_base_address: u32,
461    stamp_size: StampSizeDots,
462    stamp_map_size: StampMapSizeScreens,
463    x: u32,
464    y: u32,
465) -> u32 {
466    let stamp_dimension_pixels = stamp_size.one_dimension_in_pixels();
467    let stamp_map_dimension_pixels = stamp_map_size.one_dimension_in_pixels();
468
469    let stamp_map_x = (x & (stamp_map_dimension_pixels - 1)) / stamp_dimension_pixels;
470    let stamp_map_y = (y & (stamp_map_dimension_pixels - 1)) / stamp_dimension_pixels;
471
472    // 2 bytes per stamp
473    let stamp_map_relative_addr =
474        2 * (stamp_map_y * stamp_map_dimension_pixels / stamp_dimension_pixels + stamp_map_x);
475    stamp_map_base_address + stamp_map_relative_addr
476}
477
478fn sample_stamp(
479    word_ram: &mut WordRam,
480    stamp: StampData,
481    stamp_size: StampSizeDots,
482    x: u32,
483    y: u32,
484) -> u8 {
485    let stamp_number = match stamp_size {
486        StampSizeDots::Sixteen => stamp.stamp_number,
487        StampSizeDots::ThirtyTwo => {
488            // Lowest 2 bits are ignored in 32x32 stamp mode; treat the remaining bits as a stamp
489            // number for 32x32 tiles (4x the byte size of 16x16 tiles)
490            stamp.stamp_number >> 2
491        }
492    };
493    let stamp_number: u32 = stamp_number.into();
494
495    if stamp_number == 0 {
496        // Sampling stamp 0 always results in 0 regardless of what is in word RAM
497        return 0;
498    }
499
500    let stamp_size_dimension_pixels = stamp_size.one_dimension_in_pixels();
501    let stamp_addr = stamp_number * (stamp_size_dimension_pixels * stamp_size_dimension_pixels / 2);
502
503    let x = x & (stamp_size_dimension_pixels - 1);
504    let y = y & (stamp_size_dimension_pixels - 1);
505
506    let x = if stamp.horizontal_flip {
507        flip_stamp_coordinate(x, stamp_size_dimension_pixels)
508    } else {
509        x
510    };
511    let (x, y) = match stamp.rotation {
512        StampRotation::Zero => (x, y),
513        StampRotation::Ninety => (y, flip_stamp_coordinate(x, stamp_size_dimension_pixels)),
514        StampRotation::OneEighty => (
515            flip_stamp_coordinate(x, stamp_size_dimension_pixels),
516            flip_stamp_coordinate(y, stamp_size_dimension_pixels),
517        ),
518        StampRotation::TwoSeventy => (flip_stamp_coordinate(y, stamp_size_dimension_pixels), x),
519    };
520
521    let sample_addr = stamp_addr
522        + compute_relative_addr_v_then_h(
523            stamp_size_dimension_pixels,
524            x & (stamp_size_dimension_pixels - 1),
525            y & (stamp_size_dimension_pixels - 1),
526        );
527    let byte = read_word_ram(word_ram, sample_addr);
528    if x.bit(0) { byte & 0x0F } else { byte >> 4 }
529}
530
531fn flip_stamp_coordinate(coordinate: u32, stamp_size_dimension_pixels: u32) -> u32 {
532    stamp_size_dimension_pixels - 1 - (coordinate & (stamp_size_dimension_pixels - 1))
533}
534
535fn compute_relative_addr_v_then_h(v_size_pixels: u32, x: u32, y: u32) -> u32 {
536    assert!(y < v_size_pixels);
537
538    let v_size_cells = v_size_pixels / 8;
539
540    let cell_x = x / 8;
541    let cell_y = y / 8;
542    let cell_number = cell_x * v_size_cells + cell_y;
543
544    // 32 bytes per cell
545    let cell_addr = 32 * cell_number;
546
547    // 4 bytes per row
548    let addr_in_cell = 4 * (y & 0x07) + ((x & 0x07) >> 1);
549    cell_addr + addr_in_cell
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn stamp_map_address() {
558        let stamp_size = StampSizeDots::Sixteen;
559        let stamp_map_size = StampMapSizeScreens::Sixteen;
560
561        assert_eq!(0, compute_stamp_map_address(0, stamp_size, stamp_map_size, 0, 0));
562        assert_eq!(0x20000, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 0, 0));
563
564        assert_eq!(0x20000, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 15, 15));
565        assert_eq!(0x20002, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 16, 15));
566        assert_eq!(0x20200, compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 15, 16));
567        assert_eq!(
568            0x3FFFE,
569            compute_stamp_map_address(0x20000, stamp_size, stamp_map_size, 4095, 4095)
570        );
571    }
572}