Skip to content

ethereal-esthesia/echolab

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

150 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

EchoLab

EchoLab is a repository for retro machines emulation and experimentation.

Workflow Quickstart

# 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 --yes

Preview only (no changes):

./push.sh --dry-run
./pull.sh --dry-run

Current Scope

  • 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)

Run

cargo run

Demo: Text Hello

cargo run --example hello_text

Demo: SDL3 Text 40x24

cargo run --example sdl3_text40x24 --features sdl3

Requires 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 -- --screenshot

Screenshots 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_shots

Configuration lives in:

./echolab.toml

You can override config path:

cargo run --example sdl3_text40x24 --features sdl3 -- --config /path/to/echolab.toml --screenshot

Edit Text ROM Glyphs

Export 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 0

After 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 0

Import 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.

Scripts

  • ./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 as sync_to_dropbox.sh push 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.

Secret Scanning

Enable local pre-commit secret scanning:

git config core.hooksPath .githooks

Install gitleaks locally (example on macOS):

brew install gitleaks

CI secret scanning also runs on push and pull requests via GitHub Actions (.github/workflows/secret-scan.yml).

Dropbox API Setup

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 ~/.zshrc

Quick check:

echo "$DROPBOX_ACCESS_TOKEN"
./sync_to_dropbox.sh --dry-run

If token is missing, push.sh / pull.sh will warn and skip only the Dropbox step.

Backup Dropbox Assets

Create a backup archive of Dropbox assets locally:

./backup_dropbox.sh

Show only the files scheduled for backup (no archive written, with per-file sizes):

./backup_dropbox.sh --list-only

backup_dropbox.sh safety rules:

  • Requires a clean git working tree.
  • Excludes every git-tracked path from backup output.
  • Applies extra wildcard excludes from [exclude] in dropbox.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] in dropbox.toml.
  • --list-only is grouped by per-project runs so nested git repos are evaluated separately.

Preview included paths without writing an archive:

./backup_dropbox.sh --dry-run

Use a custom destination:

./backup_dropbox.sh --dest /path/to/backups

Backup the whole project folder (excluding .git/ and target/):

./backup_dropbox.sh --whole-project

Create a single zip that always overwrites the previous one:

./backup_dropbox.sh --whole-project --zip-overwrite

Use a custom config file:

./backup_dropbox.sh --whole-project --zip-overwrite --config /path/to/dropbox.toml

Upload scheduled Dropbox files individually (incremental, path-preserving):

./sync_to_dropbox.sh --dest /echolab_sync

Run git push + Dropbox asset push together:

./push.sh --dropbox-path /echolab_sync --yes

push.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_time Override 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/echolab

Run git pull + Dropbox asset pull together:

./pull.sh --dropbox-path /echolab_sync --dropbox-dest /Users/shane/Project/echolab --yes

pull.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.toml

default_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"

Project Layout

  • src/lib.rs: library modules exported for app + tests
  • src/capture.rs: reusable screenshot CLI/capture flow for emulator frontends
  • src/config.rs: typed config loader for echolab.toml
  • src/main.rs: CLI entry and output
  • src/lab.rs: Lab model and machine list
  • src/machines/: machine descriptors
  • src/rng.rs: deterministic FastRng from benchmark logic
  • src/screen_buffer.rs: emulator display buffer (u32 pixels + frame_id) + PPM screenshot export
  • src/sdl_display_core.rs: reusable SDL display loop core (timing, persistence, capture, text scanout integration)
  • src/timing.rs: reusable crossover timing and frame pacing helpers
  • src/postfx.rs: reusable post-processing (frame persistence blend)
  • src/video/mod.rs: text-only video controller that renders RAM into ScreenBuffer
  • tests/capture.rs: reusable capture option/capture behavior tests
  • tests/config.rs: parser tests for config behavior
  • tests/postfx.rs: persistence blend behavior and weighted-mix property tests
  • tests/rng_determinism.rs: integration tests for RNG behavior
  • tests/screen_buffer.rs: integration tests for display buffer behavior
  • tests/timing.rs: long-horizon crossover cadence/timing tests
  • tests/text_video.rs: integration tests for text scanout behavior
  • examples/hello_text.rs: simple text-page hello-world render demo
  • examples/sdl3_text40x24.rs: SDL3 windowed 40x24 text display demo
  • echolab.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/PNG
  • tools/charrom_import.py: import edited BMP/PNG back into ROM bytes
  • archive/: imported legacy projects kept for reference

Near-Term Plan

  1. Add CPU state and step execution scaffolding.
  2. Introduce memory bus abstraction.
  3. Add deterministic trace-based tests.

Hardware-Faithful Emulation Plan

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:

  1. Bus and memory arbitration timing.
  2. Video scan timing and VBlank edge semantics.
  3. Keyboard and input strobe/read-clear behavior.
  4. Audio and timer/interrupt sequencing.
  5. 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.

License

This project is licensed under the GNU General Public License v3.0. See LICENSE.

Memory Map Target

Address Space

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)

MMIO Registers (0xE000-0xE0FF)

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

MMIO I/O Block Plan

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.

About

EchoLab is a repository for legacy machine emulation and retro design experimentation. The foremost goal is accuracy and detail, with sharp focus on performance.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors