mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 01:25:20 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26022fff72 | |||
| 658dede9e9 | |||
| 6ad1a31e34 | |||
| 4476d476de | |||
| 8037fb426e | |||
| f0381a2cd1 | |||
| c8e70e6125 | |||
| e9664b1087 | |||
| 3f72700da0 | |||
| 9fcd2835eb | |||
| c6767c94a6 | |||
| f1968e3a58 | |||
| 23fdfff1ac | |||
| fc512d9517 | |||
| 3c317460fd | |||
| b1fcaa5cf4 | |||
| 98c9cf142d | |||
| 1eb1503981 | |||
| 5aa9a65e7b | |||
| 71f1d98132 | |||
| 0037c0f344 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
+275
-254
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
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"
|
||||
|
||||
@@ -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();
|
||||
@@ -108,6 +112,7 @@ fn main() {
|
||||
.allowlist_function("rdp_set_vi_register")
|
||||
.allowlist_function("rdp_update_screen")
|
||||
.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 +128,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.
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.
@@ -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();
|
||||
@@ -458,6 +539,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 +558,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;
|
||||
|
||||
@@ -34,14 +34,17 @@ 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();
|
||||
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
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
channel = "1.89.0"
|
||||
|
||||
+1
-1
@@ -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,4 +1,4 @@
|
||||
#include "src/compat/sse2neon/v1.8.0/sse2neon.h"
|
||||
#include "src/compat/sse2neon/sse2neon.h"
|
||||
|
||||
// Static wrappers
|
||||
|
||||
|
||||
Submodule
+1
Submodule src/compat/sse2neon added at 3cf69760cc
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
|
||||
+4
-14
@@ -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
@@ -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
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,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 +151,7 @@ impl Config {
|
||||
emulation: Emulation {
|
||||
overclock: false,
|
||||
disable_expansion_pak: false,
|
||||
usb: false,
|
||||
},
|
||||
rom_dir: std::path::PathBuf::new(),
|
||||
}
|
||||
|
||||
+36
-7
@@ -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,
|
||||
@@ -230,6 +241,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 +292,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 +306,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 +371,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 +394,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 +437,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 +458,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 +529,7 @@ fn open_rom(app: &AppWindow) {
|
||||
}
|
||||
},
|
||||
None,
|
||||
usb,
|
||||
weak,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -225,8 +225,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 +327,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) };
|
||||
}
|
||||
@@ -466,6 +468,7 @@ fn close_controllers(
|
||||
|
||||
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
|
||||
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
|
||||
ui::ttf_init();
|
||||
|
||||
if profile == "default" {
|
||||
println!("Profile name cannot be default");
|
||||
@@ -567,8 +570,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 +590,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 +685,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);
|
||||
}
|
||||
|
||||
+49
-43
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
+41
-50
@@ -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) {
|
||||
@@ -136,13 +144,26 @@ pub fn check_callback(device: &mut device::Device) -> bool {
|
||||
speed_limiter_toggled = true;
|
||||
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
|
||||
}
|
||||
while callback.paused {
|
||||
while callback.paused && !callback.frame_advance {
|
||||
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 {
|
||||
@@ -161,60 +182,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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user