Hi-res c64 images with Rstats 'r64' package

Hires bitmap

In this post, the r64 package will be used to:

  • generate a 320x200 hires Rstats logo in bitmap form suitable for the c64.
  • incorporate those bytes into an R program using the r64 package and the R-specific .rbyte directive.
  • compile the code and run it in an emulator (VICE).

Converting an image into a monochrome bitmap matix

  • Use imagemagick to convert an image into:
    • 320x200
    • monochrome
    • dithered
    • keeping aspect ratio while padding to the correct size
#' Create a 320x200 binary matrix out of an image file
#' @param image_filename full path to an image that can be read by imagemagick
#' @param dither boolean. floyd steinberg dithering?  default: TRUE
#' @param invert boolean. invert the image? default TRUE
#' @return a 200x320 binary matrix (values of only 0 and 1) representing the 
#'         dithered black and white image
create_bw_matrix_image <- function(image_filename = system.file('img/Rlogo.jpg',package='jpeg'), dither=TRUE, invert=FALSE, padding='white') {
  # Temporary output file
  monochrome_filename <- tempfile(fileext=".pgm")
  # The size of the hires bitmap screen on a c64
  width   <- 320
  height  <- 200
  # I couldn't work out how to do all the manipulation 
  # just using the 'magick' package (e.g. -extent), so
  # construct a imagemagick convert command in the shell
  dither_    <- ifelse(dither, "-dither FloydSteinberg", "")
  monochrome <- "-colors 2 -colorspace gray -normalize"
  command    <- glue::glue("convert {shQuote(image_filename)} -resize {width}x{height} -background {padding} -gravity center -extent {width}x{height} {monochrome} {dither_} {monochrome_filename}")
  im <- magick::image_read(monochrome_filename) %>%
    magick::as_EBImage() %>%
  # Return just the matrix data part
  bw_matrix <- t(im@.Data)
  if (invert) {
    bw_matrix <- 1L - bw_matrix

Convert a 0/1 matrix into a bytestream ready for the c64

  • The hires bitmap is encoded one character at a time, rather than 1 row at a time
  • This means extra work needs to be done to turn a matrix of 0/1 into a bytestream image for the c64
# Convert a sequence of eight 0/1 values into a byte
bits_to_byte <- function(eight_bits) {
  stopifnot(length(eight_bits) == 8)
  stopifnot(all(eight_bits %in% c(0, 1)))
  as.integer(sum(2^seq.int(7, 0)  * eight_bits))

# Convert a row of a binary matrix into a vector of bytes.
row_to_bytes <- function(row) {
  split(row, ceiling(seq_along(row)/8)) %>%

#' Turn a binary matrix into bytes for a c64 bitmap
create_c64_byte_image <- function(bw_matrix) {
  stopifnot(max(bw_matrix) == 1)
  stopifnot(identical(dim(bw_matrix), c(200L, 320L)))
  # Turn each row of bits into a row of bytes
  byte_image <- t(apply(bw_matrix, 1, row_to_bytes))
  # Reorder the bytes to use as a c64 bitmap.
  # The ordering of a c64 bitmap is unconventional by modern standards!
  c64_byte_image <- seq(1, 200-8+1, 8) %>%
    purrr::map(~as.vector(byte_image[.x:(.x+7),])) %>%

bw_matrix <- create_bw_matrix_image(dither=FALSE, invert=FALSE)
c64_image <- create_c64_byte_image(bw_matrix)

# View a matrix as an image
# plot(t(EBImage::as.Image(bw_matrix)))
# View cut-down matrix as ascii
# cat(paste(apply(bw_matrix[c(T, F, F, F, F, F, F, F), c(T, F, F, F, T, F, F, F)], 1, paste, collapse=''), collapse="\n"))

Compile ASM code to PRG


## Manual compilation steps if debugging
# line_tokens <- r64::create_line_tokens(asm)
# prg_df      <- r64::create_prg_df(line_tokens)
# prg_df      <- r64::process_symbols(prg_df)
# prg_df      <- r64::process_zero_padding(prg_df)

# compile code in a single step
prg_df       <- r64::compile(asm)

# Save code to file
prg_filename <- "../prg/bitmap.prg"
r64::save_prg(prg_df, prg_filename)

See the prg directory in this package for ready-to-run PRGs of this code.

Run code in an emulator

system(paste("/usr/local/opt/vice/x64.app/Contents/MacOS/x64 -VICIIfilter 0 -silent", prg_filename), wait=FALSE)

c64 screenshot in vice

Other examples

c64 screenshot in vice c64 screenshot in vice c64 screenshot in vice