Skip to content

Commit

Permalink
Add the "progmem" feature
Browse files Browse the repository at this point in the history
  • Loading branch information
bellinitte committed Oct 27, 2022
1 parent a3fe271 commit 0177c11
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 120 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Expand Up @@ -12,7 +12,12 @@ categories = ["embedded", "encoding", "graphics", "no-std"]
documentation = "https://docs.rs/stockbook"
exclude = [".github", "docs"]

[features]
progmem = ["avr-progmem"]

[dependencies]
avr-progmem = { version = ">=0.2.0, <0.4.0", optional = true }
cfg-if = "1.0.0"
stockbook-stamp-macro = { version = "=0.2.0", path = "macro" }

[workspace]
Expand Down
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -6,7 +6,7 @@

Stockbook embeds 1-bit raster images in your code at compile time.

Designed primarily for `#![no_std]` usage, in embedded or other program-memory-constrained environments.
Designed primarily for `#![no_std]` usage, in embedded or other program-memory-constrained environments. Compatible with [`avr-progmem`](https://crates.io/crates/avr_progmem).

```toml
[dependencies]
Expand Down Expand Up @@ -46,6 +46,10 @@ fn draw_pixel_at(x: usize, y: usize) {

Stockbook uses the [image](https://docs.rs/image) crate under the hood. See its own [list of supported formats](https://docs.rs/image/latest/image/codecs/index.html#supported-formats) for more details.

## Feature flags

- **`progmem`** &mdash; wraps all pixel data of `Stamp`s in [`avr_progmem::wrapper::ProgMem`](https://docs.rs/avr-progmem/latest/avr_progmem/wrapper/struct.ProgMem.html)s. Combined with the `avr` target, this allows you to keep most of the data in program memory without the need to copy it to RAM. A no-op for non-`avr` targets.

## Unstable features

Although this library works on `stable`, any changes to images referenced by the `stamp!` macro might not be detected because of caching. Therefore, until [`track_path` API](https://doc.rust-lang.org/stable/proc_macro/tracked_path/fn.path.html) ([Tracking Issue](https://github.com/rust-lang/rust/issues/99515)) stabilizes, it is recommended to use the `nightly` toolchain, however functionality behind this feature is unstable and may change or stop compiling at any time.
Expand Down
52 changes: 30 additions & 22 deletions macro/src/lib.rs
Expand Up @@ -22,6 +22,10 @@ use syn::{
/// This macro will encode the image and yield an expression of type
/// [`Stamp`][Stamp] with the pixel data included.
///
/// If the `"progmem"` feature is enabled and the target is set to `avr`, the pixel
/// data will be placed into the `.progmem.data` section using the
/// `#[link_section = ".progmem.data"]` attribute.
///
/// # Examples
///
/// Assume there are two files in the same directory: a 16x12 pixel image
Expand Down Expand Up @@ -168,33 +172,37 @@ impl ToTokens for Stamp {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let width = self.width;
let height = self.height;
let array = syn::ExprReference {
let array_len = self.data.len();
let array = syn::ExprArray {
attrs: Default::default(),
and_token: Default::default(),
raw: Default::default(),
mutability: Default::default(),
expr: Box::new(syn::Expr::Array(syn::ExprArray {
attrs: Default::default(),
bracket_token: Default::default(),
elems: self
.data
.iter()
.map(|byte| {
syn::Expr::Lit(syn::ExprLit {
attrs: Default::default(),
lit: syn::Lit::Int(syn::LitInt::new(
&byte.to_string(),
Span::call_site(),
)),
})
bracket_token: Default::default(),
elems: self
.data
.iter()
.map(|byte| {
syn::Expr::Lit(syn::ExprLit {
attrs: Default::default(),
lit: syn::Lit::Int(syn::LitInt::new(&byte.to_string(), Span::call_site())),
})
.collect(),
})),
})
.collect(),
};

// If the `"progmem"` feature is enabled, the array will be wrapped in the
// `ProgMem` struct, thus it is impossible for safe Rust to directly access the
// progmem data
let progmem_attr = quote! {
#[cfg_attr(all(target_arch = "avr", feature = "progmem"), link_section = ".progmem.data")]
};

tokens.extend(quote! {
unsafe {
::stockbook::Stamp::from_raw_unchecked(#width, #height, #array)
{
#progmem_attr
static VALUE: [u8; #array_len] = #array;

unsafe {
::stockbook::Stamp::from_raw(#width, #height, VALUE.as_ptr())
}
}
});
}
Expand Down
113 changes: 113 additions & 0 deletions src/data.rs
@@ -0,0 +1,113 @@
#[cfg(feature = "progmem")]
use avr_progmem::{raw::read_byte, wrapper::ProgMem};
use cfg_if::cfg_if;
use core::fmt::Debug;

/// Byte array wrapper &mdash; source of data for a [`Stamp`](crate::Stamp).
pub struct Data {
#[cfg(not(feature = "progmem"))]
source: *const u8,

#[cfg(feature = "progmem")]
source: ProgMem<u8>,
}

impl Data {
/// Constructs a new instance of this type.
///
/// # Safety
///
/// `ptr` must point to a byte array.
///
/// If the `"progmem"` feature is enabled, `ptr` must point to a valid byte array
/// that is stored in the program memory domain. The array must be initialized,
/// readable, and immutable (i.e. it must not be changed). Also the pointer must be
/// valid for the `'static` lifetime.
pub const unsafe fn from_raw(ptr: *const u8) -> Self {
cfg_if! {
if #[cfg(feature = "progmem")] {
Self {
source: ProgMem::new(ptr),
}
} else {
Self {
source: ptr,
}
}
}
}

/// Returns a byte at `idx`, without doing bounds checking.
///
/// # Safety
///
/// Calling this method with an out-of-bounds index is undefined behavior, even if
/// the resulting reference is not used.
pub unsafe fn get_unchecked(&self, idx: usize) -> u8 {
let ptr = self.as_ptr().add(idx);
Self::deref(ptr)
}

/// Return the raw pointer to the inner value.
///
/// If the `"progmem"` feature is enabled, the returned pointer must not be
/// dereferenced via the default Rust operations.
pub fn as_ptr(&self) -> *const u8 {
cfg_if! {
if #[cfg(feature = "progmem")] {
self.source.as_ptr()
} else {
self.source
}
}
}

unsafe fn deref(ptr: *const u8) -> u8 {
cfg_if! {
if #[cfg(feature = "progmem")] {
// Since we're building with the `"progmem"` feature, `ptr` is valid in the program
// domain.
read_byte(ptr)
} else {
*ptr
}
}
}
}

impl Debug for Data {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:p}", self.as_ptr())
}
}

impl Clone for Data {
fn clone(&self) -> Self {
cfg_if! {
if #[cfg(feature = "progmem")] {
// SAFETY: we construct a `ProgMem` with a pointer we got from a `ProgMem`.
// Required becase `ProgMem` doesn't provide a `Clone` implementation.
let source = unsafe { ProgMem::new(self.source.as_ptr()) };
Self {
source,
}
} else {
Self {
source: self.source,
}
}
}
}
}

unsafe impl Send for Data {
// SAFETY: pointers per-se are sound to send and share. Furthermore, we never mutate
// the underling value, thus `Data` can be considered as some sort of a sharable
// `'static` "reference". Thus it can be shared and transferred between threads.
}

unsafe impl Sync for Data {
// SAFETY: pointers per-se are sound to send and share. Furthermore, we never mutate
// the underling value, thus `Data` can be considered as some sort of a sharable
// `'static` "reference". Thus it can be shared and transferred between threads.
}
16 changes: 8 additions & 8 deletions src/iter/pixels.rs
Expand Up @@ -5,7 +5,7 @@ use core::iter::FusedIterator;
///
/// This type is created by the [`pixels`](Stamp::pixels) method on [`Stamp`]. See
/// its documentation for more details.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Pixels<'a> {
cursor: Cursor<'a>,
cursor_back: CursorBack<'a>,
Expand All @@ -23,7 +23,7 @@ impl<'a> Pixels<'a> {
}

/// An iterator that cycles throygh all pixels of a [`Stamp`] from front to back.
#[derive(Debug)]
#[derive(Debug, Clone)]
struct Cursor<'a> {
x: usize,
y: usize,
Expand Down Expand Up @@ -57,7 +57,7 @@ impl Iterator for Cursor<'_> {
}

/// An iterator that cycles throygh all pixels of a [`Stamp`] from back to front.
#[derive(Debug)]
#[derive(Debug, Clone)]
struct CursorBack<'a> {
x: usize,
y: usize,
Expand Down Expand Up @@ -126,31 +126,31 @@ mod tests {

#[test]
fn test_zero_size_stamp() {
let stamp = Stamp::from_raw(0, 0, &[]);
let stamp = unsafe { Stamp::from_raw(0, 0, [].as_ptr()) };
let mut pixels = stamp.pixels();

assert_eq!(pixels.next(), None);
}

#[test]
fn test_zero_width_stamp() {
let stamp = Stamp::from_raw(0, 3, &[]);
let stamp = unsafe { Stamp::from_raw(0, 3, [].as_ptr()) };
let mut pixels = stamp.pixels();

assert_eq!(pixels.next(), None);
}

#[test]
fn test_zero_height_stamp() {
let stamp = Stamp::from_raw(3, 0, &[]);
let stamp = unsafe { Stamp::from_raw(3, 0, [].as_ptr()) };
let mut pixels = stamp.pixels();

assert_eq!(pixels.next(), None);
}

#[test]
fn test_double_ended() {
let stamp = Stamp::from_raw(2, 2, &[0b1010_0000]);
let stamp = unsafe { Stamp::from_raw(2, 2, [0b1010_0000].as_ptr()) };
let mut pixels = stamp.pixels();

assert_eq!(pixels.next(), Some((0, 0, Color::White)));
Expand All @@ -163,7 +163,7 @@ mod tests {

#[test]
fn test_rev() {
let stamp = Stamp::from_raw(2, 2, &[0b1010_0000]);
let stamp = unsafe { Stamp::from_raw(2, 2, [0b1010_0000].as_ptr()) };
let mut pixels = stamp.pixels().rev();

assert_eq!(pixels.next(), Some((1, 1, Color::Black)));
Expand Down

0 comments on commit 0177c11

Please sign in to comment.