EchoLab is a repository for retro machines emulation and experimentation.
# 1) One-time token setup (current shell)
export DROPBOX_ACCESS_TOKEN="your_token_here"
# 2) Daily push (git + Dropbox asset sync)
./push.sh --yes
# 3) Daily pull (git + Dropbox asset sync)
./pull.sh --yesPreview only (no changes):
./push.sh --dry-run
./pull.sh --dry-run- Minimal lab model and machine registry
- One machine descriptor: Apple IIe
- Deterministic fast RNG module for emulator workloads
- Testable screen buffer with explicit frame publish counter
- Text-mode video scanout (RAM -> phosphor-green-on-black buffer with every-other-scanline output, using rounded Apple IIe glyph ROM data with unique codes 0-255)
cargo runcargo run --example hello_textcargo run --example sdl3_text40x24 --features sdl3Requires SDL3 development libraries installed on your system.
Default text color is green; add -- --white to render white-on-black.
For frame-flip stress testing, add -- --flip-test to randomize all 40x24 chars to codes 0..15 each frame.
For black/white flip testing, add -- --bw-flip-test (full-frame toggle every frame, through persistence blend).
Add -- --fullscreen to start the SDL window in fullscreen.
Default sync is crossover timing: host display refresh (autodetected from SDL mode; measured from VSync presents if unavailable) with Apple IIe NTSC guest pacing (59.92Hz).
Default presentation also applies phosphor persistence using normalized blending (current + previous = 100% each frame).
Add -- --crossover-vsync-off to keep crossover timing but disable renderer VSync (--crossfade-vsync-off is kept as an alias).
Add -- --vsync-off for raw uncoupled timing.
Capture the last rendered frame before exit:
cargo run --example sdl3_text40x24 --features sdl3 -- --screenshotScreenshots are always named screenshot_<timestamp>.ppm.
Default output directory comes from echolab.toml:
default_screenshot_dir = "screenshots"Override output directory per run:
cargo run --example sdl3_text40x24 --features sdl3 -- --screenshot /tmp/echolab_shotsConfiguration lives in:
./echolab.tomlYou can override config path:
cargo run --example sdl3_text40x24 --features sdl3 -- --config /path/to/echolab.toml --screenshotExport the full glyph set (codes 0-255) to an editable 1:1 BMP:
python3 tools/charrom_export.py \
--rom assets/roms/retro_7x8_mono.bin \
--out assets/roms/retro_7x8_mono_edit.bmp \
--bank 0After editing that BMP, import it back into the ROM:
python3 tools/charrom_import.py \
--in assets/roms/retro_7x8_mono_edit.bmp \
--rom-in assets/roms/retro_7x8_mono.bin \
--rom-out assets/roms/retro_7x8_mono.bin \
--bank 0Import is strict black/white by default and fails if any pixel is not pure #000000 or #FFFFFF.
Use --no-strict-bw only when you intentionally want thresholded conversion.
./install.sh [--force]: install toolchain and platform dependencies../install_vscode.sh: install VS Code + Rust extensions../build.sh [--release]: build the project../run.sh [--release] [-- <args>]: run the binary../test.sh [--release]: run tests../check.sh [--no-lint]: run format check, compile check, and clippy by default../ci_local.sh [--release]: run local CI sequence (fmt, clippy, test, build)../clean.sh: remove build artifacts../push.sh [options]: generic push wrapper for git + Dropbox asset sync../pull.sh [options]: generic pull wrapper for git + Dropbox asset sync../backup_dropbox.sh [--dest DIR] [--whole-project] [--zip-overwrite] [--config FILE] [--list-only]: create local Dropbox backup archives; fails if git is not clean and excludes all git-tracked files../sync_to_dropbox.sh [--dest PATH] [--state-file FILE] [--config FILE] [--dry-run]: upload scheduled Dropbox files individually via Dropbox API using one shared local sync timestamp../sync_to_dropbox.sh --pull [--src PATH] [--dest DIR] [--state-file FILE] [--config FILE] [--dry-run]: pull Dropbox files recursively from Dropbox API by comparing remote timestamps to one shared local last-sync timestamp../sync_dropbox_push.sh [--dest PATH] [--config FILE] [--state-file FILE] [--dry-run]: direct Dropbox push script (same behavior assync_to_dropbox.shpush mode)../sync_dropbox_pull.sh [--src PATH] [--dest DIR] [--config FILE] [--dry-run]: pull Dropbox files recursively from Dropbox API and skip unchanged files using one shared local last-sync timestamp.
Enable local pre-commit secret scanning:
git config core.hooksPath .githooksInstall gitleaks locally (example on macOS):
brew install gitleaksCI secret scanning also runs on push and pull requests via GitHub Actions (.github/workflows/secret-scan.yml).
Create a Dropbox app/token in the Dropbox App Console, then set your token env var:
export DROPBOX_ACCESS_TOKEN="your_token_here"Persist it for future shells (zsh):
echo 'export DROPBOX_ACCESS_TOKEN="your_token_here"' >> ~/.zshrc
source ~/.zshrcQuick check:
echo "$DROPBOX_ACCESS_TOKEN"
./sync_to_dropbox.sh --dry-runIf token is missing, push.sh / pull.sh will warn and skip only the Dropbox step.
Create a backup archive of Dropbox assets locally:
./backup_dropbox.shShow only the files scheduled for backup (no archive written, with per-file sizes):
./backup_dropbox.sh --list-onlybackup_dropbox.sh safety rules:
- Requires a clean git working tree.
- Excludes every git-tracked path from backup output.
- Applies extra wildcard excludes from
[exclude]indropbox.toml. - Prints each archived file by default.
- Detects nested git repositories, excludes their
.git/folders, and writes a queue file at.backup_state/nested_git_repos.queue. - Default candidate paths include
archive/; control inclusion via[exclude]indropbox.toml. --list-onlyis grouped by per-project runs so nested git repos are evaluated separately.
Preview included paths without writing an archive:
./backup_dropbox.sh --dry-runUse a custom destination:
./backup_dropbox.sh --dest /path/to/backupsBackup the whole project folder (excluding .git/ and target/):
./backup_dropbox.sh --whole-projectCreate a single zip that always overwrites the previous one:
./backup_dropbox.sh --whole-project --zip-overwriteUse a custom config file:
./backup_dropbox.sh --whole-project --zip-overwrite --config /path/to/dropbox.tomlUpload scheduled Dropbox files individually (incremental, path-preserving):
./sync_to_dropbox.sh --dest /echolab_syncRun git push + Dropbox asset push together:
./push.sh --dropbox-path /echolab_sync --yespush.sh always previews Dropbox changes (upload: list) first, then prompts y/N unless --yes is set.
By default, push uses one shared local timestamp file:
.backup_state/dropbox_last_sync_timeOverride with--state-file /path/to/file. The timestamp is advanced from Dropbox upload metadata (server_modified) so pull comparisons align with remote clock.
Pull Dropbox files (remote-timestamp validated):
./sync_to_dropbox.sh --pull --src /echolab_sync --dest /Users/shane/Project/echolabRun git pull + Dropbox asset pull together:
./pull.sh --dropbox-path /echolab_sync --dropbox-dest /Users/shane/Project/echolab --yespull.sh always previews Dropbox changes (download: list) first, then prompts y/N unless --yes is set.
Both wrappers use --dropbox-path for the remote Dropbox path.
Pull default source now follows default_sync_dir in dropbox.toml (same default path used by push). If default_sync_dir is empty, pull falls back to /<sync_folder_name>.
Configure Dropbox sync defaults and token environment key in:
./dropbox.tomldefault_sync_dir is a Dropbox API folder path (example: /echolab_sync) and backup_folder_name controls the default local backup subfolder name when default_backup_dir is empty.
Example:
token_env = "DROPBOX_ACCESS_TOKEN"
default_sync_dir = "/echolab_sync"
sync_folder_name = "echolab_sync"
default_backup_dir = "/absolute/local/path/for/backups"
backup_folder_name = "echolab_backups"
[exclude]
env = ".env*"
pem = "*.pem"
keys = "*.key"src/lib.rs: library modules exported for app + testssrc/capture.rs: reusable screenshot CLI/capture flow for emulator frontendssrc/config.rs: typed config loader forecholab.tomlsrc/main.rs: CLI entry and outputsrc/lab.rs:Labmodel and machine listsrc/machines/: machine descriptorssrc/rng.rs: deterministicFastRngfrom benchmark logicsrc/screen_buffer.rs: emulator display buffer (u32pixels +frame_id) + PPM screenshot exportsrc/sdl_display_core.rs: reusable SDL display loop core (timing, persistence, capture, text scanout integration)src/timing.rs: reusable crossover timing and frame pacing helperssrc/postfx.rs: reusable post-processing (frame persistence blend)src/video/mod.rs: text-only video controller that renders RAM intoScreenBuffertests/capture.rs: reusable capture option/capture behavior teststests/config.rs: parser tests for config behaviortests/postfx.rs: persistence blend behavior and weighted-mix property teststests/rng_determinism.rs: integration tests for RNG behaviortests/screen_buffer.rs: integration tests for display buffer behaviortests/timing.rs: long-horizon crossover cadence/timing teststests/text_video.rs: integration tests for text scanout behaviorexamples/hello_text.rs: simple text-page hello-world render demoexamples/sdl3_text40x24.rs: SDL3 windowed 40x24 text display demoecholab.toml: default app config values (screenshot directory, auto-exit)dropbox.toml: Dropbox sync + local backup config (token env key, optional defaults, wildcard exclude list)tools/charrom_export.py: export ROM glyphs to editable BMP/PNGtools/charrom_import.py: import edited BMP/PNG back into ROM bytesarchive/: imported legacy projects kept for reference
- Add CPU state and step execution scaffolding.
- Introduce memory bus abstraction.
- Add deterministic trace-based tests.
Goal: base emulation behavior on documented hardware timing and signal interactions, while keeping modules testable and incremental.
Approach:
- Model observable hardware behavior first (bus transactions, scan timing, soft-switch side effects), not transistor-level internals.
- Implement each subsystem as a clocked state machine with explicit inputs/outputs per tick.
- Encode hardware contracts in tests before deep optimization.
Priority order:
- Bus and memory arbitration timing.
- Video scan timing and VBlank edge semantics.
- Keyboard and input strobe/read-clear behavior.
- Audio and timer/interrupt sequencing.
- Storage/card timing once core timing is stable.
Validation strategy:
- Compare against ROM routines and deterministic traces.
- Add subsystem tests that assert cycle-level side effects at MMIO boundaries.
- Keep one reference timing table per subsystem in docs and update it with implementation changes.
This project is licensed under the GNU General Public License v3.0. See LICENSE.
| Range | Purpose |
|---|---|
0x0000-0x00FF |
ZERO PAGE RAM |
0x0100-0x01FF |
CPU STACK RAM |
0x0200-0xBFFF |
RAM |
0xC000-0xDFFF |
ROM |
0xE000-0xE0FF |
MMIO |
0xE100-0xFFFF |
monitor/firmware ROM + vectors (or flip MMIO/ROM order) |
| Offset | Name | Notes |
|---|---|---|
+0x00/+0x01 |
FRM_BASE_LO/HI |
HI = 0x00 turns off display |
+0x02/+0x03 |
VPT_BASE_LO/HI |
HI = 0x00 turns off viewport |
+0x04 |
VPT_COLS |
viewport width |
+0x05 |
VPT_ROWS |
viewport height |
+0x06 |
VPT_COL_OFFSET_CHAR |
viewport scroll column offset |
+0x07 |
VPT_ROW_OFFSET_CHAR |
viewport row offset (256-byte boundary) |
+0x08 |
VPT_X_OFFSET_PX |
0x00-0x06 |
+0x09 |
VPT_Y_OFFSET_PX |
0x00-0x07 |
+0x0A |
VBL_SYNC |
write: apply frame/viewport settings at blanking period; read: bit0=in_vblank, bit1=write_pending |
+0x0B |
SWITCH_80_COL |
write: bit0 turns 80-col mode on, bit1 turns it off; read: bit0=1 when 80-col mode is on, 0 when off |
| Range | Block | Initial Purpose |
|---|---|---|
0xE000-0xE01F |
VIDEO |
frame/viewport control, VBlank sync/status, 80-col switch |
0xE020-0xE02F |
INPUT |
keyboard data/status and basic input flags |
0xE030-0xE03F |
AUDIO |
simple tone/noise frequency, volume, gate/control |
0xE040-0xE05F |
TIMER_IRQ |
free-running timer, compare registers, IRQ enable/status/ack |
0xE060-0xE07F |
SERIAL_DEBUG |
TX/RX data/status and optional debug output port |
0xE080-0xE09F |
STORAGE |
virtual storage command/status/data stub for future expansion |
Guidelines:
- Keep read side effects and write side effects explicit per register.
- Separate status registers (read-heavy) from command registers (write-heavy).
- Use predictable reset values and document them.
- Use one IRQ status + one IRQ acknowledge path per block.