initial commit
This commit is contained in:
commit
a1fa4a16e2
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "ive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.72"
|
||||
byteorder = "1.4.3"
|
||||
egui = "0.22.0"
|
||||
egui-wgpu = "0.22.0"
|
||||
egui-winit = "0.22.0"
|
||||
env_logger = "0.10.0"
|
||||
pixels = "0.13.0"
|
||||
winit = "0.28.6"
|
||||
winit_input_helper = "0.14.1"
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2023 ipc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
|||
# ive
|
||||
|
||||
risc-v emulator with display
|
||||
|
||||
![ive](img/ive.png)
|
|
@ -0,0 +1,9 @@
|
|||
[build]
|
||||
target = "riscv32i-unknown-none-elf"
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 1
|
||||
panic = "abort"
|
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "give"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
|
@ -0,0 +1,4 @@
|
|||
# give
|
||||
|
||||
give is a library for interfacing with the ive display. To use give
|
||||
you must have the riscv32i-unknown-none-elf toolchain installed.
|
|
@ -0,0 +1,9 @@
|
|||
[build]
|
||||
target = "riscv32i-unknown-none-elf"
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 1
|
||||
panic = "abort"
|
|
@ -0,0 +1,14 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "give"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"give",
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "grid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.give]
|
||||
path = "../../"
|
Binary file not shown.
|
@ -0,0 +1,10 @@
|
|||
ENTRY(_start);
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = 0x80000000;
|
||||
.text : { *(.text .text.*) }
|
||||
. = 0x0;
|
||||
.data : { *(.data) }
|
||||
.bss : { *(.bss) }
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
fn draw_grid(grid: &[u8], data: &[u8], w: u32, h: u32, x: u32, y: u32) {
|
||||
for (i, c) in grid.iter().enumerate() {
|
||||
let cx = (x + (i as u32 % w)) * 16;
|
||||
let cy = (y + (i as u32 / h)) * 16;
|
||||
|
||||
match c {
|
||||
0 => give::draw_rect_color(0x000000ff, cx, cy),
|
||||
1 => give::draw_rect(data.as_ptr(), cx, cy),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
let tile = include_bytes!("../data/tile");
|
||||
|
||||
#[rustfmt::skip]
|
||||
let formation = [
|
||||
0, 1, 0,
|
||||
1, 0, 1,
|
||||
0, 1, 0,
|
||||
|
||||
1, 1, 1,
|
||||
1, 0, 1,
|
||||
1, 1, 1,
|
||||
|
||||
1, 0, 1,
|
||||
0, 1, 0,
|
||||
1, 0, 1,
|
||||
];
|
||||
|
||||
let mut n = 0;
|
||||
let mut timer = 0;
|
||||
loop {
|
||||
draw_grid(&formation[n * 9..(n * 9) + 9], tile, 3, 3, 400, 300);
|
||||
timer += 1;
|
||||
if timer == 16 {
|
||||
n += 1;
|
||||
timer = 0;
|
||||
}
|
||||
if n == 3 {
|
||||
n = 0;
|
||||
}
|
||||
give::redraw();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
#![no_std]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
use core::ptr;
|
||||
|
||||
const CTRL_DRAW_ADDR: *mut u32 = 0x60000000 as *mut u32;
|
||||
const DRAW_ADDR: *mut u32 = 0x60000001 as *mut u32;
|
||||
|
||||
pub const TILE_SIZE: usize = 16;
|
||||
|
||||
#[panic_handler]
|
||||
pub fn panic(_panic: &PanicInfo<'_>) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
extern "C" fn ctrl_draw_write(val: u32) {
|
||||
unsafe {
|
||||
ptr::write_volatile(CTRL_DRAW_ADDR, val);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn draw_write(val: u32) {
|
||||
unsafe {
|
||||
ptr::write_volatile(DRAW_ADDR, val);
|
||||
}
|
||||
}
|
||||
|
||||
pub extern "C" fn redraw() {
|
||||
ctrl_draw_write(1);
|
||||
}
|
||||
|
||||
pub extern "C" fn draw_rect_color(color: u32, x: u32, y: u32) {
|
||||
draw_write(0);
|
||||
draw_write((x << 16) | y);
|
||||
draw_write(color);
|
||||
}
|
||||
|
||||
pub extern "C" fn draw_rect(data: *const u8, x: u32, y: u32) {
|
||||
draw_write(1);
|
||||
draw_write((x << 16) | y);
|
||||
draw_write(data as u32);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
|
@ -0,0 +1,417 @@
|
|||
use crate::gpu::{Gpu, Primitive};
|
||||
use crate::{elf, Memory, Store};
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
|
||||
pub const RAM_BASE: u32 = 0x80000000;
|
||||
const RAM_SIZE: u32 = RAM_BASE + 0x100000;
|
||||
const GPU_BASE: u32 = 0x60000000;
|
||||
const GPU_SIZE: u32 = GPU_BASE + 0x1;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Format {
|
||||
R,
|
||||
I,
|
||||
IL,
|
||||
S,
|
||||
B,
|
||||
J,
|
||||
JI,
|
||||
U,
|
||||
UI,
|
||||
E,
|
||||
}
|
||||
|
||||
enum Opcode {
|
||||
Add,
|
||||
Sub,
|
||||
Xor,
|
||||
Or,
|
||||
And,
|
||||
Sll,
|
||||
Srl,
|
||||
Sra,
|
||||
Slt,
|
||||
Sltu,
|
||||
Addi,
|
||||
Xori,
|
||||
Ori,
|
||||
Andi,
|
||||
Slli,
|
||||
Srli,
|
||||
Srai,
|
||||
Slti,
|
||||
Sltiu,
|
||||
Lb,
|
||||
Lh,
|
||||
Lw,
|
||||
Lbu,
|
||||
Lhu,
|
||||
Sb,
|
||||
Sh,
|
||||
Sw,
|
||||
Beq,
|
||||
Bne,
|
||||
Blt,
|
||||
Bge,
|
||||
Bltu,
|
||||
Bgeu,
|
||||
Jal,
|
||||
Jalr,
|
||||
Lui,
|
||||
Auipc,
|
||||
Ecall,
|
||||
Ebreak,
|
||||
}
|
||||
|
||||
struct Ram {
|
||||
mem: Vec<u8>,
|
||||
}
|
||||
|
||||
struct Bus {
|
||||
ram: Box<dyn Memory>,
|
||||
gpu: Gpu,
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub reg: [u32; 31],
|
||||
pub pc: u32,
|
||||
bus: Bus,
|
||||
}
|
||||
|
||||
struct Instruction {
|
||||
op: Format,
|
||||
rd: u8,
|
||||
f3: u8,
|
||||
f7: u8,
|
||||
rs1: u8,
|
||||
rs2: u8,
|
||||
imm: u32,
|
||||
simm: u32,
|
||||
uimm: u32,
|
||||
bimm: u32,
|
||||
jimm: u32,
|
||||
}
|
||||
|
||||
impl Ram {
|
||||
fn new() -> Self {
|
||||
let mut mem = Vec::new();
|
||||
mem.resize((RAM_SIZE - RAM_BASE) as usize, 0);
|
||||
Self { mem }
|
||||
}
|
||||
}
|
||||
|
||||
impl Memory for Ram {
|
||||
fn read(&self, addr: u32) -> u32 {
|
||||
let addr = addr as usize;
|
||||
// should check for unaligned reads
|
||||
LittleEndian::read_u32(&self.mem[addr..addr + 4])
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u32, val: Store) {
|
||||
let addr = addr as usize;
|
||||
|
||||
match val {
|
||||
Store::Byte(b) => self.mem[addr] = b,
|
||||
Store::Half(h) => LittleEndian::write_u16(&mut self.mem[addr..addr + 2], h),
|
||||
Store::Word(w) => LittleEndian::write_u32(&mut self.mem[addr..addr + 4], w),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format {
|
||||
fn new(ins: u32) -> Self {
|
||||
match ins & 0x7f {
|
||||
0x33 => Self::R,
|
||||
0x13 => Self::I,
|
||||
0x03 => Self::IL,
|
||||
0x23 => Self::S,
|
||||
0x63 => Self::B,
|
||||
0x6f => Self::J,
|
||||
0x67 => Self::JI,
|
||||
0x37 => Self::U,
|
||||
0x17 => Self::UI,
|
||||
0x73 => Self::E,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! op {
|
||||
($op: ident) => {
|
||||
Instruction {
|
||||
op: Format::$op,
|
||||
..
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! f3f7 {
|
||||
($op: ident, $f3: expr, $f7: expr) => {
|
||||
Instruction {
|
||||
op: Format::$op,
|
||||
f3: $f3,
|
||||
f7: $f7,
|
||||
..
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! f3 {
|
||||
($op: ident, $f3: expr) => {
|
||||
Instruction {
|
||||
op: Format::$op,
|
||||
f3: $f3,
|
||||
..
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Opcode {
|
||||
fn new(ins: &Instruction) -> Self {
|
||||
match ins {
|
||||
f3f7!(R, 0x00, 0x00) => Self::Add,
|
||||
f3f7!(R, 0x00, 0x20) => Self::Sub,
|
||||
f3f7!(R, 0x04, 0x00) => Self::Xor,
|
||||
f3f7!(R, 0x06, 0x00) => Self::Or,
|
||||
f3f7!(R, 0x07, 0x00) => Self::And,
|
||||
f3f7!(R, 0x01, 0x00) => Self::Sll,
|
||||
f3f7!(R, 0x05, 0x00) => Self::Srl,
|
||||
f3f7!(R, 0x05, 0x20) => Self::Sra,
|
||||
f3f7!(R, 0x02, 0x00) => Self::Slt,
|
||||
f3f7!(R, 0x03, 0x00) => Self::Sltu,
|
||||
f3!(I, 0x00) => Self::Addi,
|
||||
f3!(I, 0x04) => Self::Xori,
|
||||
f3!(I, 0x06) => Self::Ori,
|
||||
f3!(I, 0x07) => Self::Andi,
|
||||
f3!(I, 0x01) => Self::Slli,
|
||||
f3f7!(I, 0x05, 0x00) => Self::Srli,
|
||||
f3f7!(I, 0x05, 0x20) => Self::Srai,
|
||||
f3!(I, 0x02) => Self::Slti,
|
||||
f3!(I, 0x03) => Self::Sltiu,
|
||||
f3!(IL, 0x00) => Self::Lb,
|
||||
f3!(IL, 0x01) => Self::Lh,
|
||||
f3!(IL, 0x02) => Self::Lw,
|
||||
f3!(IL, 0x04) => Self::Lbu,
|
||||
f3!(IL, 0x05) => Self::Lhu,
|
||||
f3!(S, 0x00) => Self::Sb,
|
||||
f3!(S, 0x01) => Self::Sh,
|
||||
f3!(S, 0x02) => Self::Sw,
|
||||
f3!(B, 0x00) => Self::Beq,
|
||||
f3!(B, 0x01) => Self::Bne,
|
||||
f3!(B, 0x04) => Self::Blt,
|
||||
f3!(B, 0x05) => Self::Bge,
|
||||
f3!(B, 0x06) => Self::Bltu,
|
||||
f3!(B, 0x07) => Self::Bgeu,
|
||||
op!(J) => Self::Jal,
|
||||
f3!(JI, 0x0) => Self::Jalr,
|
||||
op!(U) => Self::Lui,
|
||||
op!(UI) => Self::Auipc,
|
||||
op!(E) => match ins.imm {
|
||||
0x0 => Self::Ecall,
|
||||
_ => Self::Ebreak,
|
||||
},
|
||||
_ => panic!("bad instruction"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Instruction {
|
||||
fn new(ins: u32) -> Self {
|
||||
Self {
|
||||
op: Format::new(ins),
|
||||
rd: ((ins >> 7) & 0x1f) as u8,
|
||||
f3: ((ins >> 12) & 0x7) as u8,
|
||||
f7: ((ins >> 25) & 0x7f) as u8,
|
||||
rs1: ((ins >> 15) & 0x1f) as u8,
|
||||
rs2: ((ins >> 20) & 0x1f) as u8,
|
||||
imm: ((ins & 0xfff00000) as i64 as i32 >> 20) as u32,
|
||||
simm: (((ins & 0xfe000000) as i64 as i32 >> 20) as u32 | ((ins >> 7) & 0x1f)),
|
||||
uimm: ins >> 12,
|
||||
bimm: ((ins & 0x80000000) as i64 as i32 >> 19) as u32
|
||||
| ((ins & 0x80) << 4)
|
||||
| ((ins >> 20) & 0x7e0)
|
||||
| ((ins >> 7) & 0x1e),
|
||||
jimm: ((ins & 0x80000000) as i64 as i32 >> 11) as u32
|
||||
| (ins & 0xff000)
|
||||
| ((ins >> 9) & 0x800)
|
||||
| ((ins >> 20) & 0x7fe),
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_r(&self, cpu: &mut Cpu, op: fn(u32, u32) -> u32) {
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
let rs2 = cpu.reg_read(self.rs2);
|
||||
cpu.reg_write(self.rd, op(rs1, rs2));
|
||||
}
|
||||
|
||||
fn exec_i(&self, cpu: &mut Cpu, op: fn(u32, u32) -> u32) {
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
cpu.reg_write(self.rd, op(rs1, self.imm));
|
||||
}
|
||||
|
||||
fn exec_il(&self, cpu: &mut Cpu, op: fn(u32) -> u32) {
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
let load = cpu.read(rs1.wrapping_add_signed(self.imm as i32));
|
||||
cpu.reg_write(self.rd, op(load));
|
||||
}
|
||||
|
||||
fn exec_s(&self, cpu: &mut Cpu, op: fn(u32) -> Store) {
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
let rs2 = cpu.reg_read(self.rs2);
|
||||
cpu.write(rs1.wrapping_add_signed(self.simm as i32), op(rs2))
|
||||
}
|
||||
|
||||
fn exec_b(&self, cpu: &mut Cpu, op: fn(u32, u32) -> bool) {
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
let rs2 = cpu.reg_read(self.rs2);
|
||||
if op(rs1, rs2) {
|
||||
cpu.pc = cpu.pc.wrapping_add_signed(self.bimm as i32) - 4;
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&self, cpu: &mut Cpu) {
|
||||
match Opcode::new(self) {
|
||||
Opcode::Add => self.exec_r(cpu, |r1, r2| r1.wrapping_add_signed(r2 as i32)),
|
||||
Opcode::Sub => self.exec_r(cpu, |r1, r2| r1.wrapping_sub(r2)),
|
||||
Opcode::Xor => self.exec_r(cpu, |r1, r2| r1 ^ r2),
|
||||
Opcode::Or => self.exec_r(cpu, |r1, r2| r1 | r2),
|
||||
Opcode::And => self.exec_r(cpu, |r1, r2| r1 & r2),
|
||||
Opcode::Sll => self.exec_r(cpu, |r1, r2| r1.wrapping_shl(r2)),
|
||||
Opcode::Srl => self.exec_r(cpu, |r1, r2| r1.wrapping_shr(r2)),
|
||||
Opcode::Sra => self.exec_r(cpu, |r1, r2| r1.wrapping_shr(r2)),
|
||||
Opcode::Slt => self.exec_r(cpu, |r1, r2| (r1 < (r2 as i32) as u32) as u32),
|
||||
Opcode::Sltu => self.exec_r(cpu, |r1, r2| (r1 < r2) as u32),
|
||||
|
||||
Opcode::Addi => self.exec_i(cpu, |r1, imm| r1.wrapping_add_signed(imm as i32)),
|
||||
Opcode::Xori => self.exec_i(cpu, |r1, imm| r1 ^ imm),
|
||||
Opcode::Ori => self.exec_i(cpu, |r1, imm| r1 | imm),
|
||||
Opcode::Andi => self.exec_i(cpu, |r1, imm| r1 & imm),
|
||||
Opcode::Slli => self.exec_i(cpu, |r1, imm| r1.wrapping_shl(imm & 0x1f)),
|
||||
Opcode::Srli => self.exec_i(cpu, |r1, imm| r1.wrapping_shr(imm & 0x1f)),
|
||||
Opcode::Srai => self.exec_i(cpu, |r1, imm| r1.wrapping_shr(imm & 0x1f) as i32 as u32),
|
||||
Opcode::Slti => self.exec_i(cpu, |r1, imm| (r1 < (imm as i32) as u32) as u32),
|
||||
Opcode::Sltiu => self.exec_i(cpu, |r1, imm| (r1 < imm) as u32),
|
||||
|
||||
Opcode::Lb => self.exec_il(cpu, |imm| imm as u8 as u32),
|
||||
Opcode::Lh => self.exec_il(cpu, |imm| imm as u16 as u32),
|
||||
Opcode::Lw => self.exec_il(cpu, |imm| imm),
|
||||
Opcode::Lbu => self.exec_il(cpu, |imm| imm as u8 as u32),
|
||||
Opcode::Lhu => self.exec_il(cpu, |imm| imm as u16 as u32),
|
||||
|
||||
Opcode::Sb => self.exec_s(cpu, |r1| Store::Byte(r1 as u8)),
|
||||
Opcode::Sh => self.exec_s(cpu, |r1| Store::Half(r1 as u16)),
|
||||
Opcode::Sw => self.exec_s(cpu, Store::Word),
|
||||
|
||||
Opcode::Beq => self.exec_b(cpu, |r1, r2| (r1 as i32) == (r2 as i32)),
|
||||
Opcode::Bne => self.exec_b(cpu, |r1, r2| (r1 as i32) != (r2 as i32)),
|
||||
Opcode::Blt => self.exec_b(cpu, |r1, r2| (r1 as i32) < (r2 as i32)),
|
||||
Opcode::Bge => self.exec_b(cpu, |r1, r2| (r1 as i32) >= (r2 as i32)),
|
||||
Opcode::Bltu => self.exec_b(cpu, |r1, r2| r1 < r2),
|
||||
Opcode::Bgeu => self.exec_b(cpu, |r1, r2| r1 >= r2),
|
||||
|
||||
Opcode::Jal => {
|
||||
cpu.reg_write(self.rd, cpu.pc);
|
||||
cpu.pc = cpu.pc.wrapping_add_signed(self.jimm as i32) - 4;
|
||||
}
|
||||
Opcode::Jalr => {
|
||||
let pc = cpu.pc;
|
||||
let rs1 = cpu.reg_read(self.rs1);
|
||||
cpu.pc = rs1.wrapping_add_signed(self.imm as i32);
|
||||
cpu.reg_write(self.rd, pc);
|
||||
}
|
||||
|
||||
Opcode::Lui => cpu.reg_write(self.rd, self.uimm << 12),
|
||||
Opcode::Auipc => cpu.reg_write(self.rd, cpu.pc.wrapping_add(self.uimm << 12) - 4),
|
||||
|
||||
Opcode::Ecall | Opcode::Ebreak => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Memory for Bus {
|
||||
fn read(&self, addr: u32) -> u32 {
|
||||
match addr {
|
||||
RAM_BASE..=RAM_SIZE => self.ram.read(addr - RAM_BASE),
|
||||
GPU_BASE..=GPU_SIZE => self.gpu.read(addr - GPU_BASE),
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u32, val: Store) {
|
||||
match addr {
|
||||
RAM_BASE..=RAM_SIZE => self.ram.write(addr - RAM_BASE, val),
|
||||
GPU_BASE..=GPU_SIZE => self.gpu.write(addr - GPU_BASE, val),
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub fn new(mem: Vec<u8>) -> Result<Self, elf::Error> {
|
||||
let mut reg: [u32; 31] = [0; 31];
|
||||
let mut ram = Ram::new();
|
||||
|
||||
// set sp to point to top of ram
|
||||
reg[2] = RAM_SIZE;
|
||||
|
||||
let pc = elf::load(mem, &mut ram.mem)?;
|
||||
|
||||
Ok(Self {
|
||||
reg,
|
||||
pc,
|
||||
bus: Bus {
|
||||
ram: Box::new(ram),
|
||||
gpu: Gpu::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read(&self, addr: u32) -> u32 {
|
||||
self.bus.read(addr)
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u32, val: Store) {
|
||||
self.bus.write(addr, val);
|
||||
}
|
||||
|
||||
fn reg_read(&mut self, reg: u8) -> u32 {
|
||||
let reg = reg as usize;
|
||||
if reg > self.reg.len() {
|
||||
panic!("invalid register read");
|
||||
}
|
||||
self.reg[reg]
|
||||
}
|
||||
|
||||
fn reg_write(&mut self, reg: u8, val: u32) {
|
||||
let reg = reg as usize;
|
||||
if reg > self.reg.len() {
|
||||
panic!("invalid register write");
|
||||
}
|
||||
self.reg[reg] = val;
|
||||
self.reg[0] = 0
|
||||
}
|
||||
|
||||
pub fn gpu_queue(&self) -> &Vec<Primitive> {
|
||||
&self.bus.gpu.queue
|
||||
}
|
||||
|
||||
pub fn gpu_clear(&mut self) {
|
||||
self.bus.gpu.update = false;
|
||||
self.bus.gpu.queue.clear();
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> bool {
|
||||
self.bus.gpu.update
|
||||
}
|
||||
|
||||
pub fn step(&mut self) -> bool {
|
||||
let inst = Instruction::new(self.read(self.pc));
|
||||
|
||||
self.pc += 4;
|
||||
inst.execute(self);
|
||||
if self.pc == 0 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
use crate::cpu::RAM_BASE;
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
|
||||
const MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
|
||||
const LOAD: u32 = 1;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Eof,
|
||||
BadMagic,
|
||||
BadType,
|
||||
BadMachine,
|
||||
}
|
||||
|
||||
struct Phdr {
|
||||
ptype: u32,
|
||||
offset: u32,
|
||||
vaddr: u32,
|
||||
memsz: u32,
|
||||
}
|
||||
|
||||
fn read_u16<R: Read>(buf: &mut R) -> u16 {
|
||||
buf.read_u16::<LittleEndian>().unwrap()
|
||||
}
|
||||
|
||||
fn read_u32<R: Read>(buf: &mut R) -> u32 {
|
||||
buf.read_u32::<LittleEndian>().unwrap()
|
||||
}
|
||||
|
||||
impl Phdr {
|
||||
fn new<R: Read>(buf: &mut R) -> Self {
|
||||
let ptype = read_u32(buf);
|
||||
let offset = read_u32(buf);
|
||||
let vaddr = read_u32(buf);
|
||||
for _ in 0..2 {
|
||||
read_u32(buf);
|
||||
}
|
||||
let memsz = read_u32(buf);
|
||||
|
||||
for _ in 0..2 {
|
||||
read_u32(buf);
|
||||
}
|
||||
|
||||
Self {
|
||||
ptype,
|
||||
offset,
|
||||
vaddr,
|
||||
memsz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(elf: Vec<u8>, mem: &mut [u8]) -> Result<u32, Error> {
|
||||
let mut rdr = Cursor::new(elf);
|
||||
|
||||
let mut ident: [u8; 16] = [0; 16];
|
||||
|
||||
if rdr.read_exact(&mut ident).is_err() {
|
||||
return Err(Error::Eof);
|
||||
}
|
||||
|
||||
if ident[..MAGIC.len()] != MAGIC {
|
||||
return Err(Error::BadMagic);
|
||||
}
|
||||
|
||||
let etype = read_u16(&mut rdr);
|
||||
|
||||
if etype != 0x2 {
|
||||
return Err(Error::BadType);
|
||||
}
|
||||
|
||||
let machine = read_u16(&mut rdr);
|
||||
|
||||
if machine != 0xf3 {
|
||||
return Err(Error::BadMachine);
|
||||
}
|
||||
|
||||
// version
|
||||
read_u32(&mut rdr);
|
||||
|
||||
let entry = read_u32(&mut rdr);
|
||||
let phoff = read_u32(&mut rdr);
|
||||
// shoff
|
||||
read_u32(&mut rdr);
|
||||
// flags
|
||||
read_u32(&mut rdr);
|
||||
// ehsize
|
||||
read_u16(&mut rdr);
|
||||
// phentsize
|
||||
read_u16(&mut rdr);
|
||||
let phnum = read_u16(&mut rdr);
|
||||
|
||||
rdr.set_position(phoff as u64);
|
||||
|
||||
for _ in 0..phnum {
|
||||
let phdr = Phdr::new(&mut rdr);
|
||||
|
||||
if phdr.ptype != LOAD {
|
||||
continue;
|
||||
}
|
||||
|
||||
let vaddr = (phdr.vaddr - RAM_BASE) as usize;
|
||||
let memsz = phdr.memsz as usize;
|
||||
|
||||
let pos = rdr.position();
|
||||
rdr.set_position(phdr.offset as u64);
|
||||
if rdr.read_exact(&mut mem[vaddr..vaddr + memsz]).is_err() {
|
||||
return Err(Error::Eof);
|
||||
}
|
||||
rdr.set_position(pos);
|
||||
}
|
||||
|
||||
Ok(entry)
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
use crate::{Memory, Store};
|
||||
|
||||
enum Command {
|
||||
Control,
|
||||
Draw,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum DrawCommand {
|
||||
Rect,
|
||||
TexturedRect,
|
||||
Idle,
|
||||
}
|
||||
|
||||
pub enum Primitive {
|
||||
Rect(u32, u32),
|
||||
TexturedRect(u32, u32),
|
||||
}
|
||||
|
||||
struct Packet {
|
||||
cmd: DrawCommand,
|
||||
len: u32,
|
||||
count: u32,
|
||||
args: [u32; 2],
|
||||
}
|
||||
|
||||
pub struct Gpu {
|
||||
pub update: bool,
|
||||
packet: Packet,
|
||||
pub queue: Vec<Primitive>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn new(addr: u32) -> Self {
|
||||
match addr {
|
||||
0x0 => Self::Control,
|
||||
0x1 => Self::Draw,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawCommand {
|
||||
fn new(cmd: u32) -> Self {
|
||||
match cmd {
|
||||
0x0 => Self::Rect,
|
||||
0x1 => Self::TexturedRect,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive {
|
||||
fn new(packet: &Packet) -> Self {
|
||||
let args = packet.args;
|
||||
|
||||
match packet.cmd {
|
||||
DrawCommand::Rect => Self::Rect(args[0], args[1]),
|
||||
DrawCommand::TexturedRect => Self::TexturedRect(args[0], args[1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub fn new(cmd: DrawCommand) -> Self {
|
||||
let len = match cmd {
|
||||
DrawCommand::Rect | DrawCommand::TexturedRect => 2,
|
||||
DrawCommand::Idle => 0,
|
||||
};
|
||||
|
||||
Self {
|
||||
cmd,
|
||||
len,
|
||||
count: 0,
|
||||
args: [0; 2],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.cmd == DrawCommand::Idle
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.cmd = DrawCommand::Idle;
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
fn transfer(&self) -> bool {
|
||||
self.len > 0 && self.count == self.len
|
||||
}
|
||||
|
||||
fn read(&mut self, queue: &mut Vec<Primitive>, arg: u32) {
|
||||
self.args[self.count as usize] = arg;
|
||||
self.count += 1;
|
||||
|
||||
if self.transfer() {
|
||||
queue.push(Primitive::new(self));
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Gpu {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
update: false,
|
||||
packet: Packet::new(DrawCommand::Idle),
|
||||
queue: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Memory for Gpu {
|
||||
fn read(&self, addr: u32) -> u32 {
|
||||
panic!("attempted to read at {:#08x?}", addr);
|
||||
}
|
||||
|
||||
fn write(&mut self, addr: u32, val: Store) {
|
||||
if let Store::Word(w) = val {
|
||||
match Command::new(addr) {
|
||||
Command::Control => {
|
||||
self.update = true;
|
||||
}
|
||||
Command::Draw => {
|
||||
if self.packet.idle() {
|
||||
self.packet = Packet::new(DrawCommand::new(w));
|
||||
} else {
|
||||
self.packet.read(&mut self.queue, w);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("attempted to store {:#?}", val);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,372 @@
|
|||
use anyhow::{bail, Result};
|
||||
use egui::{ClippedPrimitive, Context, TexturesDelta};
|
||||
use egui_wgpu::renderer::{Renderer, ScreenDescriptor};
|
||||
use pixels::{wgpu, Pixels, PixelsContext, SurfaceTexture};
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::{Event, VirtualKeyCode};
|
||||
use winit::event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget};
|
||||
use winit::window::{Window, WindowBuilder};
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
mod cpu;
|
||||
mod elf;
|
||||
mod gpu;
|
||||
|
||||
use crate::cpu::Cpu;
|
||||
use crate::gpu::Primitive;
|
||||
|
||||
const FB_WIDTH: u32 = 800;
|
||||
const FB_HEIGHT: u32 = 600;
|
||||
const WINDOW_WIDTH: u32 = 1024;
|
||||
const WINDOW_HEIGHT: u32 = 768;
|
||||
|
||||
const TILE_SIZE: u32 = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Store {
|
||||
Byte(u8),
|
||||
Half(u16),
|
||||
Word(u32),
|
||||
}
|
||||
|
||||
enum Order {
|
||||
Lsb,
|
||||
Msb,
|
||||
}
|
||||
|
||||
trait Memory {
|
||||
fn read(&self, addr: u32) -> u32;
|
||||
fn write(&mut self, addr: u32, val: Store);
|
||||
}
|
||||
|
||||
struct Gui {
|
||||
window_open: bool,
|
||||
gpu_log: Vec<String>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
egui_ctx: Context,
|
||||
egui_state: egui_winit::State,
|
||||
screen_descriptor: ScreenDescriptor,
|
||||
renderer: Renderer,
|
||||
paint_jobs: Vec<ClippedPrimitive>,
|
||||
textures: TexturesDelta,
|
||||
gui: Gui,
|
||||
cpu: Cpu,
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
window_open: true,
|
||||
gpu_log: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(&mut self, ctx: &Context, cpu: &Cpu) {
|
||||
egui::Window::new("regs")
|
||||
.open(&mut self.window_open)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.monospace("pc:");
|
||||
ui.monospace(format!("{:#08x}", cpu.pc));
|
||||
});
|
||||
for (i, reg) in cpu.reg.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.monospace(format!("r{}:", i));
|
||||
ui.monospace(format!("{:#08x}", reg));
|
||||
});
|
||||
}
|
||||
});
|
||||
egui::Window::new("gpu")
|
||||
.open(&mut self.window_open)
|
||||
.constrain(true)
|
||||
.show(ctx, |ui| {
|
||||
let mut log = self.gpu_log.join("\n");
|
||||
egui::ScrollArea::vertical()
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
ui.add(egui::TextEdit::multiline(&mut log).code_editor())
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new<T>(
|
||||
event_loop: &EventLoopWindowTarget<T>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: f32,
|
||||
pixels: &Pixels,
|
||||
cpu: Cpu,
|
||||
) -> Self {
|
||||
let max_texture_size = pixels.device().limits().max_texture_dimension_2d as usize;
|
||||
|
||||
let egui_ctx = Context::default();
|
||||
let mut egui_state = egui_winit::State::new(event_loop);
|
||||
egui_state.set_max_texture_side(max_texture_size);
|
||||
egui_state.set_pixels_per_point(scale_factor);
|
||||
let screen_descriptor = ScreenDescriptor {
|
||||
size_in_pixels: [width, height],
|
||||
pixels_per_point: scale_factor,
|
||||
};
|
||||
let renderer = Renderer::new(pixels.device(), pixels.render_texture_format(), None, 1);
|
||||
let textures = TexturesDelta::default();
|
||||
let gui = Gui::new();
|
||||
|
||||
Self {
|
||||
egui_ctx,
|
||||
egui_state,
|
||||
screen_descriptor,
|
||||
renderer,
|
||||
paint_jobs: Vec::new(),
|
||||
textures,
|
||||
gui,
|
||||
cpu,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: &winit::event::WindowEvent) {
|
||||
let _ = self.egui_state.on_event(&self.egui_ctx, event);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
if width > 0 && height > 0 {
|
||||
self.screen_descriptor.size_in_pixels = [width, height];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_factor(&mut self, scale_factor: f64) {
|
||||
self.screen_descriptor.pixels_per_point = scale_factor as f32;
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, window: &Window) {
|
||||
let raw_input = self.egui_state.take_egui_input(window);
|
||||
let output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
self.gui.ui(egui_ctx, &self.cpu);
|
||||
});
|
||||
|
||||
self.textures.append(output.textures_delta);
|
||||
self.egui_state
|
||||
.handle_platform_output(window, &self.egui_ctx, output.platform_output);
|
||||
self.paint_jobs = self.egui_ctx.tessellate(output.shapes);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
render_target: &wgpu::TextureView,
|
||||
context: &PixelsContext,
|
||||
) {
|
||||
for (id, image_delta) in &self.textures.set {
|
||||
self.renderer
|
||||
.update_texture(&context.device, &context.queue, *id, image_delta);
|
||||
}
|
||||
self.renderer.update_buffers(
|
||||
&context.device,
|
||||
&context.queue,
|
||||
encoder,
|
||||
&self.paint_jobs,
|
||||
&self.screen_descriptor,
|
||||
);
|
||||
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("egui"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: render_target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
self.renderer
|
||||
.render(&mut rpass, &self.paint_jobs, &self.screen_descriptor);
|
||||
}
|
||||
|
||||
let textures = std::mem::take(&mut self.textures);
|
||||
for id in &textures.free {
|
||||
self.renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_rect(frame: &mut [u8], order: Order, offset: u32, color: u32) {
|
||||
let offset = offset as usize * 4;
|
||||
|
||||
let (r, g, b, a) = match order {
|
||||
Order::Lsb => (
|
||||
(color >> 24) as u8,
|
||||
(color >> 16) as u8,
|
||||
(color >> 8) as u8,
|
||||
color as u8,
|
||||
),
|
||||
Order::Msb => (
|
||||
color as u8,
|
||||
(color >> 8) as u8,
|
||||
(color >> 16) as u8,
|
||||
(color >> 24) as u8,
|
||||
),
|
||||
};
|
||||
|
||||
if a == 0xff {
|
||||
frame[offset..offset + 4].copy_from_slice(&[r, g, b, a]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut [u8]) {
|
||||
if !self.update() {
|
||||
return;
|
||||
}
|
||||
|
||||
let log: Vec<String> = self
|
||||
.gpu_queue()
|
||||
.iter()
|
||||
.map(|cmd| match cmd {
|
||||
Primitive::Rect(offset, color) => {
|
||||
let (x, y) = ((offset >> 16), (offset & 0xff));
|
||||
|
||||
for i in 0..TILE_SIZE * TILE_SIZE {
|
||||
let x = x + (i % TILE_SIZE);
|
||||
let y = (y + (i / TILE_SIZE)) * FB_WIDTH;
|
||||
Self::draw_rect(frame, Order::Lsb, x + y, *color);
|
||||
}
|
||||
|
||||
format!("Rect: {:#08x} {:#08x}", offset, color)
|
||||
}
|
||||
Primitive::TexturedRect(offset, addr) => {
|
||||
let (x, y) = ((offset >> 16), (offset & 0xff));
|
||||
|
||||
for i in 0..TILE_SIZE * TILE_SIZE {
|
||||
let data = self.read(addr + (i * 4));
|
||||
let x = x + (i % TILE_SIZE);
|
||||
let y = (y + (i / TILE_SIZE)) * FB_WIDTH;
|
||||
Self::draw_rect(frame, Order::Msb, x + y, data);
|
||||
}
|
||||
|
||||
format!("TexturedRect: {:#08x} {:#08x}", offset, addr)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.gui.gpu_log.extend(log);
|
||||
self.gpu_clear();
|
||||
}
|
||||
|
||||
fn read(&self, offset: u32) -> u32 {
|
||||
self.cpu.read(offset)
|
||||
}
|
||||
|
||||
fn gpu_queue(&self) -> &Vec<Primitive> {
|
||||
self.cpu.gpu_queue()
|
||||
}
|
||||
|
||||
fn gpu_clear(&mut self) {
|
||||
self.cpu.gpu_clear();
|
||||
}
|
||||
|
||||
fn update(&mut self) -> bool {
|
||||
self.cpu.update()
|
||||
}
|
||||
|
||||
fn step(&mut self) {
|
||||
self.cpu.step();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(path: String) -> Result<()> {
|
||||
env_logger::init();
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut data = Vec::new();
|
||||
|
||||
file.read_to_end(&mut data)?;
|
||||
|
||||
let cpu = match Cpu::new(data) {
|
||||
Ok(cpu) => cpu,
|
||||
Err(e) => match e {
|
||||
elf::Error::Eof => bail!("eof"),
|
||||
elf::Error::BadMagic => bail!("bad magic"),
|
||||
elf::Error::BadType => bail!("invalid executable"),
|
||||
elf::Error::BadMachine => bail!("foreign elf architecture"),
|
||||
},
|
||||
};
|
||||
|
||||
let event_loop = EventLoop::new();
|
||||
let mut input = WinitInputHelper::new();
|
||||
let window = {
|
||||
let size = LogicalSize::new(WINDOW_WIDTH as f64, WINDOW_HEIGHT as f64);
|
||||
WindowBuilder::new()
|
||||
.with_title("ive")
|
||||
.with_inner_size(size)
|
||||
.with_min_inner_size(size)
|
||||
.with_resizable(false)
|
||||
.build(&event_loop)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let (mut pixels, mut state) = {
|
||||
let window_size = window.inner_size();
|
||||
let scale_factor = 1.0;
|
||||
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
||||
let pixels = Pixels::new(FB_WIDTH, FB_HEIGHT, surface_texture).unwrap();
|
||||
let state = State::new(
|
||||
&event_loop,
|
||||
window_size.width,
|
||||
window_size.height,
|
||||
scale_factor,
|
||||
&pixels,
|
||||
cpu,
|
||||
);
|
||||
(pixels, state)
|
||||
};
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
if input.update(&event) {
|
||||
if input.key_pressed(VirtualKeyCode::Escape) || input.close_requested() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(scale_factor) = input.scale_factor() {
|
||||
state.scale_factor(scale_factor);
|
||||
}
|
||||
|
||||
if let Some(size) = input.window_resized() {
|
||||
if pixels.resize_surface(size.width, size.height).is_err() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
state.resize(size.width, size.height);
|
||||
}
|
||||
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => {
|
||||
state.handle_event(&event);
|
||||
}
|
||||
Event::RedrawRequested(_) => {
|
||||
state.draw(pixels.frame_mut());
|
||||
state.prepare(&window);
|
||||
let _ = pixels.render_with(|encoder, target, ctx| {
|
||||
ctx.scaling_renderer.render(encoder, target);
|
||||
state.render(encoder, target, ctx);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
state.step();
|
||||
if state.update() {
|
||||
window.request_redraw();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
use std::env;
|
||||
use std::process;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
if let Some(path) = env::args().nth(1) {
|
||||
ive::run(path)
|
||||
} else {
|
||||
eprintln!("usage: {} <file>", env::args().next().unwrap());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue