Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b94ae4d586 | ||
|
|
014b9ebbfa | ||
|
|
0a5e223ba7 | ||
|
|
47a05968fb | ||
|
|
1a54c2e6fc | ||
|
|
d109c71c15 | ||
|
|
1628a45f7d | ||
|
|
cebdcfe220 | ||
|
|
cb05a1e5e6 | ||
|
|
8113b003f7 | ||
|
|
2e8b39a74d | ||
|
|
1c730b3d77 | ||
|
|
249a44e278 | ||
|
|
826bc23a41 | ||
|
|
a13c1e622f | ||
|
|
becf3517e6 | ||
|
|
12a299f622 | ||
|
|
6dbb4b34c5 | ||
|
|
ba5d1ce8ac | ||
|
|
8e7ca9acb6 | ||
|
|
f4438e32e0 | ||
|
|
953e10b42b | ||
|
|
5c97c1b0f2 | ||
|
|
fe7ffd454f | ||
|
|
06fddc43c3 | ||
|
|
3c4b89a8da | ||
|
|
78289193c7 | ||
|
|
bc84d88b5f | ||
|
|
ba7a2c47b0 |
@@ -4,6 +4,7 @@
|
||||
*.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
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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
+251
-216
File diff suppressed because it is too large
Load Diff
+14
-8
@@ -1,7 +1,10 @@
|
||||
[package]
|
||||
name = "lemur"
|
||||
description = "An emulator for the Virtual Boy."
|
||||
version = "0.1.0"
|
||||
repository = "https://git.virtual-boy.com/PVB/lemur"
|
||||
publish = false
|
||||
license = "MIT"
|
||||
version = "0.2.5"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -11,12 +14,13 @@ 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.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"] }
|
||||
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"] }
|
||||
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"
|
||||
@@ -29,7 +33,8 @@ rubato = "0.16"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thread-priority = "1"
|
||||
wgpu = "22.1"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync"] }
|
||||
wgpu = "23"
|
||||
winit = { version = "0.30", features = ["serde"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@@ -37,6 +42,7 @@ windows = { version = "0.58", features = ["Win32_System_Threading"] }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
winresource = "0.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@@ -45,4 +51,4 @@ lto = true
|
||||
name = "Lemur"
|
||||
identifier = "com.virtual-boy.Lemur"
|
||||
icon = ["assets/lemur-256x256.png"]
|
||||
category = "games"
|
||||
category = "games"
|
||||
|
||||
@@ -12,4 +12,14 @@ Run
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable will be in `target/release/lemur[.exe]`
|
||||
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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
+17
-6
@@ -6,18 +6,29 @@ 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 lld libc6-dev libasound2-dev libudev-dev genisoimage && \
|
||||
cargo install cargo-bundle xwin
|
||||
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++
|
||||
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" CXX="clang++" AR="llvm-ar-14" \
|
||||
CC="clang-19" CXX="clang++-19" AR="llvm-ar-19" \
|
||||
CC_x86_64-apple-darwin="o64-clang" \
|
||||
CXX_x86_64-apple-darwin="o64-clang++" \
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link" \
|
||||
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_APPLE_DARWIN_LINKER="o64-clang" \
|
||||
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-14" \
|
||||
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" \
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::path::Path;
|
||||
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()?;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=shrooms-vb-core");
|
||||
cc::Build::new()
|
||||
.include(Path::new("shrooms-vb-core/core"))
|
||||
@@ -11,4 +17,6 @@ fn main() {
|
||||
.define("VB_DIV_GENERIC", None)
|
||||
.file(Path::new("shrooms-vb-core/core/vb.c"))
|
||||
.compile("vb");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
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
|
||||
+1
-1
Submodule shrooms-vb-core updated: 18b2c589e6...06849b54ba
+22
-14
@@ -18,7 +18,7 @@ use crate::{
|
||||
emulator::{EmulatorClient, EmulatorCommand, SimId},
|
||||
input::MappingProvider,
|
||||
persistence::Persistence,
|
||||
window::{AboutWindow, AppWindow, GameWindow, InputWindow},
|
||||
window::{AboutWindow, AppWindow, GameWindow, GdbServerWindow, InputWindow},
|
||||
};
|
||||
|
||||
fn load_icon() -> anyhow::Result<IconData> {
|
||||
@@ -183,6 +183,10 @@ 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));
|
||||
@@ -231,22 +235,13 @@ 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(),
|
||||
FontData::from_static(include_bytes!("../assets/selawik.ttf")),
|
||||
Arc::new(FontData::from_static(include_bytes!(
|
||||
"../assets/selawik.ttf"
|
||||
))),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
@@ -260,6 +255,18 @@ 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 {
|
||||
@@ -325,7 +332,7 @@ impl Viewport {
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
&clipped_primitives,
|
||||
&output.textures_delta,
|
||||
false,
|
||||
vec![],
|
||||
);
|
||||
|
||||
self.state
|
||||
@@ -371,6 +378,7 @@ impl Drop for Viewport {
|
||||
pub enum UserEvent {
|
||||
GamepadEvent(gilrs::Event),
|
||||
OpenAbout,
|
||||
OpenDebugger(SimId),
|
||||
OpenInput,
|
||||
OpenPlayer2,
|
||||
}
|
||||
|
||||
+66
-49
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
fs::{self, File},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
@@ -45,6 +46,14 @@ 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,
|
||||
@@ -141,6 +150,8 @@ 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 {
|
||||
@@ -164,6 +175,8 @@ impl Emulator {
|
||||
linked,
|
||||
renderers: HashMap::new(),
|
||||
messages: HashMap::new(),
|
||||
eye_contents: vec![0u8; 384 * 224 * 2],
|
||||
audio_samples: vec![0.0; EXPECTED_FRAME_SIZE],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -257,56 +270,8 @@ impl Emulator {
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
let mut eye_contents = vec![0u8; 384 * 224 * 2];
|
||||
let mut audio_samples = vec![];
|
||||
loop {
|
||||
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();
|
||||
let idle = self.tick();
|
||||
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.
|
||||
@@ -331,6 +296,58 @@ 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) => {
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
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,
|
||||
}
|
||||
+44
-1
@@ -1,4 +1,7 @@
|
||||
use std::{path::PathBuf, process};
|
||||
// hide console in release mode
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::{path::PathBuf, process, time::SystemTime};
|
||||
|
||||
use anyhow::Result;
|
||||
use app::Application;
|
||||
@@ -11,6 +14,7 @@ mod app;
|
||||
mod audio;
|
||||
mod controller;
|
||||
mod emulator;
|
||||
mod gdbserver;
|
||||
mod graphics;
|
||||
mod input;
|
||||
mod persistence;
|
||||
@@ -21,6 +25,43 @@ 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};
|
||||
@@ -31,6 +72,8 @@ fn set_process_priority_to_high() -> Result<()> {
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
set_panic_handler();
|
||||
|
||||
#[cfg(windows)]
|
||||
set_process_priority_to_high()?;
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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 {
|
||||
|
||||
@@ -121,6 +121,14 @@ 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();
|
||||
|
||||
@@ -63,13 +63,13 @@ impl GameScreen {
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point,
|
||||
entry_point: Some(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, &self.bind_group, &[]);
|
||||
render_pass.set_bind_group(0, Some(self.bind_group.as_ref()), &[]);
|
||||
render_pass.set_viewport(x, y, w, h, 0.0, 1.0);
|
||||
render_pass.draw(0..6, 0..1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user