Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Pre-commit hooks provide fast feedback; `prek` is a drop-in replacement that rea
- Configured hooks:
- Pre-commit: `trailing-whitespace`, `end-of-file-fixer`, `check-merge-conflict`, `check-yaml`, `check-toml`, `check-json`, `check-added-large-files`, `detect-private-key`, `check-executables-have-shebangs`, `check-symlinks`, `check-case-conflict`, `cargo fmt --check`.
- Pre-push: `cargo clippy --workspace --all-targets --all-features -D warnings`, `cargo test --workspace`.
- If a pre-push hook fails, fix the reported changes, rerun the full test suite, then commit and push again after the hook clears.

## Commit & Pull Request Guidelines
No established commit conventions are present yet. Until standards are set:
Expand All @@ -59,3 +60,17 @@ No established commit conventions are present yet. Until standards are set:
- Do not add proprietary binaries, keys, or assets to the repository.
- Always test changes, update relevant documentation, and commit all code you modify or add.
- Push all commits after creating them.

## Subagent Conflict Resolution
When using subagents, apply this workflow to keep ownership and diffs clear.

1) Record a clean baseline before spawning subagents: `git status -sb` and `git diff --stat`.
2) Assign each subagent a strict file or directory scope.
3) After each subagent finishes, compare changes to the baseline and declared scope.
4) If unexpected changes appear:
- Stop all subagents.
- Inspect the diff (`git diff --name-status` + `git diff` for unexpected files).
- Decide to accept, revert, or move the changes into a separate commit.
5) Do not mix subagent outputs across scopes in a single commit.
6) If hooks modify files on commit/push, rerun the hook targets, re-stage, re-commit, then push again.
7) Only push when `git status -sb` shows a clean tree and hooks are green.
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 103 additions & 0 deletions PLANS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ This file tracks implementation work derived from specs that do not yet have a c
- SPEC-096 Bundle Manifest Integrity
- SPEC-100 Validation and Acceptance
- SPEC-110 Target Title Selection Criteria
- SPEC-120 Homebrew Candidate Intake
- SPEC-130 Homebrew Module Extraction
- SPEC-140 Homebrew Runtime Surface
- SPEC-150 Homebrew Asset Packaging
- SPEC-160 AArch64 Decode Coverage
- SPEC-170 Function Discovery and Control-Flow Graph

## SPEC-000: Project Charter and Ethics
Outcome
Expand Down Expand Up @@ -120,3 +126,100 @@ Work items
Exit criteria (from SPEC-110)
- A documented selection that satisfies all checklist items.
- A published plan for obtaining inputs legally and privately.

## SPEC-120: Homebrew Candidate Intake
Outcome
- Accept a legally distributable homebrew candidate and emit a deterministic intake manifest.

Work items
- [x] Define a module intake manifest schema for NRO + optional NSO inputs.
- [x] Implement NRO intake parsing for header fields and asset section offsets.
- [x] Add provenance validation checks for homebrew inputs (reject proprietary or encrypted formats).
- [x] Emit deterministic `module.json` and `manifest.json` with hashes, sizes, and tool versions.
- [x] Add sample intake tests using non-proprietary NRO fixtures.

Exit criteria (from SPEC-120)
- A homebrew NRO can be ingested with hashes, build id, and asset offsets recorded.
- Asset extraction is recorded without mixing assets into code output.
- Intake errors are explicit when required fields are missing or unsupported.

## SPEC-130: Homebrew Module Extraction
Outcome
- Normalize NRO/NSO binaries into module.json and extracted segment blobs.

Work items
- [x] Implement NSO parsing including LZ4 segment decompression.
- [x] Capture build id/module id and preserve section boundaries in module.json.
- [x] Preserve relocation and symbol metadata when present.
- [x] Ensure extraction is deterministic across runs.
- [x] Add tests for NRO-only and NRO + NSO ingestion paths.

Exit criteria (from SPEC-130)
- NRO and NSO inputs yield module.json with correct segment sizes and build id.
- Compressed NSO segments are decompressed and emitted deterministically.
- Section boundaries are preserved for later translation.

## SPEC-140: Homebrew Runtime Surface
Outcome
- Provide a minimal runtime ABI surface that can boot a recompiled homebrew title.

Work items
- [x] Implement homebrew entrypoint shim with loader config setup.
- [x] Define loader config keys and defaults (EndOfList, MainThreadHandle, AppletType).
- [x] Add runtime manifest that enumerates provided config keys and stubbed services.
- [x] Implement deterministic time and input stubs for validation runs.
- [x] Add logging for unsupported service calls with explicit failure behavior.

Exit criteria (from SPEC-140)
- Recompiled binaries boot with required loader config keys present.
- Unsupported services fail with explicit, logged errors.
- Runtime manifest records provided loader config keys.

## SPEC-150: Homebrew Asset Packaging
Outcome
- Extract NRO asset section contents and package them alongside recompiled output.

Work items
- [x] Implement asset section extraction (icon, NACP, RomFS).
- [x] Validate and store NACP as `control.nacp` with expected size.
- [x] Emit deterministic asset output directory and hashes in manifest.json.
- [x] Document runtime RomFS mount expectations.
- [x] Add tests for asset extraction and manifest hashes.

Exit criteria (from SPEC-150)
- Icon, NACP, and RomFS assets are extracted deterministically when present.
- Asset hashes in manifest.json match extracted bytes.
- Code output remains separate from extracted assets.

## SPEC-160: AArch64 Decode Coverage
Outcome
- Expand decode coverage and IR support to lift real homebrew code paths.

Work items
- [x] Extend the lifted IR schema with arithmetic, logical, shift, memory, and branch ops.
- [x] Add decoder support for MOV (ORR alias), SUB, AND/OR/XOR, ADR/ADRP, LDR/STR, and branch opcodes listed in SPEC-160.
- [x] Map 32-bit W-register operations to zero-extended 64-bit IR semantics.
- [x] Add per-op unit tests that validate opcode decoding and emitted IR structure.
- [x] Add decode-limit enforcement tests for oversized text segments.

Exit criteria (from SPEC-160)
- A synthetic instruction stream containing Phase 1 opcodes lifts without errors.
- Unsupported opcodes report the PC and opcode value.
- Tests confirm 32-bit variants are zero-extended.
- Loads/stores emit correctly typed IR ops with aligned access checks.

## SPEC-170: Function Discovery and Control-Flow Graph
Outcome
- Replace linear decoding with basic blocks and deterministic control-flow graphs.

Work items
- [x] Extend the lifted module schema to allow block-based functions alongside legacy linear ops.
- [x] Implement a sorted worklist decoder that builds blocks and edges deterministically.
- [x] Add control-flow terminators for unconditional, conditional, call, and indirect branches.
- [x] Seed function discovery from entrypoint and direct call targets.
- [x] Add tests for if/else blocks, direct call discovery, and unresolved indirect branches.

Exit criteria (from SPEC-170)
- A synthetic binary with a conditional branch yields at least two blocks and correct edges.
- Direct call targets are discovered and lifted as separate functions.
- The lifted module is deterministic when run twice on the same input.
4 changes: 4 additions & 0 deletions RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Needed research:
- Nintendo Switch platform baseline: https://en.wikipedia.org/wiki/Nintendo_Switch
- Tegra X1 whitepaper: https://www.nvidia.com/content/tegra/embedded-systems/pdf/tegra-x1-whitepaper.pdf
- Switch hardware overview: https://switchbrew.org/wiki/Hardware
- Switch homebrew NRO format: https://switchbrew.org/wiki/NRO
- Switch NSO format and compression: https://switchbrew.org/wiki/NSO
- Homebrew ABI entrypoint and loader config: https://switchbrew.org/wiki/Homebrew_ABI
- NACP title metadata format: https://switchbrew.org/wiki/NACP

## Research Deliverables
- A research summary for each category with sources.
Expand Down
96 changes: 95 additions & 1 deletion crates/recomp-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};
use recomp_pipeline::bundle::{package_bundle, PackageOptions};
use recomp_pipeline::homebrew::{
intake_homebrew, lift_homebrew, IntakeOptions, LiftMode, LiftOptions,
};
use recomp_pipeline::{run_pipeline, PipelineOptions};
use std::path::PathBuf;

Expand All @@ -14,6 +17,8 @@ struct Args {
enum Command {
Run(RunArgs),
Package(PackageArgs),
HomebrewIntake(HomebrewIntakeArgs),
HomebrewLift(HomebrewLiftArgs),
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -42,6 +47,45 @@ struct PackageArgs {
assets_dir: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct HomebrewIntakeArgs {
#[arg(long)]
module: PathBuf,
#[arg(long)]
nso: Vec<PathBuf>,
#[arg(long)]
provenance: PathBuf,
#[arg(long)]
out_dir: PathBuf,
}

#[derive(Parser, Debug)]
struct HomebrewLiftArgs {
#[arg(long)]
module_json: PathBuf,
#[arg(long)]
out_dir: PathBuf,
#[arg(long, default_value = "entry")]
entry: String,
#[arg(long, value_enum, default_value = "decode")]
mode: HomebrewLiftMode,
}

#[derive(ValueEnum, Debug, Clone)]
enum HomebrewLiftMode {
Stub,
Decode,
}

impl From<HomebrewLiftMode> for LiftMode {
fn from(value: HomebrewLiftMode) -> Self {
match value {
HomebrewLiftMode::Stub => LiftMode::Stub,
HomebrewLiftMode::Decode => LiftMode::Decode,
}
}
}

fn main() {
let args = Args::parse();

Expand Down Expand Up @@ -100,5 +144,55 @@ fn main() {
}
}
}
Command::HomebrewIntake(intake) => {
let options = IntakeOptions {
module_path: intake.module,
nso_paths: intake.nso,
provenance_path: intake.provenance,
out_dir: intake.out_dir,
};
match intake_homebrew(options) {
Ok(report) => {
println!(
"Homebrew intake wrote {} files to {}",
report.files_written.len(),
report.out_dir.display()
);
println!("module.json: {}", report.module_json_path.display());
println!("manifest.json: {}", report.manifest_path.display());
}
Err(err) => {
eprintln!("Homebrew intake error: {err}");
std::process::exit(1);
}
}
}
Command::HomebrewLift(lift) => {
let options = LiftOptions {
module_json_path: lift.module_json,
out_dir: lift.out_dir,
entry_name: lift.entry,
mode: lift.mode.into(),
};
match lift_homebrew(options) {
Ok(report) => {
println!(
"Homebrew lift wrote {} functions to {}",
report.functions_emitted,
report.module_json_path.display()
);
if !report.warnings.is_empty() {
println!("Warnings:");
for warning in report.warnings {
println!("- {}", warning);
}
}
}
Err(err) => {
eprintln!("Homebrew lift error: {err}");
std::process::exit(1);
}
}
}
}
}
4 changes: 2 additions & 2 deletions crates/recomp-isa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ impl Memory {

pub fn read(&self, address: usize, size: MemSize) -> Result<u64, ExecError> {
let width = size.bytes();
if !address.is_multiple_of(width) {
if address % width != 0 {
return Err(ExecError::Unaligned {
address,
size: width,
Expand All @@ -142,7 +142,7 @@ impl Memory {

pub fn write(&mut self, address: usize, size: MemSize, value: u64) -> Result<(), ExecError> {
let width = size.bytes();
if !address.is_multiple_of(width) {
if address % width != 0 {
return Err(ExecError::Unaligned {
address,
size: width,
Expand Down
1 change: 1 addition & 0 deletions crates/recomp-pipeline/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ serde_json = "1.0"
sha2 = "0.10"
thiserror = "1.0"
toml = "0.8"
lz4_flex = "0.11"

[dev-dependencies]
tempfile = "3.10"
Loading