Compare commits

..
Author SHA1 Message Date
SonicSwordcane 094778466d Use different checkout action
Release New Version / Release (push) Failing after 27m32s
2024-12-16 19:56:28 -05:00
SonicSwordcane 3dc7c833e8 Fix reference
Release New Version / Release (push) Failing after 6s
2024-12-16 19:38:35 -05:00
SonicSwordcane e23bdb73fd Add a release action
Release New Version / Release (push) Failing after 19s
2024-12-15 23:46:08 -05:00
19 changed files with 342 additions and 930 deletions
-1
View File
@@ -4,7 +4,6 @@
*.html text eol=lf diff=html
*.java text eol=lf diff=java
*.js text eol=lf diff=js
*.sh text eol=lf
*.txt text eol=lf
*.class binary
+42
View File
@@ -0,0 +1,42 @@
name: Release New Version
run-name: ${{ gitea.actor }} is releasing a new version
on: [push]
jobs:
Release:
runs-on: lemur-build
steps:
- name: Check out repository code
uses: taiki-e/checkout-action@v1
- name: Prepare artifacts dir
run: mkdir -p /artifacts
- name: Build for Linux
run: |
cargo build --release
cp target/release/lemur /artifacts/lemur-linux
- name: Bundle for Linux
run: |
cargo bundle --release --format deb
cp target/release/bundle/deb/*.deb /artifacts
- name: Build for Windows
run: |
cargo build --release --target x86_64-pc-windows-msvc
cp target/x86_64-pc-windows-msvc/release/lemur.exe /artifacts
- name: Bundle for Windows
run: |
cargo bundle --release --target x86_64-pc-windows-msvc --format msi
cp target/x86_64-pc-windows-msvc/release/bundle/msi/Lemur.msi /artifacts
- name: Build for MacOS
run: |
cargo build --release --target x86_64-apple-darwin
cp target/x86_64-apple-darwin/release/lemur /artifacts/lemur-osx
- name: Bundle for MacOS
run: |
cargo bundle --release --target x86_64-apple-darwin --format osx
genisoimage -V lemur -D -R -apple -no-pad -o /artifacts/Lemur.dmg target/x86_64-apple-darwin/release/bundle/osx/Lemur.app
- name: Create Release
uses: https://gitea.com/actions/release-action@main
with:
files: |-
/artifacts/**
api-key: '${{secrets.RELEASE_TOKEN}}'
Generated
+215 -250
View File
File diff suppressed because it is too large Load Diff
+8 -14
View File
@@ -1,10 +1,7 @@
[package]
name = "lemur"
description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.2.5"
version = "0.1.0"
edition = "2021"
[dependencies]
@@ -14,13 +11,12 @@ bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "5"
egui = { version = "0.30", features = ["serde"] }
egui_extras = { version = "0.30", features = ["image"] }
egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" }
egui-winit = "0.30"
egui-wgpu = { version = "0.30", features = ["winit"] }
egui = { version = "0.29", features = ["serde"] }
egui_extras = { version = "0.29", features = ["image"] }
egui-toast = "0.15"
egui-winit = "0.29"
egui-wgpu = { version = "0.29", features = ["winit"] }
gilrs = { version = "0.11", features = ["serde-serialize"] }
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.13"
num-derive = "0.4"
@@ -33,8 +29,7 @@ rubato = "0.16"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
wgpu = "23"
wgpu = "22.1"
winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies]
@@ -42,7 +37,6 @@ windows = { version = "0.58", features = ["Win32_System_Threading"] }
[build-dependencies]
cc = "1"
winresource = "0.1"
[profile.release]
lto = true
@@ -51,4 +45,4 @@ lto = true
name = "Lemur"
identifier = "com.virtual-boy.Lemur"
icon = ["assets/lemur-256x256.png"]
category = "games"
category = "games"
+1 -11
View File
@@ -12,14 +12,4 @@ Run
cargo build --release
```
The executable will be in `target/release/lemur[.exe]`
## Release
Bump the version number in `Cargo.toml`, then run this script:
```sh
./scripts/release.sh
```
It uses docker to cross compile for Windows, MacOS, and Linux. All binaries are left in the `output` directory.
The executable will be in `target/release/lemur[.exe]`
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

+6 -17
View File
@@ -6,29 +6,18 @@ FROM crazymax/osxcross:latest-ubuntu AS osxcross
FROM rust:latest
RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \
apt-get update && \
apt-get install -y clang-19 lld-19 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \
ln -s $(which clang-19) /usr/bin/clang && \
ln -s $(which clang++-19) /usr/bin/clang++
apt-get install -y clang lld libc6-dev libasound2-dev libudev-dev genisoimage && \
cargo install cargo-bundle xwin
COPY --from=osxcross /osxcross /osxcross
RUN xwin --accept-license splat --output xwin && rm -rf .xwin-cache
ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-19" CXX="clang++-19" AR="llvm-ar-19" \
CC="clang" CXX="clang++" AR="llvm-ar-14" \
CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \
CXX_aarch64-apple-darwin="o6a4-clang++" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-19" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \
RC_PATH="llvm-rc-19" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-14" \
RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64" \
MACOSX_DEPLOYMENT_TARGET="14.5"
+2 -10
View File
@@ -1,12 +1,6 @@
use std::{error::Error, path::Path};
fn main() -> Result<(), Box<dyn Error>> {
if std::env::var("CARGO_CFG_TARGET_OS")? == "windows" {
let mut res = winresource::WindowsResource::new();
res.set_icon("assets/lemur.ico");
res.compile()?;
}
use std::path::Path;
fn main() {
println!("cargo::rerun-if-changed=shrooms-vb-core");
cc::Build::new()
.include(Path::new("shrooms-vb-core/core"))
@@ -17,6 +11,4 @@ fn main() -> Result<(), Box<dyn Error>> {
.define("VB_DIV_GENERIC", None)
.file(Path::new("shrooms-vb-core/core/vb.c"))
.compile("vb");
Ok(())
}
-35
View File
@@ -1,35 +0,0 @@
# Set everything up
rm -rf output
mkdir -p output
cargo clean
# Build for linux
cargo build --release
cp target/release/lemur output/lemur-linux
# Bundle for Linux
cargo bundle --release --format deb
cp target/release/bundle/deb/*.deb output
# Build for Windows
cargo build --release --target x86_64-pc-windows-msvc
cp target/x86_64-pc-windows-msvc/release/lemur.exe output
# Build for MacOS Intel
cargo build --release --target x86_64-apple-darwin
cp target/x86_64-apple-darwin/release/lemur output/lemur-osx-intel
# Bundle for MacOS Intel
cargo bundle --release --target x86_64-apple-darwin --format osx
genisoimage -V lemur -D -R -apple -no-pad -o output/Lemur-Intel.dmg target/x86_64-apple-darwin/release/bundle/osx
# Build for MacOS Apple Silicon
cargo build --release --target aarch64-apple-darwin
cp target/aarch64-apple-darwin/release/lemur output/lemur-osx-apple-silicon
# Bundle for MacOS Apple Silicon
cargo bundle --release --target aarch64-apple-darwin --format osx
genisoimage -V lemur -D -R -apple -no-pad -o output/Lemur-Apple-Silicon.dmg target/aarch64-apple-darwin/release/bundle/osx
# Clean up after ourselves
cargo clean
-72
View File
@@ -1,72 +0,0 @@
set -e
version=$(cat Cargo.toml | sed -n -e '/version/ {s/.* = *//p;q}' | tr -d '"')
read -p "You wanted to release $version, right? [Y/n] " -n 1 -r
echo
case "$REPLY" in
n|N ) exit 1;;
esac
if [ -z "${RELEASE_TOKEN}" ]; then
echo "Please set the RELEASE_TOKEN env var."
exit 1
fi
if ! command -v curl 2>&1 >/dev/null; then
echo "Please install curl."
exit 1
fi
if ! command -v jq 2>&1 >/dev/null; then
echo "Please install jq."
exit 1
fi
if ! command -v docker 2>&1 >/dev/null; then
echo "Please install docker."
exit 1
fi
docker build -f build.Dockerfile -t lemur-build .
MSYS_NO_PATHCONV=1 docker run -it --rm -v .:/app -w /app --entrypoint bash lemur-build /app/scripts/do-bundle.sh
body=$(cat <<EOF
## How to install
The emulator can be found in the "Downloads" section of this release.
### Windows users
Download \`lemur.exe\`.
### MacOS users
If your Mac uses an Intel processor, download and install \`Lemur-Intel.dmg\`.
If it uses Apple Silicon, download and install \`Lemur-Apple-Silicon.dmg\`.
If you're not sure which to choose, use [this guide](https://support.apple.com/en-us/116943) to find out.
### Linux users
You can either download and run \`lemur-linux\`, or download and install the attached .deb file.
EOF
)
payload=$(cat <<EOF
{
"body": $(echo "$body" | jq -Rsa .),
"draft": false,
"name": "v${version}",
"prerelease": false,
"tag_name": "v${version}"
}
EOF
)
echo "Creating release..."
response=$(curl -s --json "$payload" "https://git.virtual-boy.com/api/v1/repos/PVB/lemur/releases?token=$RELEASE_TOKEN")
echo "$response"
upload_url=$(echo "$response" | jq -r '.upload_url')
for file in output/*; do
echo "Uploading $(basename "$file")..."
upload_res=$(curl -s -F "attachment=@$file" "$upload_url?name=$(basename "$file")&token=$RELEASE_TOKEN")
echo "$upload_res"
done
+14 -22
View File
@@ -18,7 +18,7 @@ use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId},
input::MappingProvider,
persistence::Persistence,
window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow},
window::{AboutWindow, AppWindow, GameWindow, InputWindow},
};
fn load_icon() -> anyhow::Result<IconData> {
@@ -183,10 +183,6 @@ impl ApplicationHandler<UserEvent> for Application {
let about = AboutWindow;
self.open(event_loop, Box::new(about));
}
UserEvent::OpenDebugger(sim_id) => {
let debugger = GdbServerWindow::new(sim_id, self.client.clone());
self.open(event_loop, Box::new(debugger));
}
UserEvent::OpenInput => {
let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input));
@@ -235,13 +231,22 @@ impl Viewport {
icon: Option<Arc<IconData>>,
mut app: Box<dyn AppWindow>,
) -> Self {
let mut painter = egui_wgpu::winit::Painter::new(
egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
..egui_wgpu::WgpuConfiguration::default()
},
1,
None,
false,
true,
);
let ctx = Context::default();
let mut fonts = FontDefinitions::default();
fonts.font_data.insert(
"Selawik".into(),
Arc::new(FontData::from_static(include_bytes!(
"../assets/selawik.ttf"
))),
FontData::from_static(include_bytes!("../assets/selawik.ttf")),
);
fonts
.families
@@ -255,18 +260,6 @@ impl Viewport {
});
egui_extras::install_image_loaders(&ctx);
let mut painter = egui_wgpu::winit::Painter::new(
ctx.clone(),
egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync,
..egui_wgpu::WgpuConfiguration::default()
},
1,
None,
false,
true,
);
let mut info = ViewportInfo::default();
let mut builder = app.initial_viewport();
if let Some(icon) = icon {
@@ -332,7 +325,7 @@ impl Viewport {
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&output.textures_delta,
vec![],
false,
);
self.state
@@ -378,7 +371,6 @@ impl Drop for Viewport {
pub enum UserEvent {
GamepadEvent(gilrs::Event),
OpenAbout,
OpenDebugger(SimId),
OpenInput,
OpenPlayer2,
}
+49 -66
View File
@@ -1,6 +1,5 @@
use std::{
collections::HashMap,
fmt::Display,
fs::{self, File},
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
@@ -46,14 +45,6 @@ impl SimId {
}
}
}
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Player1 => "Player 1",
Self::Player2 => "Player 2",
})
}
}
struct Cart {
rom_path: PathBuf,
@@ -150,8 +141,6 @@ pub struct Emulator {
linked: Arc<AtomicBool>,
renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>,
eye_contents: Vec<u8>,
audio_samples: Vec<f32>,
}
impl Emulator {
@@ -175,8 +164,6 @@ impl Emulator {
linked,
renderers: HashMap::new(),
messages: HashMap::new(),
eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
})
}
@@ -270,8 +257,56 @@ impl Emulator {
}
pub fn run(&mut self) {
let mut eye_contents = vec![0u8; 384 * 224 * 2];
let mut audio_samples = vec![];
loop {
let idle = self.tick();
let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
let mut idle = p1_running || p2_running;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate();
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate();
}
for sim_id in SimId::values() {
let Some(renderer) = self.renderers.get_mut(&sim_id) else {
continue;
};
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if sim.read_pixels(&mut eye_contents) {
idle = false;
if renderer.queue_render(&eye_contents).is_err() {
self.renderers.remove(&sim_id);
}
}
}
let p1_audio =
p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_audio =
p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
if p1_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
sim.read_samples(&mut audio_samples, weight);
}
}
if p2_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
sim.read_samples(&mut audio_samples, weight);
}
}
if audio_samples.is_empty() {
audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
} else {
idle = false;
}
self.audio.update(&audio_samples);
audio_samples.clear();
if idle {
// The game is paused, and we have output all the video/audio we have.
// Block the thread until a new command comes in.
@@ -296,58 +331,6 @@ impl Emulator {
}
}
// returns true if the emulator is "idle" (i.e. this didn't output anything)
pub fn tick(&mut self) -> bool {
let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire);
let mut idle = p1_running || p2_running;
if p1_running && p2_running {
Sim::emulate_many(&mut self.sims);
} else if p1_running {
self.sims[SimId::Player1.to_index()].emulate();
} else if p2_running {
self.sims[SimId::Player2.to_index()].emulate();
}
for sim_id in SimId::values() {
let Some(renderer) = self.renderers.get_mut(&sim_id) else {
continue;
};
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
continue;
};
if sim.read_pixels(&mut self.eye_contents) {
idle = false;
if renderer.queue_render(&self.eye_contents).is_err() {
self.renderers.remove(&sim_id);
}
}
}
let p1_audio =
p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire);
let p2_audio =
p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire);
let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 };
if p1_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) {
sim.read_samples(&mut self.audio_samples, weight);
}
}
if p2_audio {
if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) {
sim.read_samples(&mut self.audio_samples, weight);
}
}
if self.audio_samples.is_empty() {
self.audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0);
} else {
idle = false;
}
self.audio.update(&self.audio_samples);
self.audio_samples.clear();
idle
}
fn handle_command(&mut self, command: EmulatorCommand) {
match command {
EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => {
-294
View File
@@ -1,294 +0,0 @@
use anyhow::{bail, Result};
use std::{
sync::{Arc, Mutex},
thread,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt as _, BufReader, BufWriter},
net::{
tcp::{OwnedReadHalf, OwnedWriteHalf},
TcpListener, TcpStream,
},
select,
sync::oneshot,
};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
pub struct GdbServer {
sim_id: SimId,
client: EmulatorClient,
status: Arc<Mutex<GdbServerStatus>>,
killer: Option<oneshot::Sender<()>>,
}
impl GdbServer {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
client,
status: Arc::new(Mutex::new(GdbServerStatus::Stopped)),
killer: None,
}
}
pub fn status(&self) -> GdbServerStatus {
self.status.lock().unwrap().clone()
}
pub fn start(&mut self, port: u16) {
let sim_id = self.sim_id;
let client = self.client.clone();
let status = self.status.clone();
let (tx, rx) = oneshot::channel();
self.killer = Some(tx);
thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
select! {
_ = run_server(sim_id, client, port, &status) => {}
_ = rx => {
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
}
})
});
}
pub fn stop(&mut self) {
if let Some(killer) = self.killer.take() {
let _ = killer.send(());
}
}
}
async fn run_server(
sim_id: SimId,
client: EmulatorClient,
port: u16,
status: &Mutex<GdbServerStatus>,
) {
client.send_command(EmulatorCommand::Pause);
let Some(stream) = try_connect(port, status).await else {
return;
};
let connection = GdbConnection::new(sim_id, client, stream);
match connection.run().await {
Ok(()) => {
*status.lock().unwrap() = GdbServerStatus::Stopped;
}
Err(error) => {
*status.lock().unwrap() = GdbServerStatus::Error(error.to_string());
}
}
}
async fn try_connect(port: u16, status: &Mutex<GdbServerStatus>) -> Option<TcpStream> {
*status.lock().unwrap() = GdbServerStatus::Connecting;
let listener = match TcpListener::bind(("127.0.0.1", port)).await {
Ok(l) => l,
Err(err) => {
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
return None;
}
};
match listener.accept().await {
Ok((stream, _)) => {
*status.lock().unwrap() = GdbServerStatus::Running;
Some(stream)
}
Err(err) => {
*status.lock().unwrap() = GdbServerStatus::Error(err.to_string());
None
}
}
}
#[derive(Clone)]
pub enum GdbServerStatus {
Stopped,
Connecting,
Running,
Error(String),
}
impl GdbServerStatus {
pub fn running(&self) -> bool {
matches!(self, Self::Connecting | Self::Running)
}
}
struct GdbConnection {
sim_id: SimId,
client: EmulatorClient,
stream_in: BufReader<OwnedReadHalf>,
stream_out: BufWriter<OwnedWriteHalf>,
ack_messages: bool,
}
impl GdbConnection {
fn new(sim_id: SimId, client: EmulatorClient, stream: TcpStream) -> Self {
let (rx, tx) = stream.into_split();
Self {
sim_id,
client,
stream_in: BufReader::new(rx),
stream_out: BufWriter::new(tx),
ack_messages: true,
}
}
async fn run(mut self) -> Result<()> {
println!("Connected for {}", self.sim_id);
self.client.send_command(EmulatorCommand::Resume);
loop {
let message = self.read_message().await?;
println!("received {:?}", message);
let mut res = ResponseWriter::new(&mut self.stream_out);
res.init(self.ack_messages).await?;
let body = match &message {
Message::String(str) => str.as_str(),
Message::Signal => {
// TODO: handle this
res.send().await?;
continue;
}
};
if body == "QStartNoAckMode" {
self.ack_messages = false;
res.send_ok().await?;
} else if body.starts_with("qSupported:") {
res.write_str("multiprocess+;swbreak+;vContSupported+")
.await?;
res.send().await?;
} else if body == "QThreadSuffixSupported" || body == "QListThreadsInStopReply" {
res.send_ok().await?;
} else if body == "qHostInfo" || body == "qProcessInfo" {
res.write_str(&format!(
"triple:{};endian:little;ptrsize:4;",
hex::encode("v810-unknown-vb")
))
.await?;
res.send().await?;
} else if body == "k" {
return Ok(());
} else {
// unrecognized command
res.send().await?;
}
}
}
async fn read_message(&mut self) -> Result<Message> {
let mut char = self.read_byte().await?;
while char == b'+' {
// just ignore positive acks
char = self.read_byte().await?;
}
if char == b'-' {
bail!("no support for negative acks");
}
if char == 0x03 {
// This is how the client "cancels an in-flight request"
return Ok(Message::Signal);
}
if char != b'$' {
// Messages are supposed to start with a dollar sign
bail!("malformed message");
}
// now read the body
let mut checksum = 0u8;
let mut body = vec![];
char = self.read_byte().await?;
while char != b'#' {
if char == b'}' {
// escape character
checksum = checksum.wrapping_add(char);
char = self.read_byte().await?;
checksum = checksum.wrapping_add(char);
body.push(char ^ 0x20);
} else {
checksum = checksum.wrapping_add(char);
body.push(char);
}
char = self.read_byte().await?;
}
let mut checksum_bytes = [b'0'; 2];
self.stream_in.read_exact(&mut checksum_bytes).await?;
let checksum_str = std::str::from_utf8(&checksum_bytes)?;
let real_checksum = u8::from_str_radix(checksum_str, 16)?;
if checksum != real_checksum {
bail!("invalid checksum");
}
let string = String::from_utf8(body)?;
Ok(Message::String(string))
}
async fn read_byte(&mut self) -> std::io::Result<u8> {
self.stream_in.read_u8().await
}
}
struct ResponseWriter<'a> {
inner: &'a mut BufWriter<OwnedWriteHalf>,
checksum: u8,
}
impl<'a> ResponseWriter<'a> {
fn new(inner: &'a mut BufWriter<OwnedWriteHalf>) -> Self {
Self { inner, checksum: 0 }
}
async fn init(&mut self, ack: bool) -> std::io::Result<()> {
if ack {
self.inner.write_u8(b'+').await?;
}
self.inner.write_u8(b'$').await
}
async fn write_str(&mut self, str: &str) -> std::io::Result<()> {
for byte in str.bytes() {
self.checksum = self.checksum.wrapping_add(byte);
}
self.inner.write_all(str.as_bytes()).await
}
async fn write_hex_u8(&mut self, value: u8) -> std::io::Result<()> {
for digit in [(value >> 4), (value & 0xf)] {
let char = if digit > 9 {
b'a' + digit - 10
} else {
b'0' + digit
};
self.checksum = self.checksum.wrapping_add(char);
self.inner.write_u8(char).await?;
}
Ok(())
}
async fn send_ok(mut self) -> std::io::Result<()> {
self.write_str("OK").await?;
self.send().await
}
async fn send(mut self) -> std::io::Result<()> {
let final_checksum = self.checksum;
self.inner.write_u8(b'#').await?;
self.write_hex_u8(final_checksum).await?;
println!("{:?}", std::str::from_utf8(self.inner.buffer()));
self.inner.flush().await
}
}
#[derive(Debug)]
enum Message {
String(String),
Signal,
}
+1 -44
View File
@@ -1,7 +1,4 @@
// hide console in release mode
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::{path::PathBuf, process, time::SystemTime};
use std::{path::PathBuf, process};
use anyhow::Result;
use app::Application;
@@ -14,7 +11,6 @@ mod app;
mod audio;
mod controller;
mod emulator;
mod gdbserver;
mod graphics;
mod input;
mod persistence;
@@ -25,43 +21,6 @@ struct Args {
rom: Option<PathBuf>,
}
fn set_panic_handler() {
std::panic::set_hook(Box::new(|info| {
let mut message = String::new();
if let Some(msg) = info.payload().downcast_ref::<&str>() {
message += &format!("{}\n", msg);
} else if let Some(msg) = info.payload().downcast_ref::<String>() {
message += &format!("{}\n", msg);
}
if let Some(location) = info.location() {
message += &format!(
" in file '{}' at line {}\n",
location.file(),
location.line()
);
}
let backtrace = std::backtrace::Backtrace::force_capture();
message += &format!("stack trace:\n{:#}\n", backtrace);
eprint!("{}", message);
let Some(project_dirs) = directories::ProjectDirs::from("com", "virtual-boy", "Lemur")
else {
return;
};
let data_dir = project_dirs.data_dir();
if std::fs::create_dir_all(data_dir).is_err() {
return;
}
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis();
let logfile_name = format!("crash-{}.txt", timestamp);
let _ = std::fs::write(data_dir.join(logfile_name), message);
}));
}
#[cfg(windows)]
fn set_process_priority_to_high() -> Result<()> {
use windows::Win32::{Foundation, System::Threading};
@@ -72,8 +31,6 @@ fn set_process_priority_to_high() -> Result<()> {
}
fn main() -> Result<()> {
set_panic_handler();
#[cfg(windows)]
set_process_priority_to_high()?;
-2
View File
@@ -1,14 +1,12 @@
pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use input::InputWindow;
use winit::event::KeyEvent;
mod about;
mod game;
mod game_screen;
mod gdb;
mod input;
pub trait AppWindow {
-8
View File
@@ -121,14 +121,6 @@ impl GameWindow {
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("GDB Server").clicked() {
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("About", |ui| {
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
+3 -3
View File
@@ -63,13 +63,13 @@ impl GameScreen {
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
entry_point: "vs_main",
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some(entry_point),
entry_point,
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Bgra8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
@@ -235,7 +235,7 @@ impl egui_wgpu::CallbackTrait for GameScreenCallback {
.get(&self.display_mode)
.unwrap_or_else(|| panic!("Unrecognized display mode {:?}", self.display_mode));
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, Some(self.bind_group.as_ref()), &[]);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.set_viewport(x, y, w, h, 0.0, 1.0);
render_pass.draw(0..6, 0..1);
}
-80
View File
@@ -1,80 +0,0 @@
use egui::{Button, CentralPanel, TextEdit, ViewportBuilder, ViewportId};
use crate::{
emulator::{EmulatorClient, SimId},
gdbserver::{GdbServer, GdbServerStatus},
};
use super::AppWindow;
pub struct GdbServerWindow {
sim_id: SimId,
port_str: String,
server: GdbServer,
}
impl GdbServerWindow {
pub fn new(sim_id: SimId, client: EmulatorClient) -> Self {
Self {
sim_id,
port_str: (8080 + sim_id.to_index()).to_string(),
server: GdbServer::new(sim_id, client),
}
}
}
impl AppWindow for GdbServerWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("Debugger-{}", self.sim_id))
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("GDB Server ({})", self.sim_id))
.with_inner_size((300.0, 200.0))
}
fn show(&mut self, ctx: &egui::Context) {
let port_num: Option<u16> = self.port_str.parse().ok();
let status = self.server.status();
CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if port_num.is_none() {
let style = ui.style_mut();
let error = style.visuals.error_fg_color;
style.visuals.widgets.active.bg_stroke.color = error;
style.visuals.widgets.hovered.bg_stroke.color = error;
}
ui.label("Port");
let port_editor = TextEdit::singleline(&mut self.port_str).desired_width(100.0);
ui.add_enabled(!status.running(), port_editor);
});
if !status.running() {
let start_button = Button::new("Start");
if ui.add_enabled(port_num.is_some(), start_button).clicked() {
let port = port_num.unwrap();
self.server.start(port);
}
} else {
let stop_button = Button::new("Stop");
if ui.add(stop_button).clicked() {
self.server.stop();
}
}
match &status {
GdbServerStatus::Stopped => {}
GdbServerStatus::Connecting => {
ui.label("Connecting...");
}
GdbServerStatus::Running => {
ui.label("Running");
}
GdbServerStatus::Error(message) => {
ui.label(message);
}
}
});
}
}