mos-hardware 0.4.0

Hardware register tables and support functions for 8-bit retro computers like the Commodore 64, MEGA65 and others.
Documentation
// copyright 2022 mikael lund aka wombat
//
// licensed under the apache license, version 2.0 (the "license");
// you may not use this file except in compliance with the license.
// you may obtain a copy of the license at
//
//     http://www.apache.org/licenses/license-2.0
//
// unless required by applicable law or agreed to in writing, software
// distributed under the license is distributed on an "as is" basis,
// without warranties or conditions of any kind, either express or implied.
// see the license for the specific language governing permissions and
// limitations under the license.

//! Classic C64 Sprite and scroll example

#![no_std]
#![feature(start)]

extern crate alloc;
extern crate mos_alloc;

use core::panic::PanicInfo;
use mos_hardware::screen_codes;
use mos_hardware::sid::SidTune;
use mos_hardware::{c64, vic2};
use vic2::*;

/// Classic, smooth x-scroll using VIC2's 0xd016 register
struct SmoothScroll {
    /// Current position in scroll text
    text_index: usize,
    /// horizontal pixel position (0 to 7)
    displacement: u8,
}

impl SmoothScroll {
    /// Vertical position of scroll
    const YPOSITION: u16 = 8;
    /// Pointer to first character of scroll line (beginning of character row)
    const LEFTMOST_CHAR: *mut u8 = (0x0400 + (40 * SmoothScroll::YPOSITION as u16)) as *mut u8;
    /// Pointer to last character of scroll line (end of character row)
    const RIGHTMOST_CHAR: *mut u8 =
        (0x0400 + (40 * SmoothScroll::YPOSITION + 39 as u16)) as *mut u8;
    /// PETSCII encoded scroll text
    const SCROLL_TEXT: [u8; 262] = screen_codes!(
        "Hello from RUST! This is a tiny demo written \
    in rust using the llvm-mos backend for 6502 code generation. \
    The top color flickering reflects time spent in the raster interrupt, i.e. \
    scroll (green) and sprite movement (red), while SID music playback is not shown. "
    );

    /// Create new instance
    ///
    /// Note this is an associated `const` function:
    /// https://doc.rust-lang.org/reference/const_eval.html#const-functions
    const fn new() -> Self {
        Self {
            text_index: 0,
            displacement: 7,
        }
    }

    pub fn init(&self) {
        // set 48 column mode for smooth x-scrolling
        let mut mask = c64::vic2().control_x.read();
        mask.set(ControlXFlags::COLUMN_SELECT, false);
        unsafe {
            c64::vic2().control_x.write(mask);
        }
    }

    /// Move screen ONE pixel to the left (cycle in the interval 7..0)
    #[inline]
    fn move_pixel(&mut self) {
        self.displacement = match self.displacement.checked_sub(1) {
            Some(x) => x,
            None => 7,
        };
        let mut mask = c64::vic2().control_x.read();
        mask.set(ControlXFlags::XSCROLL, false);
        mask = ControlXFlags::from_bits(mask.bits() + self.displacement).unwrap();
        unsafe {
            c64::vic2().control_x.write(mask);
        }
    }

    /// Copy all characters on scroll line ONE character to the left (0-39 <-- 1-40)
    #[inline]
    fn leftcopy_chars(&self) {
        // faster than core::ptr::copy(LEFTMOST_CHAR.offset(1), LEFTMOST_CHAR, 39)
        for i in 0..39 {
            unsafe {
                let character = SmoothScroll::LEFTMOST_CHAR.offset(i + 1).read_volatile();
                SmoothScroll::LEFTMOST_CHAR
                    .offset(i)
                    .write_volatile(character);
            }
        }
    }

    /// Place new character at (invisible) right-most position and left-shift line
    #[inline]
    fn update_chars(&mut self) {
        if self.text_index == SmoothScroll::SCROLL_TEXT.len() {
            self.text_index = 0;
        }
        unsafe {
            SmoothScroll::RIGHTMOST_CHAR.write_volatile(SmoothScroll::SCROLL_TEXT[self.text_index]);
        }
        self.leftcopy_chars();
        self.text_index += 1;
    }

    /// Called at every IRQ
    fn update(&mut self) {
        self.move_pixel();
        if self.displacement == 7 {
            self.update_chars();
        }
    }
}

/// Move single sprite in sine pattern in x-y direction
struct SpriteMove {
    /// Tracks x position and increased in every update
    counter_x: u8,
    /// Tracks y position and increased in every update
    counter_y: u8,
}

impl SpriteMove {
    const SPRITE_ADDRESS: u16 = 0x2000;
    const SPRITE_PTR: u8 = vic2::to_sprite_pointer(SpriteMove::SPRITE_ADDRESS);
    const OFFSET: u8 = 30;
    const MSB_THRESHOLD: u8 = 255 - SpriteMove::OFFSET;

    /// An associated `const` function to generate a new instance
    const fn new() -> Self {
        SpriteMove {
            counter_x: 0,
            counter_y: 0,
        }
    }
    /// Initialize (copy Rust logo to sprite address; set sprite shape pointers)
    pub fn init(&self) {
        unsafe {
            *(SpriteMove::SPRITE_ADDRESS as *mut [u8; 63]) = RUST_LOGO;
            c64::DEFAULT_SPRITE_PTR[0].write_volatile(SpriteMove::SPRITE_PTR);
            c64::vic2().sprite_expand_x.write(Sprites::SPRITE0);
            c64::vic2().sprite_expand_y.write(Sprites::SPRITE0);
            c64::vic2().sprite_enable.write(Sprites::SPRITE0);
        }
        c64::vic2().set_sprite_color(0, GREEN);
    }

    /// Advance sprite position in a sine pattern
    fn update(&mut self) {
        static XSINE: [u8; 256] = mos_hardware::make_sine(1, 0);
        static YSINE: [u8; 256] = mos_hardware::make_sine(4, 70);
        let x = XSINE[self.counter_x as usize];
        let y = YSINE[self.counter_y as usize];
        let (offset, msb) = match x > SpriteMove::MSB_THRESHOLD {
            true => (SpriteMove::OFFSET.wrapping_sub(255), Sprites::SPRITE0),
            false => (SpriteMove::OFFSET, Sprites::empty()),
        };
        unsafe {
            c64::vic2()
                .sprite_positions_most_significant_bit_of_x
                .write(msb);
        }
        c64::vic2().set_sprite_pos(0, x + offset, y);
        self.counter_x += 2;
        self.counter_y += 1;
    }
}

/// Global since the interrupt wrapper currently do not take arguments
static mut SCROLL: SmoothScroll = SmoothScroll::new();
static mut SPRITE_MOVE: SpriteMove = SpriteMove::new();

struct SidFile;
impl SidTune for SidFile {
    const BYTES: &'static [u8] = core::include_bytes!("../assets/last_hero.sid");
}
static MUSIC: SidFile = SidFile;

/// IRQ wrapper; called at every triggering event
///
/// The raster time taken up by the rust code is
/// visualized by changing the border color upon entering
/// and exiting the function (SID tune is excluded).
#[no_mangle]
pub extern "C" fn called_every_frame() {
    let mut cnt: u8 = 0;
    unsafe {
        c64::vic2().border_color.write(vic2::RED);
        SPRITE_MOVE.update();
        c64::vic2().border_color.write(vic2::LIGHT_GREEN);
        cnt += 2;
        if cnt % 2 == 0 {
            SCROLL.update();
        }
        c64::vic2().border_color.write(vic2::BLACK);
    }
    MUSIC.play(); // excluded from border color profiling
}

#[start]
fn _main(_argc: isize, _argv: *const *const u8) -> isize {
    // unsafe {
    //     mos_hardware::mega65::libc::mega65_io_enable();
    // }
    // //    mos_hardware::mega65::speed_mode3();
    c64::clear_screen();
    c64::set_lower_case();
    unsafe {
        SCROLL.init();
        SPRITE_MOVE.init();
        MUSIC.to_memory();
    }
    MUSIC.init(0);
    c64::hardware_raster_irq(20);
    loop {} // let's not return to dead BASIC
}

/// Sprite pattern
const RUST_LOGO: [u8; 63] = [
    0, 90, 0, 1, 255, 128, 7, 239, 224, 15, 24, 240, 28, 0, 56, 63, 255, 28, 127, 255, 158, 127,
    255, 222, 235, 195, 215, 115, 255, 142, 227, 255, 135, 99, 199, 142, 247, 195, 223, 127, 243,
    254, 127, 241, 254, 62, 0, 60, 29, 0, 184, 15, 129, 240, 7, 255, 224, 1, 255, 128, 0, 90, 0,
];

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
    loop {
        unsafe {
            c64::vic2().border_color.write(RED);
            c64::vic2().border_color.write(BLACK);
        }
    }
}