{"id":48056055,"url":"https://github.com/ethereal-esthesia/echolab","last_synced_at":"2026-04-04T14:27:50.925Z","repository":{"id":339086965,"uuid":"1159595478","full_name":"ethereal-esthesia/echolab","owner":"ethereal-esthesia","description":"EchoLab is a repository for legacy machine emulation and retro design experimentation. The foremost goal is accuracy and detail, with sharp focus on performance.","archived":false,"fork":false,"pushed_at":"2026-03-14T17:09:45.000Z","size":255,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-15T03:43:59.604Z","etag":null,"topics":["6502-emulation","apple-iie","emulation","retrocomputer","sdl3"],"latest_commit_sha":null,"homepage":"https://ethereal-esthesia.com","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ethereal-esthesia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":"SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-16T23:11:25.000Z","updated_at":"2026-03-14T17:09:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ethereal-esthesia/echolab","commit_stats":null,"previous_names":["ethereal-esthesia/echolab"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ethereal-esthesia/echolab","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethereal-esthesia%2Fecholab","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethereal-esthesia%2Fecholab/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethereal-esthesia%2Fecholab/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethereal-esthesia%2Fecholab/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ethereal-esthesia","download_url":"https://codeload.github.com/ethereal-esthesia/echolab/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ethereal-esthesia%2Fecholab/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31402467,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["6502-emulation","apple-iie","emulation","retrocomputer","sdl3"],"created_at":"2026-04-04T14:27:50.799Z","updated_at":"2026-04-04T14:27:50.906Z","avatar_url":"https://github.com/ethereal-esthesia.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EchoLab\n\nEchoLab is a repository for retro machines emulation and experimentation.\n\n## Workflow Quickstart\n\n```bash\n# 1) One-time token setup (current shell)\nexport DROPBOX_ACCESS_TOKEN=\"your_token_here\"\n\n# 2) Daily push (git + Dropbox asset sync)\n./push.sh --yes\n\n# 3) Daily pull (git + Dropbox asset sync)\n./pull.sh --yes\n```\n\nPreview only (no changes):\n\n```bash\n./push.sh --dry-run\n./pull.sh --dry-run\n```\n\n## Current Scope\n\n- Minimal lab model and machine registry\n- One machine descriptor: Apple IIe\n- Deterministic fast RNG module for emulator workloads\n- Testable screen buffer with explicit frame publish counter\n- Text-mode video scanout (RAM -\u003e phosphor-green-on-black buffer with every-other-scanline output, using rounded Apple IIe glyph ROM data with unique codes 0-255)\n\n## Run\n\n```bash\ncargo run\n```\n\n## Demo: Text Hello\n\n```bash\ncargo run --example hello_text\n```\n\n## Demo: SDL3 Text 40x24\n\n```bash\ncargo run --example sdl3_text40x24 --features sdl3\n```\n\nRequires SDL3 development libraries installed on your system.\nDefault text color is green; add `-- --white` to render white-on-black.\nFor frame-flip stress testing, add `-- --flip-test` to randomize all 40x24 chars to codes `0..15` each frame.\nFor black/white flip testing, add `-- --bw-flip-test` (full-frame toggle every frame, through persistence blend).\nAdd `-- --fullscreen` to start the SDL window in fullscreen.\nDefault 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`).\nDefault presentation also applies phosphor persistence using normalized blending (`current + previous = 100%` each frame).\nAdd `-- --crossover-vsync-off` to keep crossover timing but disable renderer VSync (`--crossfade-vsync-off` is kept as an alias).\nAdd `-- --vsync-off` for raw uncoupled timing.\n\nCapture the last rendered frame before exit:\n\n```bash\ncargo run --example sdl3_text40x24 --features sdl3 -- --screenshot\n```\n\nScreenshots are always named `screenshot_\u003ctimestamp\u003e.ppm`.\nDefault output directory comes from `echolab.toml`:\n\n```bash\ndefault_screenshot_dir = \"screenshots\"\n```\n\nOverride output directory per run:\n\n```bash\ncargo run --example sdl3_text40x24 --features sdl3 -- --screenshot /tmp/echolab_shots\n```\n\nConfiguration lives in:\n\n```bash\n./echolab.toml\n```\n\nYou can override config path:\n\n```bash\ncargo run --example sdl3_text40x24 --features sdl3 -- --config /path/to/echolab.toml --screenshot\n```\n\n## Edit Text ROM Glyphs\n\nExport the full glyph set (codes 0-255) to an editable 1:1 BMP:\n\n```bash\npython3 tools/charrom_export.py \\\n  --rom assets/roms/retro_7x8_mono.bin \\\n  --out assets/roms/retro_7x8_mono_edit.bmp \\\n  --bank 0\n```\n\nAfter editing that BMP, import it back into the ROM:\n\n```bash\npython3 tools/charrom_import.py \\\n  --in assets/roms/retro_7x8_mono_edit.bmp \\\n  --rom-in assets/roms/retro_7x8_mono.bin \\\n  --rom-out assets/roms/retro_7x8_mono.bin \\\n  --bank 0\n```\n\nImport is strict black/white by default and fails if any pixel is not pure `#000000` or `#FFFFFF`.\nUse `--no-strict-bw` only when you intentionally want thresholded conversion.\n\n## Scripts\n\n- `./install.sh [--force]`: install toolchain and platform dependencies.\n- `./install_vscode.sh`: install VS Code + Rust extensions.\n- `./build.sh [--release]`: build the project.\n- `./run.sh [--release] [-- \u003cargs\u003e]`: run the binary.\n- `./test.sh [--release]`: run tests.\n- `./check.sh [--no-lint]`: run format check, compile check, and clippy by default.\n- `./ci_local.sh [--release]`: run local CI sequence (fmt, clippy, test, build).\n- `./clean.sh`: remove build artifacts.\n- `./push.sh [options]`: generic push wrapper for git + Dropbox asset sync.\n- `./pull.sh [options]`: generic pull wrapper for git + Dropbox asset sync.\n- `./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.\n- `./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.\n- `./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.\n- `./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).\n- `./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.\n\n## Secret Scanning\n\nEnable local pre-commit secret scanning:\n\n```bash\ngit config core.hooksPath .githooks\n```\n\nInstall gitleaks locally (example on macOS):\n\n```bash\nbrew install gitleaks\n```\n\nCI secret scanning also runs on push and pull requests via GitHub Actions (`.github/workflows/secret-scan.yml`).\n\n## Dropbox API Setup\n\nCreate a Dropbox app/token in the Dropbox App Console, then set your token env var:\n\n```bash\nexport DROPBOX_ACCESS_TOKEN=\"your_token_here\"\n```\n\nPersist it for future shells (`zsh`):\n\n```bash\necho 'export DROPBOX_ACCESS_TOKEN=\"your_token_here\"' \u003e\u003e ~/.zshrc\nsource ~/.zshrc\n```\n\nQuick check:\n\n```bash\necho \"$DROPBOX_ACCESS_TOKEN\"\n./sync_to_dropbox.sh --dry-run\n```\n\nIf token is missing, `push.sh` / `pull.sh` will warn and skip only the Dropbox step.\n\n## Backup Dropbox Assets\n\nCreate a backup archive of Dropbox assets locally:\n\n```bash\n./backup_dropbox.sh\n```\n\nShow only the files scheduled for backup (no archive written, with per-file sizes):\n\n```bash\n./backup_dropbox.sh --list-only\n```\n\n`backup_dropbox.sh` safety rules:\n- Requires a clean git working tree.\n- Excludes every git-tracked path from backup output.\n- Applies extra wildcard excludes from `[exclude]` in `dropbox.toml`.\n- Prints each archived file by default.\n- Detects nested git repositories, excludes their `.git/` folders, and writes a queue file at `.backup_state/nested_git_repos.queue`.\n- Default candidate paths include `archive/`; control inclusion via `[exclude]` in `dropbox.toml`.\n- `--list-only` is grouped by per-project runs so nested git repos are evaluated separately.\n\nPreview included paths without writing an archive:\n\n```bash\n./backup_dropbox.sh --dry-run\n```\n\nUse a custom destination:\n\n```bash\n./backup_dropbox.sh --dest /path/to/backups\n```\n\nBackup the whole project folder (excluding `.git/` and `target/`):\n\n```bash\n./backup_dropbox.sh --whole-project\n```\n\nCreate a single zip that always overwrites the previous one:\n\n```bash\n./backup_dropbox.sh --whole-project --zip-overwrite\n```\n\nUse a custom config file:\n\n```bash\n./backup_dropbox.sh --whole-project --zip-overwrite --config /path/to/dropbox.toml\n```\n\nUpload scheduled Dropbox files individually (incremental, path-preserving):\n\n```bash\n./sync_to_dropbox.sh --dest /echolab_sync\n```\n\nRun git push + Dropbox asset push together:\n\n```bash\n./push.sh --dropbox-path /echolab_sync --yes\n```\n\n`push.sh` always previews Dropbox changes (`upload:` list) first, then prompts `y/N` unless `--yes` is set.\n\nBy default, push uses one shared local timestamp file:\n- `.backup_state/dropbox_last_sync_time`\nOverride with `--state-file /path/to/file`.\nThe timestamp is advanced from Dropbox upload metadata (`server_modified`) so pull comparisons align with remote clock.\n\nPull Dropbox files (remote-timestamp validated):\n\n```bash\n./sync_to_dropbox.sh --pull --src /echolab_sync --dest /Users/shane/Project/echolab\n```\n\nRun git pull + Dropbox asset pull together:\n\n```bash\n./pull.sh --dropbox-path /echolab_sync --dropbox-dest /Users/shane/Project/echolab --yes\n```\n\n`pull.sh` always previews Dropbox changes (`download:` list) first, then prompts `y/N` unless `--yes` is set.\nBoth wrappers use `--dropbox-path` for the remote Dropbox path.\n\nPull 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 `/\u003csync_folder_name\u003e`.\n\nConfigure Dropbox sync defaults and token environment key in:\n\n```bash\n./dropbox.toml\n```\n\n`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.\n\nExample:\n\n```toml\ntoken_env = \"DROPBOX_ACCESS_TOKEN\"\ndefault_sync_dir = \"/echolab_sync\"\nsync_folder_name = \"echolab_sync\"\ndefault_backup_dir = \"/absolute/local/path/for/backups\"\nbackup_folder_name = \"echolab_backups\"\n\n[exclude]\nenv = \".env*\"\npem = \"*.pem\"\nkeys = \"*.key\"\n```\n\n## Project Layout\n\n- `src/lib.rs`: library modules exported for app + tests\n- `src/capture.rs`: reusable screenshot CLI/capture flow for emulator frontends\n- `src/config.rs`: typed config loader for `echolab.toml`\n- `src/main.rs`: CLI entry and output\n- `src/lab.rs`: `Lab` model and machine list\n- `src/machines/`: machine descriptors\n- `src/rng.rs`: deterministic `FastRng` from benchmark logic\n- `src/screen_buffer.rs`: emulator display buffer (`u32` pixels + `frame_id`) + PPM screenshot export\n- `src/sdl_display_core.rs`: reusable SDL display loop core (timing, persistence, capture, text scanout integration)\n- `src/timing.rs`: reusable crossover timing and frame pacing helpers\n- `src/postfx.rs`: reusable post-processing (frame persistence blend)\n- `src/video/mod.rs`: text-only video controller that renders RAM into `ScreenBuffer`\n- `tests/capture.rs`: reusable capture option/capture behavior tests\n- `tests/config.rs`: parser tests for config behavior\n- `tests/postfx.rs`: persistence blend behavior and weighted-mix property tests\n- `tests/rng_determinism.rs`: integration tests for RNG behavior\n- `tests/screen_buffer.rs`: integration tests for display buffer behavior\n- `tests/timing.rs`: long-horizon crossover cadence/timing tests\n- `tests/text_video.rs`: integration tests for text scanout behavior\n- `examples/hello_text.rs`: simple text-page hello-world render demo\n- `examples/sdl3_text40x24.rs`: SDL3 windowed 40x24 text display demo\n- `echolab.toml`: default app config values (screenshot directory, auto-exit)\n- `dropbox.toml`: Dropbox sync + local backup config (token env key, optional defaults, wildcard exclude list)\n- `tools/charrom_export.py`: export ROM glyphs to editable BMP/PNG\n- `tools/charrom_import.py`: import edited BMP/PNG back into ROM bytes\n- `archive/`: imported legacy projects kept for reference\n\n## Near-Term Plan\n\n1. Add CPU state and step execution scaffolding.\n2. Introduce memory bus abstraction.\n3. Add deterministic trace-based tests.\n\n## Hardware-Faithful Emulation Plan\n\nGoal: base emulation behavior on documented hardware timing and signal interactions, while keeping modules testable and incremental.\n\nApproach:\n- Model observable hardware behavior first (bus transactions, scan timing, soft-switch side effects), not transistor-level internals.\n- Implement each subsystem as a clocked state machine with explicit inputs/outputs per tick.\n- Encode hardware contracts in tests before deep optimization.\n\nPriority order:\n1. Bus and memory arbitration timing.\n2. Video scan timing and VBlank edge semantics.\n3. Keyboard and input strobe/read-clear behavior.\n4. Audio and timer/interrupt sequencing.\n5. Storage/card timing once core timing is stable.\n\nValidation strategy:\n- Compare against ROM routines and deterministic traces.\n- Add subsystem tests that assert cycle-level side effects at MMIO boundaries.\n- Keep one reference timing table per subsystem in docs and update it with implementation changes.\n\n## License\n\nThis project is licensed under the GNU General Public License v3.0. See `LICENSE`.\n\n## Memory Map Target\n\n### Address Space\n\n| Range | Purpose |\n|---|---|\n| `0x0000-0x00FF` | ZERO PAGE RAM |\n| `0x0100-0x01FF` | CPU STACK RAM |\n| `0x0200-0xBFFF` | RAM |\n| `0xC000-0xDFFF` | ROM |\n| `0xE000-0xE0FF` | MMIO |\n| `0xE100-0xFFFF` | monitor/firmware ROM + vectors (or flip MMIO/ROM order) |\n\n### MMIO Registers (`0xE000-0xE0FF`)\n\n| Offset | Name | Notes |\n|---|---|---|\n| `+0x00/+0x01` | `FRM_BASE_LO/HI` | `HI = 0x00` turns off display |\n| `+0x02/+0x03` | `VPT_BASE_LO/HI` | `HI = 0x00` turns off viewport |\n| `+0x04` | `VPT_COLS` | viewport width |\n| `+0x05` | `VPT_ROWS` | viewport height |\n| `+0x06` | `VPT_COL_OFFSET_CHAR` | viewport scroll column offset |\n| `+0x07` | `VPT_ROW_OFFSET_CHAR` | viewport row offset (256-byte boundary) |\n| `+0x08` | `VPT_X_OFFSET_PX` | `0x00-0x06` |\n| `+0x09` | `VPT_Y_OFFSET_PX` | `0x00-0x07` |\n| `+0x0A` | `VBL_SYNC` | write: apply frame/viewport settings at blanking period; read: bit0=`in_vblank`, bit1=`write_pending` |\n| `+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 |\n\n### MMIO I/O Block Plan\n\n| Range | Block | Initial Purpose |\n|---|---|---|\n| `0xE000-0xE01F` | `VIDEO` | frame/viewport control, VBlank sync/status, 80-col switch |\n| `0xE020-0xE02F` | `INPUT` | keyboard data/status and basic input flags |\n| `0xE030-0xE03F` | `AUDIO` | simple tone/noise frequency, volume, gate/control |\n| `0xE040-0xE05F` | `TIMER_IRQ` | free-running timer, compare registers, IRQ enable/status/ack |\n| `0xE060-0xE07F` | `SERIAL_DEBUG` | TX/RX data/status and optional debug output port |\n| `0xE080-0xE09F` | `STORAGE` | virtual storage command/status/data stub for future expansion |\n\nGuidelines:\n- Keep read side effects and write side effects explicit per register.\n- Separate status registers (read-heavy) from command registers (write-heavy).\n- Use predictable reset values and document them.\n- Use one IRQ status + one IRQ acknowledge path per block.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fethereal-esthesia%2Fecholab","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fethereal-esthesia%2Fecholab","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fethereal-esthesia%2Fecholab/lists"}