Compare commits

...

31 Commits

Author SHA1 Message Date
Logan McNaughton b3297f82b2 cargo update 2025-08-24 10:58:01 +02:00
Logan McNaughton 2127705e7b set deadzon max to 99 2025-08-24 07:29:07 +02:00
Logan McNaughton cccb079d63 cleanup deadzone code (#558) 2025-08-24 00:05:03 +02:00
Logan McNaughton c52b680753 use saturating abs for deadzone (#557) 2025-08-23 22:52:04 +02:00
Logan McNaughton 8799650245 add deadzone option (#554)
* add deadzone option

* add deadzone calc

* more
2025-08-23 15:36:03 +02:00
Logan McNaughton 90be2130c1 move cheats back 2025-08-23 09:36:47 +02:00
Logan McNaughton 96af33c434 Split render frame and update screen, move pause loop (#556)
* split render and update screen functions

* move pause loop
2025-08-23 08:54:47 +02:00
Logan McNaughton 0a42152bcb move cheats 2025-08-22 22:42:09 +02:00
Logan McNaughton db335e24d2 render frame before speed limiter (#555) 2025-08-22 07:59:28 +02:00
Logan McNaughton 4857b86897 bump to 1.1.5 (#553) 2025-08-21 15:22:19 +02:00
Logan McNaughton 26022fff72 cargo update 2025-08-21 15:03:50 +02:00
Logan McNaughton 658dede9e9 use dependabot to update rust (#552) 2025-08-20 10:26:26 +02:00
Logan McNaughton 6ad1a31e34 correct joystick hat diagonal input (#551) 2025-08-20 09:15:41 +02:00
Logan McNaughton 4476d476de add frame advance (#549) 2025-08-18 10:42:29 +02:00
Logan McNaughton 8037fb426e UNF loader support (#526) 2025-08-18 08:44:12 +02:00
Logan McNaughton f0381a2cd1 add uppercase file extensions (#548) 2025-08-17 21:01:23 +02:00
Logan McNaughton c8e70e6125 cargo update 2025-08-17 20:56:02 +02:00
dependabot[bot] e9664b1087 Bump actions/checkout from 4 to 5 (#545)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 13:02:31 +02:00
Logan McNaughton 3f72700da0 bump to 1.1.4 (#541) 2025-08-07 19:21:54 +02:00
Logan McNaughton 9fcd2835eb Update to rust 1.89.0 (#540) 2025-08-07 13:17:39 +02:00
Logan McNaughton c6767c94a6 move sse2neon to submodule (#539) 2025-08-07 12:17:50 +02:00
Logan McNaughton f1968e3a58 use NearestClamp for image 2025-08-06 23:02:08 +02:00
Logan McNaughton 23fdfff1ac some clippy fixes (#538)
* some clippy fixes

* more
2025-08-06 12:18:40 +02:00
Logan McNaughton fc512d9517 fix netplay for unknown ROMs 2025-08-06 09:12:35 +02:00
dependabot[bot] 3c317460fd Bump signpath/github-action-submit-signing-request from 1.2 to 1.3 (#537)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.2 to 1.3.
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.2...v1.3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '1.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 08:56:09 +02:00
Logan McNaughton b1fcaa5cf4 add minimum size to combobox 2025-08-05 21:55:41 +02:00
Logan McNaughton 98c9cf142d slight improvment to combobox sizes 2025-08-05 21:40:31 +02:00
Logan McNaughton 1eb1503981 Onscreen drawing (#536)
* onscreen messages

* more

* more

* more

* more

* use sdl_ttf

* more

* more

* more

* more

* test

* mroe

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-08-05 15:59:49 +02:00
Logan McNaughton 5aa9a65e7b add support for save state slots (#534)
* add support for save state slots

* add log
2025-08-04 14:59:50 +02:00
Logan McNaughton 71f1d98132 bump to 1.1.3 (#533) 2025-08-03 13:30:27 +02:00
Logan McNaughton 0037c0f344 Don't wait for signpath completion 2025-08-03 11:56:12 +02:00
48 changed files with 1103 additions and 9996 deletions
+5 -1
View File
@@ -3,7 +3,11 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
- package-ecosystem: "rust-toolchain"
directory: "/"
schedule:
interval: "weekly"
# - package-ecosystem: "cargo"
# directory: "/"
# schedule:
+5 -4
View File
@@ -29,7 +29,7 @@ jobs:
arch: linux-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -81,7 +81,7 @@ jobs:
# arch: windows-aarch64
# llvm: woa64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -110,7 +110,7 @@ jobs:
path: output/gopher64-${{ matrix.arch }}.exe
- name: SignPath
if: ${{ github.ref_type == 'tag' }}
uses: signpath/github-action-submit-signing-request@v1.2
uses: signpath/github-action-submit-signing-request@v1.3
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
@@ -118,6 +118,7 @@ jobs:
artifact-configuration-slug: "gopher64-${{ matrix.arch }}"
signing-policy-slug: "release-signing"
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
wait-for-completion: false
build-macos:
runs-on: macos-15
@@ -130,7 +131,7 @@ jobs:
- target: aarch64-apple-darwin
arch: mac-aarch64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
lint-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
checks: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: rustsec/audit-check@v2
+3
View File
@@ -2,3 +2,6 @@
path = parallel-rdp/parallel-rdp-standalone
url = https://github.com/gopher64/parallel-rdp-standalone.git
branch = gopher64
[submodule "src/compat/sse2neon"]
path = src/compat/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
Generated
+287 -266
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,8 +1,8 @@
[package]
name = "gopher64"
version = "1.1.2"
version = "1.1.5"
edition = "2024"
rust-version = "1.88"
rust-version = "1.89"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -18,8 +18,8 @@ serde-big-array = "0.5"
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
open = "5.3"
sha2 = "0.10"
ab_glyph = "0.2"
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.2", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
spin_sleep = "1.3"
+7 -1
View File
@@ -64,6 +64,10 @@ fn main() {
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").to_owned().unwrap())
.join("include"),
)
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").to_owned().unwrap())
.join("include"),
);
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
@@ -107,7 +111,9 @@ fn main() {
.allowlist_function("rdp_close")
.allowlist_function("rdp_set_vi_register")
.allowlist_function("rdp_update_screen")
.allowlist_function("rdp_render_frame")
.allowlist_function("rdp_process_commands")
.allowlist_function("rdp_onscreen_message")
.allowlist_function("rdp_check_callback")
.allowlist_function("rdp_new_processor")
.allowlist_function("rdp_state_size")
@@ -123,7 +129,7 @@ fn main() {
if arch == "aarch64" {
let simd_bindings = bindgen::Builder::default()
.header("src/compat/sse2neon/v1.8.0/sse2neon.h")
.header("src/compat/sse2neon/sse2neon.h")
.allowlist_function("_mm_setzero_si128")
.allowlist_function("_mm_set_epi8")
.allowlist_function("_mm_movemask_epi8")
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+101 -8
View File
@@ -5,6 +5,7 @@
#include "spirv.hpp"
#include "spirv_crt.hpp"
#include <SDL3/SDL_vulkan.h>
#include <SDL3_ttf/SDL_ttf.h>
using namespace Vulkan;
@@ -74,6 +75,12 @@ static GFX_INFO gfx_info;
static const uint32_t *fragment_spirv;
static size_t fragment_size;
static TTF_Font *message_font;
static std::queue<std::string> messages;
static uint64_t message_timer;
#define MESSAGE_TIME 3000 // 3 seconds
typedef struct
{
float SourceSize[4];
@@ -200,6 +207,27 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
case SDL_SCANCODE_RIGHTBRACKET:
callback.raise_volume = true;
break;
case SDL_SCANCODE_SLASH:
callback.frame_advance = true;
break;
case SDL_SCANCODE_0:
case SDL_SCANCODE_1:
case SDL_SCANCODE_2:
case SDL_SCANCODE_3:
case SDL_SCANCODE_4:
case SDL_SCANCODE_5:
case SDL_SCANCODE_6:
case SDL_SCANCODE_7:
case SDL_SCANCODE_8:
case SDL_SCANCODE_9:
if (event->key.mod & SDL_KMOD_ALT)
{
if (event->key.scancode == SDL_SCANCODE_0)
callback.save_state_slot = 0;
else
callback.save_state_slot = event->key.scancode - SDL_SCANCODE_1 + 1;
}
break;
default:
break;
}
@@ -232,7 +260,23 @@ void rdp_new_processor(GFX_INFO _gfx_info)
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0, gfx_info.RDRAM_SIZE, gfx_info.RDRAM_SIZE / 2, flags);
}
void rdp_init(void *_window, GFX_INFO _gfx_info)
static ImageHandle create_message_image(Vulkan::Device &device, int width, const char *message)
{
SDL_Color fg = {255, 255, 255, 255};
SDL_Color bg = {0, 0, 0, 0};
SDL_Surface *surface = TTF_RenderText_LCD_Wrapped(message_font, message, 0, fg, bg, width);
ImageCreateInfo info = ImageCreateInfo::immutable_2d_image(surface->w, surface->h, VK_FORMAT_B8G8R8A8_UNORM, false);
ImageInitialData initial_data = {};
initial_data.data = surface->pixels;
initial_data.row_length = surface->pitch / 4;
initial_data.image_height = surface->h;
ImageHandle handle = device.create_image(info, &initial_data);
SDL_DestroySurface(surface);
return handle;
}
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, size_t font_size)
{
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
@@ -269,33 +313,51 @@ void rdp_init(void *_window, GFX_INFO _gfx_info)
if (!::Vulkan::Context::init_loader((PFN_vkGetInstanceProcAddr)SDL_Vulkan_GetVkGetInstanceProcAddr()))
{
rdp_close();
return;
}
if (!wsi->init_simple(1, handles))
{
rdp_close();
return;
}
rdp_new_processor(gfx_info);
if (!processor->device_is_supported())
{
delete processor;
delete wsi;
processor = nullptr;
rdp_close();
return;
}
message_font = TTF_OpenFontIO(SDL_IOFromConstMem(font, font_size), true, 30.0);
if (!message_font)
{
rdp_close();
return;
}
wsi->begin_frame();
callback.emu_running = true;
callback.enable_speedlimiter = true;
callback.paused = false;
callback.save_state_slot = 0;
crop_letterbox = false;
messages = std::queue<std::string>();
message_timer = 0;
}
void rdp_close()
{
wsi->end_frame();
if (wsi)
wsi->end_frame();
if (message_font)
{
TTF_CloseFont(message_font);
message_font = nullptr;
}
if (processor)
{
delete processor;
@@ -425,11 +487,30 @@ static void render_frame(Vulkan::Device &device)
cmd->push_constants(&push, 0, sizeof(push));
}
cmd->set_texture(0, 0, image->get_view(), Vulkan::StockSampler::LinearClamp);
cmd->set_texture(0, 0, image->get_view(), Vulkan::StockSampler::NearestClamp);
cmd->set_viewport(vp);
// The vertices are constants in the shader.
// Draws fullscreen quad using oversized triangle.
cmd->draw(3);
if (!messages.empty())
{
Vulkan::ImageHandle message_image = create_message_image(device, vp.width, messages.front().c_str());
cmd->set_texture(0, 0, message_image->get_view(), Vulkan::StockSampler::NearestClamp);
vp.x = vp.x + (vp.width - message_image->get_width()) / 2;
vp.y = vp.y + vp.height - message_image->get_height();
vp.height = message_image->get_height();
vp.width = message_image->get_width();
cmd->set_viewport(vp);
cmd->draw(3);
if (SDL_GetTicks() > message_timer)
{
messages.pop();
message_timer = SDL_GetTicks() + MESSAGE_TIME;
}
}
}
cmd->end_render_pass();
@@ -442,13 +523,17 @@ void rdp_set_vi_register(uint32_t reg, uint32_t value)
processor->set_vi_register(RDP::VIRegister(reg), value);
}
void rdp_update_screen()
void rdp_render_frame()
{
auto &device = wsi->get_device();
render_frame(device);
rdp_device.vi_counter += 1;
}
void rdp_update_screen()
{
wsi->end_frame();
wsi->begin_frame();
rdp_device.vi_counter += 1;
}
CALL_BACK rdp_check_callback()
@@ -458,6 +543,7 @@ CALL_BACK rdp_check_callback()
callback.load_state = false;
callback.lower_volume = false;
callback.raise_volume = false;
callback.frame_advance = false;
return return_value;
}
@@ -476,6 +562,13 @@ void rdp_load_state(const uint8_t *state)
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
}
void rdp_onscreen_message(const char *_message)
{
if (messages.empty())
message_timer = SDL_GetTicks() + MESSAGE_TIME;
messages.push(_message);
}
uint64_t rdp_process_commands()
{
uint64_t interrupt_timer = 0;
+5 -1
View File
@@ -34,14 +34,18 @@ extern "C"
bool lower_volume;
bool raise_volume;
bool paused;
bool frame_advance;
uint32_t save_state_slot;
} CALL_BACK;
void rdp_init(void *_window, GFX_INFO _gfx_info);
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, size_t font_size);
void rdp_close();
void rdp_set_vi_register(uint32_t reg, uint32_t value);
void rdp_update_screen();
void rdp_render_frame();
CALL_BACK rdp_check_callback();
uint64_t rdp_process_commands();
void rdp_onscreen_message(const char *_message);
void rdp_new_processor(GFX_INFO _gfx_info);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.88.0"
channel = "1.89.0"
+1 -1
View File
@@ -71,7 +71,7 @@ pub fn init(
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::audio::play_cheat_event(&device.ui);
ui::video::onscreen_message(&device.ui, "Cheats enabled");
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
#include "src/compat/sse2neon/v1.8.0/sse2neon.h"
#include "src/compat/sse2neon/sse2neon.h"
// Static wrappers
Submodule src/compat/sse2neon added at 3cf69760cc
File diff suppressed because it is too large Load Diff
+1
View File
@@ -347,6 +347,7 @@ impl Device {
sector: 0,
buffer: vec![0; 8192],
writeback_sector: vec![0; 256],
usb_buffer: vec![],
},
flashram: cart::sram::Flashram {
status: 0,
-10
View File
@@ -21,16 +21,6 @@ const JDT_EEPROM_16K: u16 = 0xc000; /* 16k EEPROM */
const EEPROM_BLOCK_SIZE: usize = 8;
pub const EEPROM_MAX_SIZE: usize = 0x800;
#[derive(serde::Serialize, serde::Deserialize)]
pub enum CicType {
CicNus6101,
CicNus6102,
CicNus6103,
CicNus6105,
CicNus6106,
CicNus5167,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct AfRtc {
pub control: u16,
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::device;
use crate::netplay;
use sha2::{Digest, Sha256};
const CART_MASK: usize = 0xFFFFFFF;
pub const CART_MASK: usize = 0xFFFFFFF;
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
let mut data: [u8; 4] = [0; 4];
+127 -2
View File
@@ -28,6 +28,7 @@ pub struct Sc64 {
pub cfg: [u32; SC64_CFG_COUNT as usize],
pub sector: u32,
pub writeback_sector: Vec<u32>,
pub usb_buffer: Vec<u8>,
}
fn format_sdcard(device: &mut device::Device) {
@@ -209,8 +210,132 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
device.ui.storage.saves.sdcard.written = true;
}
'U' | 'u' => {} // USB status, ignored
'M' | 'm' => {} // USB read/write, ignored
'U' => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
}
'u' => {
// used to notify the game that there is data to read
if let Some(cart_rx) = device.ui.usb.cart_rx.as_mut() {
match cart_rx.try_recv() {
Ok(data) => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] =
data.data_type & 0xFF; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] =
data.data_size & 0xFFFFFF; // length
device.cart.sc64.usb_buffer = data.data; // store the data to be read
}
Err(err) => {
match err {
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
panic!("cart_rx lagged: {err}");
}
_ => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
}
}
}
}
} else {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
}
}
'M' => {
// Send data from from flashcart to USB
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let length =
device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize & 0xFFFFFF;
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
let mut usb_buffer = vec![0; length];
let mut i = 0;
if address < device.rdram.size as u64 {
while i < length {
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
.rdram
.mem
.get((address as usize + i) ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
}
} else if address >= device::memory::MM_CART_ROM as u64
&& address < device::memory::MM_PIF_MEM as u64
{
while i < length {
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
.ui
.storage
.saves
.romsave
.data
.get(
&(((address as usize + i)
& device::cart::rom::CART_MASK)
as u32),
)
.unwrap_or(
device
.cart
.rom
.get(
(address as usize + i)
& device::cart::rom::CART_MASK,
)
.unwrap_or(&0),
);
i += 1;
}
} else {
panic!("Unknown address {address:#x} for SC64 M command");
}
ui::usb::send_to_usb(
usb_tx,
ui::usb::UsbData {
data: usb_buffer,
data_type: device.cart.sc64.regs[SC64_DATA1_REG as usize] >> 24,
data_size: device.cart.sc64.regs[SC64_DATA1_REG as usize]
& 0xFFFFFF,
},
);
}
}
'm' => {
// Receive data from USB to flashcart
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let length = device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize;
let mut i = 0;
if address < device.rdram.size as u64 {
while i < length {
*device
.rdram
.mem
.get_mut((address as usize + i) ^ device.byte_swap)
.unwrap_or(&mut 0) =
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0);
i += 1;
}
} else if address >= device::memory::MM_CART_ROM as u64
&& address < device::memory::MM_PIF_MEM as u64
{
while i < length {
device.ui.storage.saves.romsave.data.insert(
((address as usize + i) & device::cart::rom::CART_MASK) as u32,
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0),
);
i += 1;
}
} else {
panic!("Unknown address {address:#x} for SC64 m command");
}
}
'w' => {
// SD card writeback pending
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
+3 -1
View File
@@ -189,20 +189,22 @@ pub fn pak_switch_event(device: &mut device::Device) {
write: device::controller::mempak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "MemPak selected");
} else if new_pak_type == PakType::RumblePak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "RumblePak selected");
} else if new_pak_type == PakType::TransferPak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "TransferPak selected");
}
ui::audio::play_pak_switch(&device.ui, new_pak_type);
channel.change_pak = PakType::None;
}
}
+5 -2
View File
@@ -1,4 +1,4 @@
use crate::{device, savestates};
use crate::{device, savestates, ui};
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
pub enum State {
@@ -227,7 +227,10 @@ pub fn init(device: &mut device::Device) {
device.cpu.clock_rate = if !device.cpu.overclock {
93750000
} else {
println!("Overclocking enabled, setting clock rate to 125 MHz");
ui::video::onscreen_message(
&device.ui,
"Overclocking enabled, setting clock rate to 125 MHz",
);
125000000
};
+10 -2
View File
@@ -137,7 +137,10 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
let mut speed_limiter_toggled = ui::video::check_callback(device);
ui::video::render_frame();
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
if device.netplay.is_some() {
netplay::send_sync_check(device);
@@ -147,11 +150,16 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
}
}
device.vi.vi_counter += 1;
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
speed_limiter(device, speed_limiter_toggled);
}
ui::video::update_screen();
device.vi.vi_counter += 1;
if device.netplay.is_none() && paused {
ui::video::pause_loop();
}
/*
let vis = if device.cart.pal { 50 } else { 60 };
+10 -2
View File
@@ -22,15 +22,22 @@ struct Args {
short,
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)."
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(
short,
long,
help = "Use DirectInput when configuring a new input profile."
help = "Use DirectInput when configuring a new input profile"
)]
use_dinput: bool,
#[arg(
short,
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
deadzone: Option<i32>,
#[arg(
short,
long,
@@ -112,6 +119,7 @@ async fn main() {
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
return;
}
+4 -14
View File
@@ -46,14 +46,6 @@ struct InputEvent {
plugin: u8,
}
pub const NETPLAY_ERROR_DESYNC: u32 = 0;
pub const NETPLAY_ERROR_LOST_CONNECTION: u32 = 1;
pub const NETPLAY_ERROR_PLAYER_DISCONNECTED: u32 = 2;
pub const NETPLAY_ERROR_PLAYER_1_DISCONNECTED: u32 = 3;
pub const NETPLAY_ERROR_PLAYER_2_DISCONNECTED: u32 = 4;
pub const NETPLAY_ERROR_PLAYER_3_DISCONNECTED: u32 = 5;
pub const NETPLAY_ERROR_PLAYER_4_DISCONNECTED: u32 = 6;
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
let size: u32 = 8;
@@ -155,7 +147,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
.remove(&netplay.player_data[channel].count);
if std::time::Instant::now() > timeout {
ui::audio::play_netplay_audio(&device.ui, NETPLAY_ERROR_LOST_CONNECTION);
ui::video::onscreen_message(&device.ui, "Lost connection to netplay server");
input = Some(InputEvent {
input: 0,
plugin: 0,
@@ -200,15 +192,13 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
}
if current_status != netplay.status {
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
ui::audio::play_netplay_audio(ui, NETPLAY_ERROR_DESYNC);
ui::video::onscreen_message(ui, "Netplay desync detected");
ui::video::onscreen_message(ui, "Netplay desync detected"); // queue this message twice to ensure they see it
}
for dis in 1..5 {
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
{
ui::audio::play_netplay_audio(
ui,
NETPLAY_ERROR_PLAYER_DISCONNECTED + dis,
);
ui::video::onscreen_message(ui, &format!("Player {dis} disconnected"));
}
}
netplay.status = current_status;
+19 -7
View File
@@ -87,9 +87,12 @@ pub fn create_savestate(device: &device::Device) {
compressed_file,
)
.unwrap();
println!(
"Savestate created at {}",
device.ui.storage.paths.savestate_file_path.display()
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
);
}
@@ -195,12 +198,21 @@ pub fn load_savestate(device: &mut device::Device) {
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
ui::video::load_state(device, rdp_state.as_ptr());
println!(
"Savestate loaded from {}",
device.ui.storage.paths.savestate_file_path.display()
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate loaded from slot {}",
device.ui.storage.save_state_slot
),
);
} else {
println!("Failed to load savestate");
ui::video::onscreen_message(
&device.ui,
&format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
);
}
}
}
+22 -18
View File
@@ -5,6 +5,7 @@ pub mod gui;
pub mod input;
pub mod netplay;
pub mod storage;
pub mod usb;
pub mod video;
pub mod vru;
@@ -16,9 +17,7 @@ pub struct Dirs {
pub struct Audio {
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub audio_device: u32,
pub event_audio: audio::EventAudio,
pub gain: f32,
}
@@ -32,6 +31,7 @@ pub struct Storage {
pub save_type: Vec<storage::SaveTypes>,
pub paths: storage::Paths,
pub saves: storage::Saves,
pub save_state_slot: u32,
}
pub struct Video {
@@ -39,6 +39,11 @@ pub struct Video {
pub fullscreen: bool,
}
pub struct Usb {
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
}
pub struct Ui {
pub dirs: Dirs,
pub config: config::Config,
@@ -49,12 +54,14 @@ pub struct Ui {
pub input: Input,
pub storage: Storage,
pub video: Video,
pub usb: Usb,
}
impl Drop for Ui {
fn drop(&mut self) {
if self.with_sdl {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
@@ -73,6 +80,14 @@ pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
}
}
pub fn ttf_init() {
unsafe {
if !sdl3_ttf_sys::ttf::TTF_Init() {
panic!("Could not initialize TTF");
}
}
}
pub fn get_dirs() -> Dirs {
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
@@ -125,6 +140,7 @@ impl Ui {
joysticks,
},
storage: Storage {
save_state_slot: 0,
save_type: vec![],
paths: storage::Paths {
eep_file_path: std::path::PathBuf::new(),
@@ -167,24 +183,8 @@ impl Ui {
game_id: String::new(),
game_hash: String::new(),
audio: Audio {
event_audio: audio::EventAudio {
mempak: include_bytes!("../data/mempak.wav").to_vec(),
rumblepak: include_bytes!("../data/rumblepak.wav").to_vec(),
transferpak: include_bytes!("../data/transferpak.wav").to_vec(),
netplay_desync: include_bytes!("../data/netplay_desync.wav").to_vec(),
netplay_lost_connection: include_bytes!("../data/netplay_lost_connection.wav")
.to_vec(),
netplay_disconnected: [
include_bytes!("../data/netplay_p1_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p2_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p3_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p4_disconnected.wav").to_vec(),
],
cheats_enabled: include_bytes!("../data/cheats_enabled.wav").to_vec(),
},
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
event_audio_stream: std::ptr::null_mut(),
audio_device: 0,
gain: 1.0,
},
@@ -192,6 +192,10 @@ impl Ui {
window: std::ptr::null_mut(),
fullscreen: false,
},
usb: Usb {
usb_tx: None,
cart_rx: None,
},
dirs,
with_sdl,
}
-105
View File
@@ -1,17 +1,6 @@
use crate::device;
use crate::netplay;
use crate::ui;
pub struct EventAudio {
pub mempak: Vec<u8>,
pub rumblepak: Vec<u8>,
pub transferpak: Vec<u8>,
pub netplay_desync: Vec<u8>,
pub netplay_lost_connection: Vec<u8>,
pub netplay_disconnected: [Vec<u8>; 4],
pub cheats_enabled: Vec<u8>,
}
pub fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
@@ -40,24 +29,6 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
panic!("Could not get audio device format");
}
let wav_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 24000,
channels: 1,
};
ui.audio.event_audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &ui.audio.audio_device_spec)
};
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(
ui.audio.audio_device,
ui.audio.event_audio_stream,
)
} {
panic!("Could not bind audio stream");
}
init_game_audio(ui, frequency);
}
@@ -85,10 +56,6 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
unsafe {
if !ui.audio.event_audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.event_audio_stream);
ui.audio.event_audio_stream = std::ptr::null_mut();
}
sdl3_sys::audio::SDL_CloseAudioDevice(ui.audio.audio_device);
ui.audio.audio_device = 0;
}
@@ -110,7 +77,6 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
ui.audio.gain = 0.0;
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
}
}
@@ -121,77 +87,6 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
ui.audio.gain = 2.0;
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
}
}
pub fn play_cheat_event(ui: &ui::Ui) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = &ui.audio.event_audio.cheats_enabled;
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = match error {
netplay::NETPLAY_ERROR_DESYNC => &ui.audio.event_audio.netplay_desync,
netplay::NETPLAY_ERROR_LOST_CONNECTION => &ui.audio.event_audio.netplay_lost_connection,
netplay::NETPLAY_ERROR_PLAYER_1_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[0]
}
netplay::NETPLAY_ERROR_PLAYER_2_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[1]
}
netplay::NETPLAY_ERROR_PLAYER_3_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[2]
}
netplay::NETPLAY_ERROR_PLAYER_4_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[3]
}
_ => panic!("Invalid netplay error"),
};
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_pak_switch(ui: &ui::Ui, pak: device::controller::PakType) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = match pak {
device::controller::PakType::RumblePak => &ui.audio.event_audio.rumblepak,
device::controller::PakType::MemPak => &ui.audio.event_audio.mempak,
device::controller::PakType::TransferPak => &ui.audio.event_audio.transferpak,
_ => panic!("Invalid pak type"),
};
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
+3
View File
@@ -29,6 +29,7 @@ pub struct InputProfile {
pub joystick_hat: [InputJoystickHat; ui::input::PROFILE_SIZE],
pub joystick_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
pub dinput: bool,
pub deadzone: i32,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -54,6 +55,7 @@ pub struct Video {
pub struct Emulation {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub usb: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -150,6 +152,7 @@ impl Config {
emulation: Emulation {
overclock: false,
disable_expansion_pak: false,
usb: false,
},
rom_dir: std::path::PathBuf::new(),
}
+44 -8
View File
@@ -5,7 +5,9 @@ use slint::Model;
slint::include_modules!();
pub const N64_EXTENSIONS: [&str; 5] = ["n64", "v64", "z64", "7z", "zip"];
pub const N64_EXTENSIONS: [&str; 12] = [
"n64", "v64", "z64", "7z", "zip", "bin", "N64", "V64", "Z64", "7Z", "ZIP", "BIN",
];
#[derive(serde::Deserialize)]
struct GithubData {
@@ -62,15 +64,23 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb: ui::Usb) {
let dirs = ui::get_dirs();
let weak = app.as_weak();
let controller_paths = controller_paths.to_owned();
app.on_open_rom_button_clicked(move || {
let controller_paths = controller_paths.clone();
let mut usb_tx = None;
if let Some(usb_tx_inner) = usb.usb_tx.as_ref() {
usb_tx = Some(usb_tx_inner.clone());
}
let mut cart_rx = None;
if let Some(cart_rx_inner) = usb.cart_rx.as_ref() {
cart_rx = Some(cart_rx_inner.resubscribe());
}
weak.upgrade_in_event_loop(move |handle| {
save_settings(&handle, &controller_paths);
open_rom(&handle)
open_rom(&handle, ui::Usb { usb_tx, cart_rx })
})
.unwrap();
});
@@ -96,6 +106,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_apply_crt_shader(config.video.crt);
app.set_overclock_n64_cpu(config.emulation.overclock);
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
app.set_emulate_usb(config.emulation.usb);
let combobox_value = match config.video.upscale {
1 => 0,
2 => 1,
@@ -200,6 +211,7 @@ fn controller_window(
let weak_app = app.as_weak();
app.on_input_profile_button_clicked(move || {
let dialog = InputProfileDialog::new().unwrap();
dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
let weak_dialog = dialog.as_weak();
let weak_app = weak_app.clone();
dialog.on_profile_creation_button_clicked(move || {
@@ -209,10 +221,16 @@ fn controller_window(
handle.hide().unwrap();
let profile_name = handle.get_profile_name().into();
let dinput = handle.get_dinput();
let deadzone = handle.get_deadzone();
tokio::spawn(async move {
let mut game_ui = ui::Ui::new();
ui::input::configure_input_profile(&mut game_ui, profile_name, dinput);
ui::input::configure_input_profile(
&mut game_ui,
profile_name,
dinput,
deadzone,
);
update_input_profiles(&weak_app, &game_ui.config);
});
})
@@ -230,6 +248,7 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
config.video.crt = app.get_apply_crt_shader();
config.emulation.overclock = app.get_overclock_n64_cpu();
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
config.emulation.usb = app.get_emulate_usb();
let upscale_values = [1, 2, 4];
config.video.upscale = upscale_values[app.get_resolution() as usize];
@@ -280,6 +299,11 @@ fn about_window(app: &AppWindow) {
pub fn app_window() {
let app = AppWindow::new().unwrap();
about_window(&app);
let mut usb = ui::Usb {
usb_tx: None,
cart_rx: None,
};
let mut shutdown_tx = None;
let mut controller_paths;
{
let game_ui = ui::Ui::new();
@@ -289,11 +313,18 @@ pub fn app_window() {
controller_paths.insert(0, None);
settings_window(&app, &game_ui.config);
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
if game_ui.config.emulation.usb {
(shutdown_tx, usb) = ui::usb::init(app.as_weak());
}
}
local_game_window(&app, &controller_paths);
local_game_window(&app, &controller_paths, usb);
ui::netplay::netplay_window(&app, &controller_paths);
ui::cheats::cheats_window(&app);
app.run().unwrap();
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx);
}
save_settings(&app, &controller_paths);
}
@@ -347,6 +378,7 @@ pub fn run_rom(
mut game_settings: GameSettings,
vru_channel: VruChannel,
netplay: Option<NetplayDevice>,
usb: ui::Usb,
weak: slint::Weak<AppWindow>,
) {
std::thread::Builder::new()
@@ -369,6 +401,9 @@ pub fn run_rom(
}
}
device.ui.usb.usb_tx = usb.usb_tx;
device.ui.usb.cart_rx = usb.cart_rx;
device.vru_window.window_notifier = vru_channel.vru_window_notifier;
device.vru_window.word_receiver = vru_channel.vru_word_receiver;
@@ -409,7 +444,7 @@ pub fn run_rom(
.unwrap();
}
fn open_rom(app: &AppWindow) {
fn open_rom(app: &AppWindow, usb: ui::Usb) {
let rom_dir = app.get_rom_dir();
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
@@ -430,13 +465,13 @@ fn open_rom(app: &AppWindow) {
select_gb_rom[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", i + 1))
.add_filter("GB ROM files", &["gb", "gbc"])
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
.pick_file(),
);
select_gb_ram[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", i + 1))
.add_filter("GB RAM files", &["sav", "ram"])
.add_filter("GB RAM files", &["sav", "ram", "SAV", "RAM"])
.pick_file(),
);
}
@@ -501,6 +536,7 @@ fn open_rom(app: &AppWindow) {
}
},
None,
usb,
weak,
);
}
+1
View File
@@ -43,6 +43,7 @@ export component AppWindow inherits Window {
in-out property overclock_n64_cpu <=> SettingsData.overclock_n64_cpu;
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
in-out property resolution <=> SettingsData.resolution;
in-out property emulate_usb <=> SettingsData.emulate_usb;
in-out property rom_dir <=> SettingsData.rom_dir;
in-out property emulate_vru <=> ControllerData.emulate_vru;
in-out property controller_enabled <=> ControllerData.controller_enabled;
+4 -1
View File
@@ -89,10 +89,12 @@ export component ControllerConfig inherits Page {
if !ControllerData.blank_profiles: GroupBox {
title: @tr("Profile");
horizontal-stretch: 0.25;
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.input_profiles;
min-width: self.preferred-width > 150px ? self.preferred-width : 150px;
current-index: ControllerData.selected_profile_binding[player];
selected => {
ControllerData.selected_profile_binding[player] = self.current-index;
@@ -103,11 +105,12 @@ export component ControllerConfig inherits Page {
GroupBox {
title: @tr("Controller");
horizontal-stretch: 0.75;
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.controller_names;
width: self.preferred-width;
min-width: self.preferred-width > 300px ? self.preferred-width : 300px;
current-index: ControllerData.selected_controller[player];
selected => {
ControllerData.selected_controller[player] = self.current-index;
+25 -1
View File
@@ -1,8 +1,9 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox, Slider } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
in-out property <string> profile_name;
in-out property <int> deadzone;
}
export component InputProfileDialog inherits Window {
@@ -10,6 +11,7 @@ export component InputProfileDialog inherits Window {
callback profile_creation_button_clicked;
in-out property dinput <=> InputProfileData.dinput;
in-out property profile_name <=> InputProfileData.profile_name;
in-out property deadzone <=> InputProfileData.deadzone;
VerticalBox {
HorizontalBox {
LineEdit {
@@ -21,6 +23,28 @@ export component InputProfileDialog inherits Window {
}
}
HorizontalBox {
alignment: center;
Text {
text: @tr("Deadzone Percentage");
vertical-alignment: center;
}
Slider {
min-width: 160px;
maximum: 99;
value: InputProfileData.deadzone;
changed(value) => {
InputProfileData.deadzone = value;
}
}
Text {
text: InputProfileData.deadzone + "%";
vertical-alignment: center;
}
}
HorizontalBox {
alignment: center;
CheckBox {
+12
View File
@@ -12,6 +12,7 @@ export global SettingsData {
in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak;
in-out property <int> resolution;
in-out property <bool> emulate_usb;
in-out property <string> rom_dir;
}
@@ -105,5 +106,16 @@ export component Settings inherits Page {
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Emulate UNFLoader (reload emulator after changing)");
checked: SettingsData.emulate_usb;
toggled => {
SettingsData.emulate_usb = self.checked;
}
}
}
}
}
+51 -11
View File
@@ -28,6 +28,8 @@ const Y_AXIS_SHIFT: usize = 24;
const MAX_AXIS_VALUE: f64 = 85.0;
pub const DEADZONE_DEFAULT: i32 = 5;
pub struct Controllers {
pub rumble: bool,
pub game_controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
@@ -40,7 +42,7 @@ pub struct InputData {
}
fn bound_axis(x: &mut f64, y: &mut f64) {
let radius = 95.0; // this is roughly the maxium diagonal distance of the controller
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.0); // this is roughly the maxium diagonal distance of the controller
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
@@ -53,6 +55,24 @@ fn bound_axis(x: &mut f64, y: &mut f64) {
}
}
fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
let axis_deadzone = MAX_AXIS_VALUE * (deadzone as f64 / 100.0);
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
if distance <= axis_deadzone {
*x = 0.0;
*y = 0.0;
return;
}
let new_distance =
(distance - axis_deadzone) * MAX_AXIS_VALUE / (MAX_AXIS_VALUE - axis_deadzone);
*x = *x / distance * new_distance;
*y = *y / distance * new_distance;
}
fn set_axis_from_joystick(
profile: &ui::config::InputProfile,
joystick: *mut sdl3_sys::joystick::SDL_Joystick,
@@ -225,8 +245,9 @@ fn set_buttons_from_joystick(
let profile_joystick_hat = profile.joystick_hat[i];
if profile_joystick_hat.enabled
&& unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
== profile_joystick_hat.direction
&& (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
& profile_joystick_hat.direction)
!= 0
{
*keys |= 1 << i;
}
@@ -326,8 +347,9 @@ fn change_paks(
pressed =
unsafe { sdl3_sys::joystick::SDL_GetJoystickButton(joystick, joystick_button.id) };
} else if joystick_hat.enabled && !joystick.is_null() {
pressed = unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
== joystick_hat.direction;
pressed = (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
& joystick_hat.direction)
!= 0;
} else if key.enabled {
pressed = unsafe { *keyboard_state.offset(key.id as isize) };
}
@@ -407,9 +429,11 @@ pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
}
if !controller.is_null() {
(x, y) = set_axis_from_controller(profile, controller)
(x, y) = set_axis_from_controller(profile, controller);
apply_deadzone(&mut x, &mut y, profile.deadzone);
} else if !joystick.is_null() {
(x, y) = set_axis_from_joystick(profile, joystick)
(x, y) = set_axis_from_joystick(profile, joystick);
apply_deadzone(&mut x, &mut y, profile.deadzone);
}
bound_axis(&mut x, &mut y);
@@ -464,8 +488,9 @@ fn close_controllers(
}
}
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, deadzone: i32) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
if profile == "default" {
println!("Profile name cannot be default");
@@ -567,8 +592,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
axis: 0,
};
let font =
ab_glyph::FontRef::try_from_slice(include_bytes!("../../data/Roboto-Regular.ttf")).unwrap();
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
let font = unsafe {
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
sdl3_ttf_sys::ttf::TTF_OpenFontIO(
sdl3_sys::everything::SDL_IOFromConstMem(
font_bytes.as_ptr() as *const std::ffi::c_void,
font_bytes.len(),
),
true,
35.0,
)
};
for (key, value) in key_labels.iter() {
let mut event: sdl3_sys::events::SDL_Event = Default::default();
@@ -577,7 +612,8 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
&font,
text_engine,
font,
);
let mut key_set = false;
@@ -671,6 +707,8 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
close_controllers(open_joysticks, open_controllers);
unsafe {
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
sdl3_sys::render::SDL_DestroyRenderer(renderer);
sdl3_sys::video::SDL_DestroyWindow(window);
}
@@ -683,6 +721,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
joystick_hat: new_joystick_hat,
joystick_axis: new_joystick_axis,
dinput,
deadzone,
};
ui.config.input.input_profiles.insert(profile, new_profile);
}
@@ -883,6 +922,7 @@ pub fn get_default_profile() -> ui::config::InputProfile {
axis: 0,
}; PROFILE_SIZE],
dinput: false,
deadzone: DEADZONE_DEFAULT,
}
}
+49 -43
View File
@@ -187,7 +187,7 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
if let Some(file) = select_rom.await {
if let Some(rom_contents) = device::get_rom_contents(file.path()) {
let hash = device::cart::rom::calculate_hash(&rom_contents);
let game_name = ui::storage::get_game_name(&rom_contents);
let mut game_name = ui::storage::get_game_name(&rom_contents);
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
let mut parsed_cheats = "".to_string();
@@ -196,6 +196,14 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
{
parsed_cheats = serde_json::to_string(game_cheats).unwrap();
}
if game_name.is_empty() {
game_name = file
.path()
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
}
weak.upgrade_in_event_loop(move |handle| {
handle.set_game_name(game_name.into());
@@ -515,50 +523,44 @@ fn update_sessions(weak: slint::Weak<NetplayJoin>) {
.await
.unwrap();
if let Some(Ok(response)) = read.next().await {
if let Ok(message) =
if let Some(Ok(response)) = read.next().await
&& let Ok(message) =
serde_json::from_slice::<NetplayMessage>(&response.into_data())
{
if message.message_type == "reply_get_rooms"
&& message.accept.unwrap() == 0
&& let Some(rooms) = message.rooms
{
for room in rooms {
let mut session = vec![];
room_urls.push(server_url.into());
&& message.message_type == "reply_get_rooms"
&& message.accept.unwrap() == 0
&& let Some(rooms) = message.rooms
{
for room in rooms {
let mut session = vec![];
room_urls.push(server_url.into());
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(server_name),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(room.room_name.unwrap()),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(room.game_name.unwrap()),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(if room.protected.unwrap() {
"True"
} else {
"False"
}),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(
if room
.features
.unwrap_or_default()
.contains_key("cheats")
{
"True"
} else {
"False"
},
),
));
sessions.push(session);
}
}
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(server_name),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(room.room_name.unwrap()),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(room.game_name.unwrap()),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(if room.protected.unwrap() {
"True"
} else {
"False"
}),
));
session.push(slint::StandardListViewItem::from(
slint::SharedString::from(
if room.features.unwrap_or_default().contains_key("cheats")
{
"True"
} else {
"False"
},
),
));
sessions.push(session);
}
}
}
@@ -1127,6 +1129,10 @@ fn setup_wait_window(
peer_addr: socket_addr,
player_number: player_number as u8,
}),
ui::Usb {
usb_tx: None,
cart_rx: None,
},
weak_app,
);
})
+7 -4
View File
@@ -199,10 +199,13 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
.paths
.savestate_file_path
.clone_from(&states_path);
ui.storage
.paths
.savestate_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".state");
ui.storage.paths.savestate_file_path.push(
prefix.to_owned()
+ "-"
+ &ui.game_hash
+ ".state"
+ ui.storage.save_state_slot.to_string().as_str(),
);
}
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
+224
View File
@@ -0,0 +1,224 @@
use crate::ui;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const DATATYPE_TCPTEST: u32 = 0x07;
const DATATYPE_ROMUPLOAD: u32 = 0x08;
#[derive(Clone, Debug)]
pub struct UsbData {
pub data: Vec<u8>,
pub data_type: u32,
pub data_size: u32,
}
fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data: Vec<u8>) {
if let Ok(data) = String::from_utf8(data)
&& data == "N64"
{
ui::usb::send_to_usb(
usb_tx,
ui::usb::UsbData {
data: [b'N', b'6', b'4'].to_vec(),
data_type: DATATYPE_TCPTEST,
data_size: 3,
},
);
}
}
fn upload_rom(
weak: slint::Weak<ui::gui::AppWindow>,
rom: Vec<u8>,
usb_tx: &tokio::sync::broadcast::Sender<UsbData>,
cart_rx: &tokio::sync::broadcast::Receiver<UsbData>,
) {
let weak_clone = weak.clone();
let usb_tx = usb_tx.clone();
let cart_rx = cart_rx.resubscribe();
weak.upgrade_in_event_loop(move |handle| {
let dir = std::env::temp_dir();
let rom_path = dir.join("rom_upload.bin");
std::fs::write(rom_path.clone(), rom).unwrap();
ui::gui::run_rom(
ui::gui::GbPaths {
rom: [None, None, None, None],
ram: [None, None, None, None],
},
rom_path,
ui::gui::GameSettings {
fullscreen: handle.get_fullscreen(),
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // will be filled in later
},
ui::gui::VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
},
None,
ui::Usb {
usb_tx: Some(usb_tx),
cart_rx: Some(cart_rx),
},
weak_clone,
);
})
.unwrap();
}
async fn handle_connection(
conn: tokio::net::TcpStream,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_rx: tokio::sync::broadcast::Receiver<UsbData>,
weak: slint::Weak<ui::gui::AppWindow>,
) {
let (mut incoming, mut outgoing) = conn.into_split();
let mut shutdown_rx_clone = shutdown_rx.clone();
tokio::spawn(async move {
loop {
tokio::select! {
data = usb_rx.recv() => {
match data {
Ok(data) => {
let mut output: Vec<u8> = vec![];
output.extend_from_slice(&data.data_type.to_be_bytes());
output.extend_from_slice(&data.data_size.to_be_bytes());
output.extend_from_slice(&data.data);
if outgoing.write_all(&output).await.is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
panic!("usb_rx lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
}
}
_ = shutdown_rx_clone.changed() => {
break;
}
}
}
});
let mut incoming_buffer = vec![0u8; 4096];
let mut data_type: Option<u32> = None;
let mut data_size: Option<u32> = None;
let mut usb_buffer: Vec<u8> = vec![];
loop {
tokio::select! {
result = incoming.read(&mut incoming_buffer) => {
match result {
Ok(0) => {
break;
}
Ok(n) => {
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
if data_type.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if data_type.is_some() && data_size.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if let Some(d_type) = data_type && let Some(d_size) = data_size {
let length = d_size as usize;
if usb_buffer.len() >= length {
let usb_data = UsbData {
data: usb_buffer[0..length].to_vec(),
data_type: d_type,
data_size: d_size,
};
usb_buffer.drain(0..length);
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
upload_rom(weak.clone(), usb_data.data,&usb_tx,&cart_rx);
} else {
cart_tx.send(usb_data).unwrap();
}
data_type = None;
data_size = None;
}
}
}
Err(_e) => {
break;
}
}
}
_ = shutdown_rx.changed() => {
break;
}
}
}
}
pub fn init(
weak: slint::Weak<ui::gui::AppWindow>,
) -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let (usb_tx, usb_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
let (cart_tx, cart_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
let usb_tx_clone = usb_tx.clone();
let cart_rx_clone = cart_rx.resubscribe();
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind("localhost:64000")
.await
.unwrap();
loop {
tokio::select! {
res = listener.accept() => {
if let Ok((c,_)) = res {
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone(),cart_rx.resubscribe(),weak.clone()).await;
} else {
break;
}
}
_ = shutdown_rx.changed() => {
break;
}
}
}
});
(
Some(shutdown_tx),
ui::Usb {
usb_tx: Some(usb_tx_clone),
cart_rx: Some(cart_rx_clone),
},
)
}
pub fn close(shutdown_tx: &tokio::sync::watch::Sender<()>) {
let _ = shutdown_tx.send(());
}
pub fn send_to_usb(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, buffer: UsbData) {
usb_tx.send(buffer).unwrap();
}
+59 -58
View File
@@ -1,10 +1,10 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
use crate::{device, ui};
use ab_glyph::{Font, ScaleFont};
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
let title = std::ffi::CString::new("gopher64").unwrap();
@@ -74,7 +74,15 @@ pub fn init(device: &mut device::Device) {
crt: device.ui.config.video.crt,
};
unsafe { rdp_init(device.ui.video.window as *mut std::ffi::c_void, gfx_info) }
unsafe {
let font_bytes = include_bytes!("../../data/Roboto-Regular.ttf");
rdp_init(
device.ui.video.window as *mut std::ffi::c_void,
gfx_info,
font_bytes.as_ptr() as *const std::ffi::c_void,
font_bytes.len(),
)
}
}
pub fn close(ui: &ui::Ui) {
@@ -85,10 +93,13 @@ pub fn close(ui: &ui::Ui) {
}
pub fn update_screen() {
// when the window is closed, running is set to false
unsafe { rdp_update_screen() }
}
pub fn render_frame() {
unsafe { rdp_render_frame() }
}
pub fn state_size() -> usize {
unsafe { rdp_state_size() }
}
@@ -122,9 +133,21 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
}
}
pub fn check_callback(device: &mut device::Device) -> bool {
pub fn pause_loop() {
let mut paused = true;
let mut frame_advance = false;
while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
let callback = unsafe { rdp_check_callback() };
paused = callback.paused;
frame_advance = callback.frame_advance;
}
}
pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
let mut speed_limiter_toggled = false;
let mut callback = unsafe { rdp_check_callback() };
let callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
if callback.save_state {
@@ -136,19 +159,27 @@ pub fn check_callback(device: &mut device::Device) -> bool {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
}
while callback.paused {
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
callback = unsafe { rdp_check_callback() };
}
}
if device.ui.storage.save_state_slot != callback.save_state_slot {
ui::video::onscreen_message(
&device.ui,
&format!("Switching savestate slot to {}", callback.save_state_slot,),
);
device.ui.storage.save_state_slot = callback.save_state_slot;
device
.ui
.storage
.paths
.savestate_file_path
.set_extension("state".to_owned() + callback.save_state_slot.to_string().as_str());
}
if callback.lower_volume {
ui::audio::lower_audio_volume(&mut device.ui);
} else if callback.raise_volume {
ui::audio::raise_audio_volume(&mut device.ui);
}
speed_limiter_toggled
(speed_limiter_toggled, callback.paused)
}
pub fn set_register(reg: u32, value: u32) {
@@ -161,60 +192,30 @@ pub fn process_rdp_list() -> u64 {
unsafe { rdp_process_commands() }
}
pub fn onscreen_message(_ui: &ui::Ui, message: &str) {
unsafe { rdp_onscreen_message(std::ffi::CString::new(message).unwrap().as_ptr()) };
}
pub fn draw_text(
text: &str,
renderer: *mut sdl3_sys::render::SDL_Renderer,
font: &ab_glyph::FontRef,
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
) {
// Clear the canvas
unsafe {
sdl3_sys::render::SDL_SetRenderDrawColor(
renderer,
let (mut w, mut h) = (0, 0);
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(
text_engine,
font,
std::ffi::CString::new(text).unwrap().as_ptr(),
0,
0,
0,
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
);
sdl3_sys::render::SDL_RenderClear(renderer);
};
let text_size = 40.0;
let (mut w, mut h) = (0, 0);
unsafe { sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h) };
let x_start = 20.0;
let y_start = (h / 2) as f32;
let mut x_offset = 0.0;
for c in text.chars() {
let q_glyph_id = font.glyph_id(c);
let q_glyph = q_glyph_id.with_scale(text_size);
if let Some(q) = font.outline_glyph(q_glyph) {
q.draw(|x, y, c| {
if c > 0.5 {
unsafe {
sdl3_sys::render::SDL_SetRenderDrawColor(
renderer,
255,
255,
255,
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
);
sdl3_sys::render::SDL_RenderPoint(
renderer,
x_start + x_offset + x as f32 - q.px_bounds().width()
+ q.px_bounds().max.x,
y_start + y as f32 - q.px_bounds().height() + q.px_bounds().max.y,
);
};
}
});
}
x_offset += font.as_scaled(text_size).h_advance(q_glyph_id);
}
// Present the canvas
if !unsafe { sdl3_sys::render::SDL_RenderPresent(renderer) } {
panic!("Could not present renderer");
sdl3_sys::everything::SDL_RenderClear(renderer);
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, 20.0, h as f32 / 2.0);
sdl3_sys::render::SDL_RenderPresent(renderer);
sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);
}
}