Skip to content

Add PICO-8 custom font compilation target#6

Open
The-Nice-One wants to merge 4 commits intomainfrom
feature/spfc-target-p8
Open

Add PICO-8 custom font compilation target#6
The-Nice-One wants to merge 4 commits intomainfrom
feature/spfc-target-p8

Conversation

@The-Nice-One
Copy link
Copy Markdown
Member

@The-Nice-One The-Nice-One commented May 9, 2026

Summary by CodeRabbit

  • New Features

    • Added Pico-8 backend support, enabling conversion of fonts into Pico-8 cartridge programs with integrated font rendering and sample usage templates.
  • Chores

    • Updated development container configuration with Rust tooling and Git LFS support.
    • Refactored workspace dependencies for improved maintainability across backends.

@The-Nice-One The-Nice-One linked an issue May 9, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a complete Pico-8 backend for SPFC. Changes include workspace setup, a 256-entry character lookup table, pixmap-to-bitmap glyph conversion, Lua program generation with embedded font data, compile entry points, and development container configuration. The TTF backend is updated to use shared workspace dependencies.

Changes

Pico-8 Backend Implementation

Layer / File(s) Summary
Workspace & Manifest Setup
Cargo.toml, backends/spfc-target-p8/Cargo.toml
Root workspace adds backends/spfc-target-p8 member and shared spf/render_spf dependencies; new P8 crate declares cdylib library with bitvec and workspace dependencies.
Data Contracts – Character Set
backends/spfc-target-p8/src/utilities/characters.rs
P8Char struct (id, symbol, description) and 256-entry compile-time character table P8SCII_CHARSET with lookup function get_character_by_symbol.
Data Contracts – Pixmap Glyph
backends/spfc-target-p8/src/utilities/pixmap.rs
PixmapGlyph struct (width, height, bitmap); PixmapGlyphTextureBuilder converts bit-packed pixmaps to row-wise bitmaps; RenderableTexture trait impl; create_pixmap_pairs factory.
Utilities Module Integration
backends/spfc-target-p8/src/utilities/mod.rs
Exports character and pixmap submodules; provides max_width, max_height, last_character_index helpers over glyph maps.
Builder Process Structure
backends/spfc-target-p8/src/builders/mod.rs
Process struct holds family metadata (family_name, family_version, manufacturer) and pixmap_pairs map; declares writer submodule.
Program Generation – Writer
backends/spfc-target-p8/src/builders/writer.rs
create_program_string generates full Pico-8 Lua cartridge: header, memory buffer allocation, glyph bitmap population, poke/unpack serialization, and sample _init/_draw functions.
Compile Entrypoint
backends/spfc-target-p8/src/lib.rs
Exports get_backend_info, get_plugin_options, compile; compile reads input layout, constructs Process, generates Lua output, writes file, and returns success.
Dependency Alignment
backends/spfc-target-ttf/Cargo.toml
TTF backend updated to use workspace-managed spf and render_spf dependencies instead of inline versions.
Development Environment
.devcontainer/devcontainer.json
VS Code devcontainer configured with Rust image, spell-check extension, and Git LFS installation.

Possibly Related Issues

  • Support P8CII compilation target #3: This PR directly implements the Pico-8 (P8SCII) backend requested in the issue, adding character set utilities, pixmap-to-glyph rendering, and a complete compile implementation for P8 cartridge generation.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A pixel-painted bunny hops with glee,
P8 glyphs now dance in cartridge memory!
From bitmap rows to Lua-crafted script,
The font's complete, not a syllable skipped.
Welcome, dear Pico, to our font-bearing quest! 🎮✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding a new PICO-8 compilation backend for custom font compilation, which is precisely what the PR implements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/spfc-target-p8

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
.devcontainer/devcontainer.json (1)

8-10: ⚡ Quick win

Prefer Dev Container Feature for Git LFS instead of apt in postCreateCommand.

Lines 8–10 show an unused features section. Moving Git LFS provisioning from line 27's postCreateCommand to the proper ghcr.io/devcontainers/features/git-lfs:1 feature is cleaner, faster, and more reproducible.

Proposed change
-	// Features to add to the dev container. More info: https://containers.dev/features.
-	// "features": {},
+	// Features to add to the dev container. More info: https://containers.dev/features.
+	"features": {
+		"ghcr.io/devcontainers/features/git-lfs:1": {}
+	},
-	"postCreateCommand": "sudo apt update && sudo apt install -y git-lfs && git lfs install"
+	"postCreateCommand": "git lfs install --skip-repo"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/devcontainer.json around lines 8 - 10, The devcontainer.json
currently leaves "features" unused and installs Git LFS in postCreateCommand;
replace that by adding the Git LFS devcontainer feature instead of apt install
in postCreateCommand: populate the "features" object with
"ghcr.io/devcontainers/features/git-lfs:1": {} and remove the Git LFS install
steps from postCreateCommand (references: the "features" key and the
"postCreateCommand" entry) so provisioning is handled by the Dev Container
feature.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backends/spfc-target-p8/src/builders/writer.rs`:
- Around line 9-12: Sanitize process.family_name and process.manufacturer before
splicing into program_string and when building font_function: create a safe
comment string by stripping/normalizing newlines and removing any "--" sequences
and control chars (replace with spaces), create a safe identifier for
font_function by to_ascii_lowercase(), replacing any non [a-z0-9_] with "_" and
prefixing (e.g., "f_") if it would start with a digit, and create a safe quoted
string by escaping backslashes and quotes and replacing newlines with "\n"; then
use those sanitized values instead of raw
process.family_name/process.manufacturer when constructing program_string and
font_function (references: program_string, font_function, load_... function).
- Around line 15-30: The loop currently skips glyphs whose keys are not in
P8SCII (crate::utilities::get_character_by_symbol), leading to silent missing
characters; before allocating the buffer or writing (i.e., before the call that
uses last_character_index(&process.pixmap_pairs) and before filling buffer),
iterate process.pixmap_pairs and validate every key via get_character_by_symbol,
and if any validation returns None immediately return an Err (or propagate an
error) naming the unsupported symbol so the caller fails fast; only proceed to
buffer allocation and the existing write loop after all keys are confirmed
valid.

In `@backends/spfc-target-p8/src/lib.rs`:
- Around line 27-28: The code currently picks layout.font_tables.first() and
font_table.fonts.first() without validation; update the selection logic in
lib.rs to validate cardinality or accept an explicit selection option: check
layout.font_tables.len() and font_table.fonts.len(), and if >1 either return a
clear error (including counts and available identifiers) or use a provided
plugin/config option to choose the desired table/font (by index or name);
reference the symbols layout.font_tables, font_table.fonts, font_table, and font
when adding these checks and error messages so ambiguous inputs fail loudly
instead of silently selecting the first item.
- Around line 25-37: The current compile entrypoint unwraps I/O and parse
operations (std::fs::read, layout_from_data, font_table/font lookup,
create_program_string, std::fs::write) which will panic; replace each unwrap
with proper error handling that converts failures into a controlled
CompileResult (or the crate's error type) and returns early. Specifically,
validate the file read (options.input), layout_from_data(error), presence checks
on layout.font_tables.first() and font_table.fonts.first(), the
create_program_string result, and the write to options.output; map each failure
to a clear CompileResult error variant or message and propagate (use ? or
explicit map_err) instead of panicking so Process, create_pixmap_pairs, and
downstream callers never cause a panic.

In `@backends/spfc-target-p8/src/utilities/pixmap.rs`:
- Around line 28-42: The code currently unwraps and uses todo!() which panics
for malformed or unsupported pixmap inputs; replace the unwrap on bits_per_pixel
(from pixmap_table.constant_bits_per_pixel.or(pixmap.custom_bits_per_pixel))
with graceful handling that returns a clear diagnostic error or uses a safe
fallback (e.g., treat missing bits_per_pixel as 1 or return Err with a helpful
message), and replace the todo!() checks for character.advance_x vs width,
width/height > 8, and bits_per_pixel != 1 with non-panicking logic that emits
diagnostics (or logs) and applies the intended fallbacks: set advance_x = width
when unsupported, clamp or treat width and height > 8 as 8, and coerce
bits_per_pixel to 1 (opaque) when non-1; update the functions/structs
referencing pixmap_table, pixmap.custom_bits_per_pixel, character.advance_x,
width, height, and bits_per_pixel to propagate errors or diagnostics instead of
calling todo!()/unwrap().

---

Nitpick comments:
In @.devcontainer/devcontainer.json:
- Around line 8-10: The devcontainer.json currently leaves "features" unused and
installs Git LFS in postCreateCommand; replace that by adding the Git LFS
devcontainer feature instead of apt install in postCreateCommand: populate the
"features" object with "ghcr.io/devcontainers/features/git-lfs:1": {} and remove
the Git LFS install steps from postCreateCommand (references: the "features" key
and the "postCreateCommand" entry) so provisioning is handled by the Dev
Container feature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cffec29b-e34a-462d-8503-81ef360c037c

📥 Commits

Reviewing files that changed from the base of the PR and between 59299a1 and eec6f4c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .devcontainer/devcontainer.json
  • Cargo.toml
  • backends/spfc-target-p8/Cargo.toml
  • backends/spfc-target-p8/src/builders/mod.rs
  • backends/spfc-target-p8/src/builders/writer.rs
  • backends/spfc-target-p8/src/lib.rs
  • backends/spfc-target-p8/src/utilities/characters.rs
  • backends/spfc-target-p8/src/utilities/mod.rs
  • backends/spfc-target-p8/src/utilities/pixmap.rs
  • backends/spfc-target-ttf/Cargo.toml

Comment on lines +9 to +12
program_string += format!("-- {} v{}\n-- {}\n-- Generated by spfc\n-- https://github.com/SimplePixelFont/spfc\n\n", process.family_name, process.family_version, process.manufacturer).as_str();

let font_function = format!("load_{}", process.family_name.to_ascii_lowercase().replace(" ", "_"));
program_string += format!("-- Copy the function below\n-- into your own cart\nfunction {}()\n\t-- enable custom fonts\n\tpoke(0x5f58,0x81)\n\n\t-- add font to memory\n", font_function).as_str();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize font metadata before embedding it into Lua.

family_name and manufacturer are spliced into a comment, a function name, and a quoted string verbatim. Quotes, newlines, punctuation, or a leading digit can generate invalid Lua, and crafted metadata can inject extra code into the emitted cart.

Also applies to: 34-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backends/spfc-target-p8/src/builders/writer.rs` around lines 9 - 12, Sanitize
process.family_name and process.manufacturer before splicing into program_string
and when building font_function: create a safe comment string by
stripping/normalizing newlines and removing any "--" sequences and control chars
(replace with spaces), create a safe identifier for font_function by
to_ascii_lowercase(), replacing any non [a-z0-9_] with "_" and prefixing (e.g.,
"f_") if it would start with a digit, and create a safe quoted string by
escaping backslashes and quotes and replacing newlines with "\n"; then use those
sanitized values instead of raw process.family_name/process.manufacturer when
constructing program_string and font_function (references: program_string,
font_function, load_... function).

Comment on lines +15 to +30
let mut buffer = vec![0; (last_character_index(&process.pixmap_pairs) as usize + 1) * 8];
buffer[0] = max_width(&process.pixmap_pairs) + 1; //1 added for letter spacing
buffer[1] = max_width(&process.pixmap_pairs); // This will be added as a custom flag
buffer[2] = max_height(&process.pixmap_pairs) + 1; //1 added for line spacing
buffer[3] = 0; // This will be added as a custom flag offset_x
buffer[4] = 0; // This will be added as a custom flag offset_y

for (key, glyph) in &process.pixmap_pairs {
if let Some(p8_char) = crate::utilities::get_character_by_symbol(key) {
let char_index = p8_char.id as usize;
let bitmap_data = &glyph.bitmap;
for (i, byte) in bitmap_data.iter().enumerate() {
buffer[char_index * 8 + i] = *byte;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on unsupported glyph symbols instead of silently dropping them.

If a glyph key is not present in P8SCII_CHARSET, last_character_index(...) effectively ignores it and this loop just skips writing it. The result is a “successful” cart with missing characters and no signal to the caller. Please reject unsupported symbols before allocating/writing the buffer.

Suggested direction
+    let unsupported: Vec<_> = process
+        .pixmap_pairs
+        .keys()
+        .filter(|key| crate::utilities::get_character_by_symbol(key).is_none())
+        .cloned()
+        .collect();
+
+    if !unsupported.is_empty() {
+        anyhow::bail!(
+            "unsupported Pico-8 glyphs: {}",
+            unsupported.join(", ")
+        );
+    }
+
     let mut buffer = vec![0; (last_character_index(&process.pixmap_pairs) as usize + 1) * 8];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backends/spfc-target-p8/src/builders/writer.rs` around lines 15 - 30, The
loop currently skips glyphs whose keys are not in P8SCII
(crate::utilities::get_character_by_symbol), leading to silent missing
characters; before allocating the buffer or writing (i.e., before the call that
uses last_character_index(&process.pixmap_pairs) and before filling buffer),
iterate process.pixmap_pairs and validate every key via get_character_by_symbol,
and if any validation returns None immediately return an Err (or propagate an
error) naming the unsupported symbol so the caller fails fast; only proceed to
buffer allocation and the existing write loop after all keys are confirmed
valid.

Comment on lines +25 to +37
let data = std::fs::read(&options.input).unwrap();
let layout = layout_from_data(&data).unwrap();
let font_table = layout.font_tables.first().unwrap();
let font = font_table.fonts.first().unwrap();

let mut process = Process::default();
process.family_name = font.name.clone();
process.family_version = font.version as f64;
process.manufacturer = font.author.clone();
process.pixmap_pairs = create_pixmap_pairs(&layout);

let font_data = create_program_string(&process).unwrap();
std::fs::write(&options.output, &font_data).unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove panic paths from the exported compile entrypoint.

Line 25–Line 37 use unwrap() repeatedly on I/O, parse, lookup, generation, and write. Any bad input or FS failure will panic instead of returning a controlled CompileResult, which makes this backend brittle in host environments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backends/spfc-target-p8/src/lib.rs` around lines 25 - 37, The current compile
entrypoint unwraps I/O and parse operations (std::fs::read, layout_from_data,
font_table/font lookup, create_program_string, std::fs::write) which will panic;
replace each unwrap with proper error handling that converts failures into a
controlled CompileResult (or the crate's error type) and returns early.
Specifically, validate the file read (options.input), layout_from_data(error),
presence checks on layout.font_tables.first() and font_table.fonts.first(), the
create_program_string result, and the write to options.output; map each failure
to a clear CompileResult error variant or message and propagate (use ? or
explicit map_err) instead of panicking so Process, create_pixmap_pairs, and
downstream callers never cause a panic.

Comment on lines +27 to +28
let font_table = layout.font_tables.first().unwrap();
let font = font_table.fonts.first().unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid implicit “first font” selection.

Line 27 and Line 28 silently pick the first table/font. For inputs containing multiple fonts, this can compile the wrong asset with no signal. Please validate the expected cardinality (or add explicit selection via plugin options) and fail clearly when ambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backends/spfc-target-p8/src/lib.rs` around lines 27 - 28, The code currently
picks layout.font_tables.first() and font_table.fonts.first() without
validation; update the selection logic in lib.rs to validate cardinality or
accept an explicit selection option: check layout.font_tables.len() and
font_table.fonts.len(), and if >1 either return a clear error (including counts
and available identifiers) or use a provided plugin/config option to choose the
desired table/font (by index or name); reference the symbols layout.font_tables,
font_table.fonts, font_table, and font when adding these checks and error
messages so ambiguous inputs fail loudly instead of silently selecting the first
item.

Comment on lines +28 to +42
let bits_per_pixel = pixmap_table
.constant_bits_per_pixel
.or(pixmap.custom_bits_per_pixel)
.unwrap();

let advance_x = character.advance_x.unwrap_or(width);
if advance_x != width {
todo!("ABI 0.2.0: will add logging; On p8 target the advance_x is ignored because pico-8 doesn't support it.")
}
if width > 8 || height > 8 {
todo!("ABI 0.2.0: will add logging; On p8 target only 8x8 pixels are supported, Anything larger than that will be treated as 8x8.")
}
if bits_per_pixel != 1 {
todo!("ABI 0.2.0: will add logging; On p8 target only 1 bit per pixel is supported, Anything other than 0 will be treated as opaque.")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace the panicking placeholder paths before release.

This builder currently aborts on reachable input: Line 31 panics if bits-per-pixel is missing, and Lines 35, 38, and 41 panic on non-default metrics or unsupported glyph sizes. That makes unsupported or malformed fonts crash compilation instead of returning a diagnostic or applying the fallback the messages describe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backends/spfc-target-p8/src/utilities/pixmap.rs` around lines 28 - 42, The
code currently unwraps and uses todo!() which panics for malformed or
unsupported pixmap inputs; replace the unwrap on bits_per_pixel (from
pixmap_table.constant_bits_per_pixel.or(pixmap.custom_bits_per_pixel)) with
graceful handling that returns a clear diagnostic error or uses a safe fallback
(e.g., treat missing bits_per_pixel as 1 or return Err with a helpful message),
and replace the todo!() checks for character.advance_x vs width, width/height >
8, and bits_per_pixel != 1 with non-panicking logic that emits diagnostics (or
logs) and applies the intended fallbacks: set advance_x = width when
unsupported, clamp or treat width and height > 8 as 8, and coerce bits_per_pixel
to 1 (opaque) when non-1; update the functions/structs referencing pixmap_table,
pixmap.custom_bits_per_pixel, character.advance_x, width, height, and
bits_per_pixel to propagate errors or diagnostics instead of calling
todo!()/unwrap().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support P8CII compilation target

1 participant