Skip to main content

cpclib_sprite_compiler/
lib.rs

1#![feature(let_chains)]
2
3use std::collections::{BTreeMap, HashMap};
4use std::ops::{Deref, DerefMut};
5
6use bon::Builder;
7use cpclib_asm::{
8    IfBuilder, Listing, ListingBuilder, ListingExt, ListingFromStr, ListingSelector, Register8,
9    Register16, TestKind, dec_e, dec_l, inc_e, inc_l
10};
11use cpclib_image::convert::{SpriteEncoding, SpriteOutput};
12use itertools::Itertools;
13use smol_str::SmolStr;
14
15/// The action handled by the code
16#[derive(Default)]
17pub enum RoutineAction {
18    #[default]
19    DrawPixelMaskedSprite,
20    SaveBackgroundAndDrawPixelMaskedSprite,
21    RestoreBackground
22}
23
24impl RoutineAction {
25    pub fn save_background(&self) -> bool {
26        matches!(self, RoutineAction::SaveBackgroundAndDrawPixelMaskedSprite)
27    }
28
29    pub fn build_register_store(&self) -> RegistersStore {
30        let regs: &[Register8] = match self {
31            RoutineAction::DrawPixelMaskedSprite => {
32                &[Register8::D, Register8::E, Register8::B, Register8::C]
33            },
34            RoutineAction::SaveBackgroundAndDrawPixelMaskedSprite => &[Register8::B, Register8::C],
35            RoutineAction::RestoreBackground => &[]
36        };
37        RegistersStore::new(regs)
38    }
39}
40
41#[derive(Default)]
42pub struct RegistersStore {
43    regs: HashMap<Register8, u8>,
44    available_regs: Vec<Register8>
45}
46
47impl RegistersStore {
48    pub fn new(regs: &[Register8]) -> Self {
49        Self {
50            regs: Default::default(),
51            available_regs: regs.to_vec()
52        }
53    }
54
55    pub fn listing(&self) -> Listing {
56        let mut lst = ListingBuilder::default();
57
58        let mut available_registers = self.regs.keys().collect_vec();
59        while let Some(first) = available_registers.pop() {
60            // if possible build a 16 bits number
61            let second = first.neighbourg().unwrap();
62            if let Some(idx) = available_registers.iter().position(|v| **v == second) {
63                let second = available_registers.swap_remove(idx);
64                let complete = first.complete();
65                let value = self.value_for_r16(complete).unwrap();
66                lst = lst.ld_r16_expr(complete, value);
67            }
68            else {
69                // otherwise fallback on a 8bits one
70                let value = self.value_for_r8(*first).unwrap();
71                lst = lst.ld_r8_expr(*first, value);
72            }
73        }
74
75        lst.build()
76    }
77
78    pub fn register_for(&self, val: u8) -> Option<Register8> {
79        self.regs.iter().find(|(r, v)| **v == val).map(|(r, v)| *r)
80    }
81
82    pub fn value_for_r8(&self, r: Register8) -> Option<u8> {
83        self.regs.get(&r).cloned()
84    }
85
86    pub fn value_for_r16(&self, r: Register16) -> Option<u16> {
87        let low = self.value_for_r8(r.low().unwrap());
88        let high = self.value_for_r8(r.high().unwrap());
89        if let (Some(low), Some(high)) = (low, high) {
90            Some(low as u16 + (high as u16) * 256)
91        }
92        else {
93            None
94        }
95    }
96
97    pub fn set(&mut self, r: Register8, v: u8) -> &mut Self {
98        assert!(!self.available_regs.contains(&r));
99        self.regs.insert(r, v);
100        self
101    }
102
103    pub fn next_available_regs(&mut self) -> Option<Register8> {
104        self.available_regs.pop()
105    }
106
107    pub fn has_available_regs(&self) -> bool {
108        !self.available_regs.is_empty()
109    }
110}
111
112#[derive(Builder)]
113//#[builder(start_fn = with_header)]
114#[builder(on(String, into))]
115#[builder(on(SmolStr, into))]
116pub struct Compiler {
117    //  #[builder(start_fn)]
118    header_comment: Option<String>,
119    // #[builder(start_fn)]
120    header_label: Option<SmolStr>,
121
122    /// The listing of the final code
123    #[builder(default)]
124    display_lst: Listing,
125
126    #[builder(default)]
127    restore_lst: Listing,
128
129    /// Utility to handle line changes
130    bc26: Bc26,
131
132    #[builder(default)]
133    regs: RegistersStore,
134
135    #[builder(default)]
136    action: RoutineAction,
137
138    #[builder(skip)]
139    nb_lines_to_pass: usize
140}
141
142impl Deref for Compiler {
143    type Target = Listing;
144
145    fn deref(&self) -> &Self::Target {
146        &self.display_lst
147    }
148}
149
150impl DerefMut for Compiler {
151    fn deref_mut(&mut self) -> &mut Self::Target {
152        &mut self.display_lst
153    }
154}
155
156impl Compiler {
157    pub fn build_stats(spr: &SpriteOutput, msk: &SpriteOutput) -> BTreeMap<u8, usize> {
158        let mut set: BTreeMap<u8, usize> = Default::default();
159        for b in spr
160            .data()
161            .iter()
162            .zip(msk.data().iter())
163            .filter_map(|(&b, &m)| {
164                if m != 0xFF && m != 0x00 {
165                    Some([b, m])
166                }
167                else {
168                    None
169                }
170            })
171            .flatten()
172        {
173            if let std::collections::btree_map::Entry::Vacant(e) = set.entry(b) {
174                e.insert(1);
175            }
176            else {
177                *set.get_mut(&b).unwrap() += 1;
178            }
179        }
180
181        set
182    }
183
184    pub fn compile(mut self, spr: &SpriteOutput, msk: &SpriteOutput) -> Listing {
185        assert_eq!(spr.bytes_width(), msk.bytes_width());
186        assert_eq!(spr.height(), msk.height());
187        assert_eq!(spr.encoding(), msk.encoding());
188
189        let stats = Self::build_stats(spr, msk);
190        let mut retained = stats
191            .into_iter()
192            .filter(|(k, v)| *v > 3)
193            .sorted_by_key(|(k, v)| *v)
194            .collect_vec();
195
196        // prefetch registers with most used values
197        self.regs = self.action.build_register_store();
198        while let Some((v, _)) = retained.pop()
199            && self.regs.has_available_regs()
200        {
201            let r = self.regs.next_available_regs().unwrap();
202            self.regs.set(r, v);
203        }
204
205        let width = spr.bytes_width();
206
207        self.emit_header();
208        let mut write_cursor = 0;
209        for (idx, line) in spr
210            .data()
211            .iter()
212            .cloned()
213            .zip(msk.data().iter().cloned())
214            .chunks(width)
215            .into_iter()
216            .enumerate()
217        {
218            write_cursor = self.emit_line(idx, line, write_cursor)
219        }
220
221        self.emit_footer();
222        self.display_lst
223    }
224
225    fn emit_line(
226        &mut self,
227        line_idx: usize,
228        line: impl Iterator<Item = (u8, u8)>,
229        mut write_cursor: u16
230    ) -> u16 {
231        // let mut restore_data = Vec::new();
232        // let mut restore_address = None;
233
234        let nb_moves = 0;
235        self.emit_line_header(line_idx);
236
237        let mut lst = ListingBuilder::default();
238
239        let line = line.collect_vec();
240        let nb_steps = line.len() as u16;
241        for (read_idx, (pixs, mask)) in line.into_iter().enumerate() {
242            // handle zig-zag stuff
243            let read_cursor = if line_idx.is_multiple_of(2) {
244                read_idx as u16
245            }
246            else {
247                nb_steps - read_idx as u16 - 1
248            };
249            let will_write_on_screen = mask != 0xFF;
250
251            // we need to lazily  move the write buffer when some modifications have to be done
252            if will_write_on_screen {
253                assert!(self.use_8bits_addresses_handling());
254
255                self.flushing_pending_next_line_computations();
256
257                // get the number of displacement since last screen update
258                let local_moves = (read_cursor as i32) - (write_cursor as i32);
259
260                // select the fastest way to do it
261                let chosen: Listing = match local_moves {
262                    0 => Listing::new(),
263                    1 => inc_l().into(),
264                    -1 => dec_l().into(),
265                    local_moves => {
266                        let choice1: Listing = ListingBuilder::default()
267                            .repeat(
268                                local_moves.abs(),
269                                if local_moves > 0 { inc_l() } else { dec_l() }
270                            )
271                            .build();
272                        let choice2: Listing = ListingBuilder::default()
273                            .ld_a_expr(local_moves)
274                            .add_l()
275                            .ld_l_a()
276                            .build();
277
278                        ListingSelector::default()
279                            .add(choice1)
280                            .add(choice2)
281                            .select()
282                    }
283                };
284
285                // add it to the generated code
286                lst = lst
287                    .comment(format!("Move of {local_moves} bytes"))
288                    .extend(chosen); // inject the fastest one
289
290                write_cursor = read_cursor;
291
292                if self.action.save_background() {
293                    // backup the new address
294
295                    match local_moves {
296                        0 => {},
297                        1 => {
298                            // nothing to do in the current listing
299                            self.restore_lst.add(inc_e());
300                        },
301                        -1 => {
302                            self.restore_lst.add(dec_e());
303                        },
304                        _ => {
305                            lst = lst
306                                .comment("Save screen address")
307                                .ex_hl_de()
308                                .ld_mem_hl_e()
309                                .inc_hl() // TODO use incl when possible
310                                .ld_mem_hl_d()
311                                .inc_hl() // TODO use inc l when possible
312                                .ex_hl_de();
313
314                            self.restore_lst.inject_listing(
315                                &ListingBuilder::default()
316                                    .comment("Retreive screen address")
317                                    .ld_e_mem_hl()
318                                    .inc_hl()
319                                    .ld_d_mem_hl()
320                                    .inc_hl()
321                                    .build()
322                            );
323                        }
324                    }
325                }
326            }
327
328            // properly handle the byte
329            match mask {
330                0x00 => {
331                    if self.action.save_background() {
332                        lst = lst
333                            .comment("No masking, but need to save the background")
334                            .ld_a_mem_hl()
335                            .ld_mem_de_a()
336                            .inc_de();
337
338                        self.restore_lst.inject_listing(
339                            &ListingBuilder::default()
340                                .comment("Read and write byte")
341                                .ld_a_mem_hl()
342                                .inc_hl()
343                                .ld_mem_de_a()
344                                .build()
345                        );
346                    }
347                    else {
348                        // no background pixels are kept
349                        lst = lst.comment("No masking here");
350                    }
351
352                    if let Some(reg) = self.regs.register_for(pixs) {
353                        lst = lst.ld_mem_hl_r8(reg);
354                    }
355                    else {
356                        lst = lst.ld_mem_hl_expr(pixs);
357                    }
358                },
359
360                0xFF => {
361                    // all background pixels are kept
362                    lst = lst.comment("Nothing shown here");
363                },
364
365                mask => {
366                    // masking is necessary
367                    let comment = if pixs != 0 {
368                        "Masked byte"
369                    }
370                    else {
371                        "masked byte BUT no bit to set"
372                    };
373
374                    // mask screen byte
375                    lst = lst.comment(comment).ld_a_mem_hl();
376
377                    if let Some(reg) = self.regs.register_for(mask) {
378                        lst = lst.and_r8(reg);
379                    }
380                    else {
381                        lst = lst.and_expr(mask);
382                    }
383
384                    // request additional bits
385                    match pixs {
386                        0 => {}, // nothing to draw
387                        1 => {
388                            lst = lst.inc_a();
389                        }, // faster/maller than OR 1
390                        val => {
391                            if let Some(reg) = self.regs.register_for(val) {
392                                lst = lst.or_r8(reg);
393                            }
394                            else {
395                                lst = lst.or_expr(pixs);
396                            }
397                        },
398                    }
399
400                    // save
401                    lst = lst.ld_mem_hl_a();
402                }
403            };
404
405            if nb_steps - 1 == read_cursor {
406                lst = lst.comment("End of line");
407            }
408        }
409
410        self.inject_listing(&lst.build());
411
412        self.emit_line_footer();
413        write_cursor
414    }
415
416    fn emit_line_header(&mut self, idx: usize) {
417        self.add_comment(format!("> Handle line {idx}"));
418        if idx == 0 {
419            self.add_comment("  HL already contains the destination address");
420        }
421        else {
422            self.request_next_line_computation();
423        }
424    }
425
426    fn emit_line_footer(&mut self) {}
427
428    // fn emit_move_to_line_start(&mut self, step: u16) {
429    // let lst = if self.use_8bits_addresses_handling() {
430    // ListingBuilder::default()
431    // .comment("Move from end of line to beginning of the same line")
432    // .ld_a_expr(Expr::Value(step as i32).neg())
433    // .add_l()
434    // .ld_l_a()
435    // .build()
436    // } else {
437    // unimplemented!()
438    // };
439    //
440    // self.inject_listing(&lst);
441    // }
442    pub fn use_8bits_addresses_handling(&self) -> bool {
443        self.bc26.use_8bits_addresses_handling().unwrap()
444    }
445
446    fn request_next_line_computation(&mut self) {
447        self.nb_lines_to_pass += 1;
448    }
449
450    fn flushing_pending_next_line_computations(&mut self) {
451        // no found yet why it does not work in my test program :( ...
452        // let nb_chars = self.nb_lines_to_pass/8;
453        // let nb_lines = self.nb_lines_to_pass%8;
454        //
455        // if nb_chars > 0 {
456        // TODO optimize that better if it happens in a sprite, there is no need to have a loop
457        // let lst = self.bc26.routine_8lines().unwrap();
458        // self.inject_listing(&lst);
459        // }
460        //
461
462        let nb_lines = self.nb_lines_to_pass;
463        for _ in 0..nb_lines {
464            self.emit_compute_next_line_address();
465        }
466        self.nb_lines_to_pass = 0;
467    }
468
469    fn emit_compute_next_line_address(&mut self) {
470        self.add_comment("Compute the address of the next line");
471        let execute = self.bc26.clone().execute();
472        self.inject_listing(&execute);
473    }
474
475    fn emit_header(&mut self) {
476        if let Some(comment) = self.header_comment.take() {
477            self.add_comment(comment);
478        }
479        if let Some(label) = self.header_label.take() {
480            self.add_label(label);
481        }
482        self.display_lst.inject_listing(&self.regs.listing());
483    }
484
485    fn emit_footer(&mut self) {
486        self.add(cpclib_asm::ret());
487
488        if let Some(bc26_routine) = self.bc26.routine_1line() {
489            let bc26_label = self.bc26.label();
490            let r#if = IfBuilder::default()
491                .condition(TestKind::ifndef(bc26_label), bc26_routine)
492                .build();
493            self.add(r#if);
494        }
495    }
496}
497
498// https://roudoudou.com/AmstradCPC/programmationAssembleurZ80Ecran.html
499#[derive(Clone, Copy)]
500pub enum Bc26 {
501    Compute16KbC000 { r1: u8 },
502    Compute16KbUniversal { r1: u8 }
503}
504
505impl Bc26 {
506    pub fn new_universal_16k(r1: u8) -> Self {
507        Self::Compute16KbUniversal { r1 }
508    }
509
510    pub fn new_c000_16k(r1: u8) -> Self {
511        Self::Compute16KbC000 { r1 }
512    }
513}
514
515impl Bc26 {
516    pub fn label(&self) -> String {
517        match self {
518            Bc26::Compute16KbC000 { r1 } => Self::c000_routine_label(*r1),
519            Bc26::Compute16KbUniversal { r1 } => Self::universal_routine_label(*r1)
520        }
521    }
522
523    pub fn routine_1line(&self) -> Option<Listing> {
524        match self {
525            Bc26::Compute16KbC000 { r1 } => Some(Self::c000_routine_1line(*r1)),
526            Bc26::Compute16KbUniversal { r1 } => Some(Self::universal_routine_1line(*r1))
527        }
528    }
529
530    pub fn routine_8lines(&self) -> Option<Listing> {
531        match self {
532            Bc26::Compute16KbC000 { r1 } => Some(Self::c000_routine_8lines(*r1)),
533            Bc26::Compute16KbUniversal { r1 } => Some(Self::universal_routine_8lines(*r1))
534        }
535    }
536
537    pub fn execute(&self) -> Listing {
538        match self {
539            Bc26::Compute16KbC000 { r1 } | Bc26::Compute16KbUniversal { r1 } => {
540                ListingBuilder::default().call(self.label()).build()
541            },
542        }
543    }
544
545    pub fn r1(&self) -> Option<u8> {
546        match self {
547            Self::Compute16KbC000 { r1 } | Self::Compute16KbUniversal { r1 } => Some(*r1)
548        }
549    }
550
551    pub fn use_8bits_addresses_handling(&self) -> Option<bool> {
552        self.r1().map(|r1| r1 == 32)
553    }
554
555    fn universal_routine_label(r1: u8) -> String {
556        format!("universal_bc26_r1_{r1}")
557    }
558
559    fn c000_routine_label(r1: u8) -> String {
560        format!("c000_bc26_r1_{r1}")
561    }
562
563    fn universal_routine_1line(r1: u8) -> Listing {
564        let label = Self::universal_routine_label(r1);
565        Listing::from_str(&format!(
566            "
567{}
568            ld a,h : add 8 : ld h,a : and #38 : ret nz
569            ld a,{} : add l : ld l,a : ld a,#C0 : adc h : ld h,a : res 3,h
570            ret
571        ",
572            label,
573            r1 * 2
574        ))
575        .unwrap()
576    }
577
578    fn universal_routine_8lines(r1: u8) -> Listing {
579        if r1 == 32 {
580            Listing::from_str(&format!("ld a,{} : add l : ld l,a", r1 * 2)).unwrap()
581        }
582        else {
583            unimplemented!()
584        }
585    }
586
587    fn c000_routine_1line(r1: u8) -> Listing {
588        let label = Self::c000_routine_label(r1);
589        Listing::from_str(&format!(
590            "
591    {}
592            ld a,h : add 8 : ld h,a : ret nc
593            ld a,{} : add l : ld l,a : ld a,#C0 : adc h : ld h,a : res 3,h
594            ret
595        ",
596            label,
597            r1 * 2
598        ))
599        .unwrap()
600    }
601
602    fn c000_routine_8lines(r1: u8) -> Listing {
603        Self::universal_routine_8lines(r1)
604    }
605}
606
607/// Dummy display routine.
608///
609/// Display from left to right then right to left and so on ...
610/// Prefetch most important bytes in registers (but do not update them)
611///
612/// ; Input: HL = drawing address
613pub fn standard_sprite_compiler(
614    label: &str,
615    spr: &SpriteOutput,
616    msk: &SpriteOutput,
617    r1: u8
618) -> Listing {
619    let spr = spr.with_encoding(SpriteEncoding::LeftToRightToLeft);
620    let msk = msk.with_encoding(SpriteEncoding::LeftToRightToLeft);
621
622    let comp = Compiler::builder()
623        .header_comment("Linear sprite display routine. HL contains screen address")
624        .header_label(label)
625        .bc26(Bc26::new_universal_16k(r1))
626        .build();
627    comp.compile(&spr, &msk)
628}
629
630pub fn standard_sprite_with_background_backup_and_restore_compiler(
631    label: &str,
632    spr: &SpriteOutput,
633    msk: &SpriteOutput,
634    r1: u8
635) -> Listing {
636    let spr = spr.with_encoding(SpriteEncoding::LeftToRightToLeft);
637    let msk = msk.with_encoding(SpriteEncoding::LeftToRightToLeft);
638
639    todo!(
640        "XXX Nothing is finished here. Cannot be used without fixes. Restore code is not stored  BTW"
641    );
642
643    let comp = Compiler::builder()
644            .header_comment("Linear sprite display routine. HL contains screen address and DE contains save_buffer_address")
645            .header_label(label)
646            .bc26(Bc26::new_universal_16k(r1))
647            .action(RoutineAction::SaveBackgroundAndDrawPixelMaskedSprite)
648            .build();
649    comp.compile(&spr, &msk)
650}