mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 09:35:21 +02:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6d608b674 | |||
| f254bec968 | |||
| bd4aff2b2c | |||
| 0bf70a77ea | |||
| e23e7f33c1 | |||
| 9d0516545e | |||
| e3023a902f | |||
| 431c7c2a8d | |||
| cf99539f85 | |||
| c2493b829e | |||
| 2f25de55d7 | |||
| 116ffe9845 | |||
| f3271cba63 | |||
| 9037680cda | |||
| fc41e26a6d | |||
| d1f28bb7a8 | |||
| 9db831fdef | |||
| 004b603e96 | |||
| 8661e2c9b1 | |||
| c712f2a288 | |||
| 366162e6fe | |||
| 2b93fb8aa1 | |||
| a191701610 | |||
| 0605967c28 | |||
| d6e6a16814 | |||
| e374825d5e | |||
| f6d6746207 | |||
| 100238f198 | |||
| daee53a626 | |||
| 018d8ca3e3 | |||
| 24d26d864e | |||
| 7b8c549e8e | |||
| f6c7e3d3c0 | |||
| 374b01dccf | |||
| 569b26a067 | |||
| 380d196ae7 | |||
| 4fa1a8ad39 | |||
| 8acf3b9de8 | |||
| b3297f82b2 | |||
| 2127705e7b | |||
| cccb079d63 | |||
| c52b680753 | |||
| 8799650245 | |||
| 90be2130c1 | |||
| 96af33c434 | |||
| 0a42152bcb | |||
| db335e24d2 | |||
| 4857b86897 | |||
| 26022fff72 | |||
| 658dede9e9 | |||
| 6ad1a31e34 | |||
| 4476d476de | |||
| 8037fb426e | |||
| f0381a2cd1 | |||
| c8e70e6125 | |||
| e9664b1087 | |||
| 3f72700da0 | |||
| 9fcd2835eb | |||
| c6767c94a6 | |||
| f1968e3a58 | |||
| 23fdfff1ac | |||
| fc512d9517 | |||
| 3c317460fd | |||
| b1fcaa5cf4 | |||
| 98c9cf142d | |||
| 1eb1503981 | |||
| 5aa9a65e7b | |||
| 71f1d98132 | |||
| 0037c0f344 | |||
| 752e804101 | |||
| 73354246f3 | |||
| a2d23c2f41 | |||
| 6e652c7dbd | |||
| 20749cf036 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
[target.'cfg(target_os = "linux")']
|
||||
[target.'cfg(all(target_arch = "aarch64", target_os = "linux"))']
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
|
||||
@@ -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.1
|
||||
uses: signpath/github-action-submit-signing-request@v1.3
|
||||
with:
|
||||
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
|
||||
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
|
||||
@@ -118,7 +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: true
|
||||
wait-for-completion: false
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-15
|
||||
@@ -131,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
+1197
-806
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -1,32 +1,32 @@
|
||||
[package]
|
||||
name = "gopher64"
|
||||
version = "1.1.1"
|
||||
version = "1.1.8"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.90"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dirs = "6.0"
|
||||
zip = "4.3"
|
||||
zip = "5.1"
|
||||
governor = "0.10"
|
||||
sevenz-rust2 = { version = "0.17", default-features = false }
|
||||
sevenz-rust2 = { version = "0.19", default-features = false }
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-big-array = "0.5"
|
||||
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
|
||||
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg-wgpu"] }
|
||||
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"
|
||||
encoding_rs = "0.8"
|
||||
mimalloc = "0.1"
|
||||
regex = "1.11"
|
||||
tokio-tungstenite = "0.27"
|
||||
tokio-tungstenite = "0.28"
|
||||
futures = "0.3"
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
rand_chacha = "0.9"
|
||||
@@ -45,3 +45,7 @@ cc = { version = "1.2", features = ["parallel"] }
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
@@ -19,7 +19,7 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
|
||||
|
||||
## netplay
|
||||
|
||||
Gopher64 supports netplay (online play with others). It has a few public netplay servers. If you are interested in running a public netplay server, please let me know (open an issue or discussion or contact me on Discord). You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
|
||||
|
||||
## portable mode
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ fn main() {
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/timeline_trace_file.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/environment.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/thread_name.cpp")
|
||||
.file("parallel-rdp/parallel-rdp-standalone/util/slab_allocator.cpp")
|
||||
.file("parallel-rdp/interface.cpp")
|
||||
.file("parallel-rdp/wsi_platform.cpp")
|
||||
.include("parallel-rdp/parallel-rdp-standalone/parallel-rdp")
|
||||
@@ -64,11 +65,14 @@ 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();
|
||||
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
|
||||
let profile = std::env::var("PROFILE").unwrap();
|
||||
let opt_flag = if arch == "x86_64" {
|
||||
"-march=x86-64-v3"
|
||||
} else if arch == "aarch64" {
|
||||
@@ -91,11 +95,10 @@ fn main() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if profile == "release" {
|
||||
volk_build.flag("-flto=thin");
|
||||
rdp_build.flag("-flto=thin");
|
||||
simd_build.flag("-flto=thin");
|
||||
}
|
||||
volk_build.flag("-flto=thin");
|
||||
rdp_build.flag("-flto=thin");
|
||||
simd_build.flag("-flto=thin");
|
||||
|
||||
volk_build.compile("volk");
|
||||
rdp_build.compile("parallel-rdp");
|
||||
|
||||
@@ -107,9 +110,12 @@ 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_check_framebuffers")
|
||||
.allowlist_function("rdp_state_size")
|
||||
.allowlist_function("rdp_save_state")
|
||||
.allowlist_function("rdp_load_state")
|
||||
@@ -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")
|
||||
@@ -157,6 +163,7 @@ fn main() {
|
||||
.allowlist_function("_mm_cmplt_epi16")
|
||||
.allowlist_function("_mm_subs_epu16")
|
||||
.allowlist_function("_mm_set1_epi32")
|
||||
.allowlist_function("_mm_set1_epi16")
|
||||
.blocklist_type("__m128i")
|
||||
.blocklist_type("int64x2_t")
|
||||
.wrap_static_fns(true)
|
||||
|
||||
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.
+245
-16
@@ -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;
|
||||
|
||||
@@ -52,14 +53,27 @@ enum vi_registers
|
||||
VI_REGS_COUNT
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t depthbuffer_address;
|
||||
uint32_t framebuffer_address;
|
||||
uint32_t framebuffer_y_offset;
|
||||
uint32_t texture_address;
|
||||
uint32_t framebuffer_pixel_size;
|
||||
uint32_t framebuffer_width;
|
||||
uint32_t texture_pixel_size;
|
||||
uint32_t texture_width;
|
||||
uint32_t framebuffer_height;
|
||||
bool depth_buffer_enabled;
|
||||
} FrameBufferInfo;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t cmd_data[0x00040000 >> 2];
|
||||
int cmd_cur;
|
||||
int cmd_ptr;
|
||||
uint32_t region;
|
||||
uint64_t vi_counter;
|
||||
uint64_t last_sync;
|
||||
FrameBufferInfo frame_buffer_info;
|
||||
} RDP_DEVICE;
|
||||
|
||||
static SDL_Window *window;
|
||||
@@ -74,6 +88,15 @@ static GFX_INFO gfx_info;
|
||||
static const uint32_t *fragment_spirv;
|
||||
static size_t fragment_size;
|
||||
|
||||
std::vector<bool> rdram_dirty;
|
||||
uint64_t sync_signal;
|
||||
|
||||
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 +223,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;
|
||||
}
|
||||
@@ -212,6 +256,9 @@ void rdp_new_processor(GFX_INFO _gfx_info)
|
||||
{
|
||||
gfx_info = _gfx_info;
|
||||
|
||||
sync_signal = 0;
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
|
||||
if (processor)
|
||||
{
|
||||
delete processor;
|
||||
@@ -228,11 +275,32 @@ void rdp_new_processor(GFX_INFO _gfx_info)
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_4X_BIT;
|
||||
}
|
||||
else if (gfx_info.upscale == 8)
|
||||
{
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
|
||||
flags |= RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT;
|
||||
}
|
||||
|
||||
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 +337,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 +511,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 +547,16 @@ 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);
|
||||
}
|
||||
|
||||
void rdp_update_screen()
|
||||
{
|
||||
wsi->end_frame();
|
||||
wsi->begin_frame();
|
||||
rdp_device.vi_counter += 1;
|
||||
}
|
||||
|
||||
CALL_BACK rdp_check_callback()
|
||||
@@ -458,9 +566,32 @@ CALL_BACK rdp_check_callback()
|
||||
callback.load_state = false;
|
||||
callback.lower_volume = false;
|
||||
callback.raise_volume = false;
|
||||
callback.frame_advance = false;
|
||||
return return_value;
|
||||
}
|
||||
|
||||
void rdp_check_framebuffers(uint32_t address, uint32_t length)
|
||||
{
|
||||
if (sync_signal)
|
||||
{
|
||||
address >>= 3;
|
||||
length = (length + 7) >> 3;
|
||||
|
||||
if (address >= rdram_dirty.size())
|
||||
return;
|
||||
|
||||
uint32_t end_addr = std::min(address + length, static_cast<uint32_t>(rdram_dirty.size()));
|
||||
|
||||
auto it = std::find(rdram_dirty.begin() + address, rdram_dirty.begin() + end_addr, true);
|
||||
if (it != rdram_dirty.begin() + end_addr)
|
||||
{
|
||||
processor->wait_for_timeline(sync_signal);
|
||||
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
|
||||
sync_signal = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t rdp_state_size()
|
||||
{
|
||||
return sizeof(RDP_DEVICE);
|
||||
@@ -468,6 +599,7 @@ size_t rdp_state_size()
|
||||
|
||||
void rdp_save_state(uint8_t *state)
|
||||
{
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
|
||||
}
|
||||
|
||||
@@ -476,6 +608,31 @@ 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);
|
||||
}
|
||||
|
||||
uint32_t pixel_size(uint32_t pixel_type, uint32_t area)
|
||||
{
|
||||
switch (pixel_type)
|
||||
{
|
||||
case 0:
|
||||
return area / 2;
|
||||
case 1:
|
||||
return area;
|
||||
case 2:
|
||||
return area * 2;
|
||||
case 3:
|
||||
return area * 4;
|
||||
default:
|
||||
printf("Invalid pixel size: %u\n", pixel_type);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t rdp_process_commands()
|
||||
{
|
||||
uint64_t interrupt_timer = 0;
|
||||
@@ -539,6 +696,71 @@ uint64_t rdp_process_commands()
|
||||
|
||||
switch (RDP::Op(command))
|
||||
{
|
||||
case RDP::Op::FillTriangle:
|
||||
case RDP::Op::FillZBufferTriangle:
|
||||
case RDP::Op::TextureTriangle:
|
||||
case RDP::Op::TextureZBufferTriangle:
|
||||
case RDP::Op::ShadeTriangle:
|
||||
case RDP::Op::ShadeZBufferTriangle:
|
||||
case RDP::Op::ShadeTextureTriangle:
|
||||
case RDP::Op::ShadeTextureZBufferTriangle:
|
||||
case RDP::Op::TextureRectangle:
|
||||
case RDP::Op::TextureRectangleFlip:
|
||||
case RDP::Op::FillRectangle:
|
||||
{
|
||||
uint32_t offset_address = (rdp_device.frame_buffer_info.framebuffer_address + pixel_size(rdp_device.frame_buffer_info.framebuffer_pixel_size, rdp_device.frame_buffer_info.framebuffer_y_offset * rdp_device.frame_buffer_info.framebuffer_width)) >> 3;
|
||||
if (offset_address < rdram_dirty.size() && !rdram_dirty[offset_address])
|
||||
{
|
||||
std::fill_n(rdram_dirty.begin() + offset_address, (pixel_size(rdp_device.frame_buffer_info.framebuffer_pixel_size, rdp_device.frame_buffer_info.framebuffer_width * rdp_device.frame_buffer_info.framebuffer_height) + 7) >> 3, true);
|
||||
}
|
||||
|
||||
if (rdp_device.frame_buffer_info.depth_buffer_enabled)
|
||||
{
|
||||
offset_address = (rdp_device.frame_buffer_info.depthbuffer_address + pixel_size(2, rdp_device.frame_buffer_info.framebuffer_y_offset * rdp_device.frame_buffer_info.framebuffer_width)) >> 3;
|
||||
if (offset_address < rdram_dirty.size() && !rdram_dirty[offset_address])
|
||||
{
|
||||
std::fill_n(rdram_dirty.begin() + offset_address, (pixel_size(2, rdp_device.frame_buffer_info.framebuffer_width * rdp_device.frame_buffer_info.framebuffer_height) + 7) >> 3, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RDP::Op::LoadTLut:
|
||||
case RDP::Op::LoadTile:
|
||||
{
|
||||
uint32_t upper_left_t = (w1 & 0xFFF) >> 2;
|
||||
uint32_t offset_address = (rdp_device.frame_buffer_info.texture_address + pixel_size(rdp_device.frame_buffer_info.texture_pixel_size, upper_left_t * rdp_device.frame_buffer_info.texture_width)) >> 3;
|
||||
if (offset_address < rdram_dirty.size() && !rdram_dirty[offset_address])
|
||||
{
|
||||
uint32_t lower_right_t = (w2 & 0xFFF) >> 2;
|
||||
std::fill_n(rdram_dirty.begin() + offset_address, (pixel_size(rdp_device.frame_buffer_info.texture_pixel_size, (lower_right_t - upper_left_t) * rdp_device.frame_buffer_info.texture_width) + 7) >> 3, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RDP::Op::LoadBlock:
|
||||
{
|
||||
uint32_t upper_left_s = ((w1 >> 12) & 0xFFF);
|
||||
uint32_t upper_left_t = (w1 & 0xFFF);
|
||||
uint32_t offset_address = (rdp_device.frame_buffer_info.texture_address + pixel_size(rdp_device.frame_buffer_info.texture_pixel_size, upper_left_s + upper_left_t * rdp_device.frame_buffer_info.texture_width)) >> 3;
|
||||
if (offset_address < rdram_dirty.size() && !rdram_dirty[offset_address])
|
||||
{
|
||||
uint32_t lower_right_s = ((w2 >> 12) & 0xFFF);
|
||||
std::fill_n(rdram_dirty.begin() + offset_address, (pixel_size(rdp_device.frame_buffer_info.texture_pixel_size, lower_right_s - upper_left_s) + 7) >> 3, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RDP::Op::SetColorImage:
|
||||
rdp_device.frame_buffer_info.framebuffer_address = (w2 & 0x00FFFFFF);
|
||||
rdp_device.frame_buffer_info.framebuffer_pixel_size = (w1 >> 19) & 0x3;
|
||||
rdp_device.frame_buffer_info.framebuffer_width = (w1 & 0x3FF) + 1;
|
||||
break;
|
||||
case RDP::Op::SetMaskImage:
|
||||
rdp_device.frame_buffer_info.depthbuffer_address = (w2 & 0x00FFFFFF);
|
||||
break;
|
||||
case RDP::Op::SetTextureImage:
|
||||
rdp_device.frame_buffer_info.texture_address = (w2 & 0x00FFFFFF);
|
||||
rdp_device.frame_buffer_info.texture_pixel_size = (w1 >> 19) & 0x3;
|
||||
rdp_device.frame_buffer_info.texture_width = (w1 & 0x3FF) + 1;
|
||||
break;
|
||||
case RDP::Op::SetScissor:
|
||||
{
|
||||
uint32_t upper_left_x = ((w1 >> 12) & 0xFFF) >> 2;
|
||||
@@ -553,14 +775,21 @@ uint64_t rdp_process_commands()
|
||||
{
|
||||
rdp_device.region = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
rdp_device.frame_buffer_info.framebuffer_y_offset = upper_left_y;
|
||||
rdp_device.frame_buffer_info.framebuffer_height = lower_right_y - upper_left_y;
|
||||
}
|
||||
break;
|
||||
case RDP::Op::SetOtherModes:
|
||||
{
|
||||
uint8_t cycle_type = (w1 >> 20) & 3;
|
||||
uint8_t depth_read_write = (w2 >> 4) & 3;
|
||||
rdp_device.frame_buffer_info.depth_buffer_enabled = ((cycle_type & 2) == 0) && (depth_read_write != 0);
|
||||
}
|
||||
break;
|
||||
case RDP::Op::SyncFull:
|
||||
if (rdp_device.last_sync != rdp_device.vi_counter)
|
||||
{
|
||||
processor->wait_for_timeline(processor->signal_timeline());
|
||||
rdp_device.last_sync = rdp_device.vi_counter;
|
||||
}
|
||||
sync_signal = processor->signal_timeline();
|
||||
|
||||
interrupt_timer = rdp_device.region;
|
||||
if (interrupt_timer == 0)
|
||||
interrupt_timer = 5000;
|
||||
|
||||
@@ -34,15 +34,20 @@ 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);
|
||||
void rdp_check_framebuffers(uint32_t address, uint32_t length);
|
||||
size_t rdp_state_size();
|
||||
void rdp_save_state(uint8_t *state);
|
||||
void rdp_load_state(const uint8_t *state);
|
||||
|
||||
Submodule parallel-rdp/parallel-rdp-standalone updated: 3bff34d835...ba3293573d
+1
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
channel = "1.90.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
|
||||
};
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
let data = std::str::from_utf8(
|
||||
&device.cart.is_viewer_buffer[0x20_usize..(0x20 + length) as usize],
|
||||
);
|
||||
if data.is_ok() {
|
||||
print!("{}", data.unwrap());
|
||||
if let Ok(data) = data {
|
||||
print!("{}", data);
|
||||
}
|
||||
} else {
|
||||
let mut data = u32::from_be_bytes(
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
use crate::device;
|
||||
|
||||
const MI_INIT_MODE_REG: u32 = 0;
|
||||
pub const MI_INIT_MODE_REG: u32 = 0;
|
||||
const MI_VERSION_REG: u32 = 1;
|
||||
pub const MI_INTR_REG: u32 = 2;
|
||||
pub const MI_INTR_MASK_REG: u32 = 3;
|
||||
@@ -29,7 +29,7 @@ const MI_CLR_DP: u32 = 1 << 10;
|
||||
const MI_SET_DP: u32 = 1 << 11;
|
||||
|
||||
/* mode read */
|
||||
const MI_INIT_MODE: u32 = 1 << 7;
|
||||
pub const MI_INIT_MODE: u32 = 1 << 7;
|
||||
const MI_EBUS_MODE: u32 = 1 << 8;
|
||||
const MI_RDRAM_MODE: u32 = 1 << 9;
|
||||
|
||||
@@ -42,7 +42,7 @@ const MI_CLR_DP_INTR: u32 = 1 << 11;
|
||||
const MI_CLR_RDRAM: u32 = 1 << 12;
|
||||
const MI_SET_RDRAM: u32 = 1 << 13;
|
||||
|
||||
const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Mi {
|
||||
@@ -78,9 +78,15 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
|
||||
|
||||
if w & MI_CLR_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
|
||||
}
|
||||
if w & MI_SET_INIT != 0 {
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
|
||||
}
|
||||
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
|
||||
}
|
||||
if w & MI_CLR_EBUS != 0 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
const PI_DRAM_ADDR_REG: u32 = 0;
|
||||
@@ -92,6 +93,9 @@ fn dma_write(device: &mut device::Device) {
|
||||
length -= dram_addr & 0x7;
|
||||
}
|
||||
|
||||
// check RDP before PI writes to RDRAM
|
||||
ui::video::check_framebuffers(dram_addr, length);
|
||||
|
||||
let cycles = (handler.write)(device, cart_addr, dram_addr, length);
|
||||
|
||||
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use std::alloc::{Layout, alloc_zeroed};
|
||||
|
||||
//const RDRAM_CONFIG_REG: u32 = 0;
|
||||
@@ -50,6 +51,8 @@ pub fn read_mem(
|
||||
);
|
||||
let masked_address = address as usize & RDRAM_MASK;
|
||||
|
||||
ui::video::check_framebuffers(masked_address as u32, 4);
|
||||
|
||||
u32::from_ne_bytes(
|
||||
device
|
||||
.rdram
|
||||
@@ -62,6 +65,8 @@ pub fn read_mem(
|
||||
}
|
||||
|
||||
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
ui::video::check_framebuffers(address as u32, 4);
|
||||
|
||||
let mut data = u32::from_ne_bytes(
|
||||
device
|
||||
.rdram
|
||||
@@ -80,6 +85,39 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
|
||||
.copy_from_slice(&data.to_ne_bytes());
|
||||
}
|
||||
|
||||
pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, mask: u32) {
|
||||
if mask != 0xFFFFFFFF {
|
||||
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
|
||||
}
|
||||
|
||||
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
|
||||
& device::mi::MI_INIT_LENGTH_MASK)
|
||||
+ 1;
|
||||
|
||||
if !repeat_length.is_multiple_of(4) {
|
||||
panic!(
|
||||
"RDRAM write_mem_repeat called with non-word-aligned length {}",
|
||||
repeat_length
|
||||
);
|
||||
}
|
||||
|
||||
ui::video::check_framebuffers(address as u32, repeat_length);
|
||||
|
||||
for i in (0..repeat_length as u64).step_by(4) {
|
||||
device
|
||||
.rdram
|
||||
.mem
|
||||
.get_mut((address + i) as usize..(address + i) as usize + 4)
|
||||
.unwrap_or(&mut [0; 4])
|
||||
.copy_from_slice(&value.to_ne_bytes());
|
||||
}
|
||||
|
||||
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
|
||||
for i in 0..(0x3F00000 >> 16) {
|
||||
device.memory.memory_map_write[i] = device::rdram::write_mem;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_regs(
|
||||
device: &mut device::Device,
|
||||
address: u64,
|
||||
|
||||
+14
-10
@@ -1,4 +1,5 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SP_MEM_ADDR_REG: u32 = 0;
|
||||
const SP_DRAM_ADDR_REG: u32 = 1;
|
||||
@@ -146,6 +147,7 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
|
||||
let mut dram_addr = dma.dramaddr & 0xfffff8;
|
||||
let offset = dma.memaddr & 0x1000;
|
||||
|
||||
ui::video::check_framebuffers(dram_addr, count * (length + skip) - skip);
|
||||
if dma.dir == DmaDir::Read {
|
||||
let mut j = 0;
|
||||
while j < count {
|
||||
@@ -219,6 +221,8 @@ fn fifo_push(device: &mut device::Device, dir: DmaDir) {
|
||||
panic!("RSP DMA already full")
|
||||
}
|
||||
|
||||
device.rsp.cpu.sync_point = true;
|
||||
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
|
||||
device.rsp.fifo[1].dir = dir;
|
||||
if dir == DmaDir::Read {
|
||||
@@ -276,21 +280,20 @@ pub fn read_regs(
|
||||
}
|
||||
let reg = (address & 0xFFFF) >> 2;
|
||||
match reg as u32 {
|
||||
SP_DMA_BUSY_REG | SP_DMA_FULL_REG => {
|
||||
if device.rsp.regs[reg as usize] != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
device.rsp.regs[reg as usize]
|
||||
}
|
||||
SP_STATUS_REG => {
|
||||
let value = device.rsp.regs[reg as usize]
|
||||
& !(SP_STATUS_HALT | SP_STATUS_BROKE | SP_STATUS_INTR_BREAK);
|
||||
& (SP_STATUS_SIG0
|
||||
| SP_STATUS_SIG1
|
||||
| SP_STATUS_SIG2
|
||||
| SP_STATUS_SIG3
|
||||
| SP_STATUS_SIG4
|
||||
| SP_STATUS_SIG5
|
||||
| SP_STATUS_SIG6
|
||||
| SP_STATUS_SIG7);
|
||||
if value == device.rsp.last_status_value && value != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
if value & (SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL) != 0 {
|
||||
device.rsp.cpu.sync_point = true;
|
||||
}
|
||||
|
||||
device.rsp.last_status_value = value;
|
||||
device.rsp.regs[reg as usize]
|
||||
}
|
||||
@@ -464,6 +467,7 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
|
||||
fn do_task(device: &mut device::Device) {
|
||||
device.rsp.cpu.sync_point = false;
|
||||
device.rsp.last_status_value = 0;
|
||||
device.rdp.last_status_value = 0;
|
||||
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
|
||||
device.rsp.run_after_dma = true
|
||||
} else {
|
||||
|
||||
@@ -304,25 +304,44 @@ pub fn lh(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 2];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFE {
|
||||
u16::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let w0 = device.rsp.mem[addr_masked];
|
||||
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
|
||||
((w0 as u16) << 8) | w1 as u16
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
(((w[0] as u16) << 8) | w[1] as u16) as i16 as i32 as u32
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value as i16 as i32 as u32;
|
||||
}
|
||||
|
||||
pub fn lw(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
|
||||
}
|
||||
|
||||
pub fn lbu(device: &mut device::Device, opcode: u32) {
|
||||
@@ -336,24 +355,44 @@ pub fn lhu(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 2];
|
||||
w[0] = device.rsp.mem[address as usize & 0xFFF];
|
||||
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFE {
|
||||
u16::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let w0 = device.rsp.mem[addr_masked];
|
||||
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
|
||||
((w0 as u16) << 8) | w1 as u16
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = (((w[0] as u16) << 8) | w[1] as u16) as u32
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value as u32;
|
||||
}
|
||||
|
||||
pub fn lwu(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
let mut w = [0; 4];
|
||||
for (i, item) in w.iter_mut().enumerate() {
|
||||
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] =
|
||||
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
|
||||
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
|
||||
}
|
||||
|
||||
pub fn sb(device: &mut device::Device, opcode: u32) {
|
||||
@@ -367,18 +406,30 @@ pub fn sh(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
device.rsp.mem[address as usize & 0xFFF] = (device.rsp.cpu.gpr[rt(opcode) as usize] >> 8) as u8;
|
||||
device.rsp.mem[(address as usize + 1) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize]) as u8;
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = device.rsp.cpu.gpr[rt(opcode) as usize] as u16;
|
||||
// Optimize for aligned access when possible
|
||||
if addr_masked <= 0xFFE {
|
||||
device.rsp.mem[addr_masked..addr_masked + 2].copy_from_slice(&value.to_be_bytes());
|
||||
} else {
|
||||
device.rsp.mem[addr_masked] = (value >> 8) as u8;
|
||||
device.rsp.mem[(addr_masked + 1) & 0xFFF] = value as u8;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sw(device: &mut device::Device, opcode: u32) {
|
||||
let address =
|
||||
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
|
||||
|
||||
for i in 0..4 {
|
||||
device.rsp.mem[(address as usize + i) & 0xFFF] =
|
||||
(device.rsp.cpu.gpr[rt(opcode) as usize] >> ((3 - i) * 8)) as u8;
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = device.rsp.cpu.gpr[rt(opcode) as usize];
|
||||
// Optimize for aligned access when possible
|
||||
if addr_masked <= 0xFFC {
|
||||
device.rsp.mem[addr_masked..addr_masked + 4].copy_from_slice(&value.to_be_bytes());
|
||||
} else {
|
||||
for i in 0..4 {
|
||||
device.rsp.mem[(addr_masked + i) & 0xFFF] = (value >> ((3 - i) * 8)) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,10 +788,21 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element.is_multiple_of(4) {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFFC {
|
||||
u32::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..4 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr32(
|
||||
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
|
||||
element / 4,
|
||||
@@ -767,10 +829,21 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element.is_multiple_of(8) {
|
||||
let mut value = 0;
|
||||
for i in 0..8 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
|
||||
}
|
||||
// Optimize for aligned access when possible
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF8 {
|
||||
u64::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..8 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u64) << (8 * (7 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr64(
|
||||
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
|
||||
element / 8,
|
||||
@@ -797,10 +870,21 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = velement(opcode);
|
||||
|
||||
if element == 0 && address.is_multiple_of(16) {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
|
||||
}
|
||||
// Optimize for aligned 16-byte access
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF0 {
|
||||
u128::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 16]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
|
||||
} else {
|
||||
let end = std::cmp::min(16 + element - ((address & 15) as u8), 16);
|
||||
@@ -823,10 +907,21 @@ pub fn lrv(device: &mut device::Device, opcode: u32) {
|
||||
let mut element = 16u8.wrapping_sub(((address & 15) as u8).wrapping_sub(velement(opcode)));
|
||||
address &= !15;
|
||||
if element == 0 {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
|
||||
}
|
||||
// Optimize for aligned 16-byte access
|
||||
let addr_masked = address as usize & 0xFFF;
|
||||
let value = if addr_masked <= 0xFF0 {
|
||||
u128::from_be_bytes(
|
||||
device.rsp.mem[addr_masked..addr_masked + 16]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
let mut value = 0;
|
||||
for i in 0..16 {
|
||||
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
|
||||
}
|
||||
value
|
||||
};
|
||||
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
|
||||
} else {
|
||||
while element < 16 {
|
||||
|
||||
+431
-447
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
|
||||
const SI_DRAM_ADDR_REG: u32 = 0;
|
||||
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
|
||||
@@ -98,6 +99,8 @@ fn copy_pif_rdram(device: &mut device::Device) {
|
||||
i += 4;
|
||||
}
|
||||
} else if device.si.dma_dir == DmaDir::Read {
|
||||
// check RDP before SI writes to RDRAM
|
||||
ui::video::check_framebuffers(dram_addr as u32, device::pif::PIF_RAM_SIZE as u32);
|
||||
let mut i = 0;
|
||||
while i < device::pif::PIF_RAM_SIZE {
|
||||
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
|
||||
|
||||
+10
-2
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -14,11 +15,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 +29,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 +37,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 +52,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 +78,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 +138,7 @@ impl Ui {
|
||||
joysticks,
|
||||
},
|
||||
storage: Storage {
|
||||
save_state_slot: 0,
|
||||
save_type: vec![],
|
||||
paths: storage::Paths {
|
||||
eep_file_path: std::path::PathBuf::new(),
|
||||
@@ -167,31 +181,17 @@ 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,
|
||||
},
|
||||
video: Video {
|
||||
window: std::ptr::null_mut(),
|
||||
fullscreen: false,
|
||||
},
|
||||
usb: Usb {
|
||||
usb_tx: None,
|
||||
cart_rx: None,
|
||||
},
|
||||
dirs,
|
||||
with_sdl,
|
||||
}
|
||||
|
||||
+9
-139
@@ -1,63 +1,8 @@
|
||||
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);
|
||||
|
||||
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
|
||||
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
|
||||
freq: 48000,
|
||||
channels: 2,
|
||||
};
|
||||
ui.audio.audio_device = unsafe {
|
||||
sdl3_sys::audio::SDL_OpenAudioDevice(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&device_audio_spec,
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_device == 0 {
|
||||
panic!("Could not open audio device");
|
||||
}
|
||||
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
|
||||
ui.audio.audio_device,
|
||||
&mut ui.audio.audio_device_spec,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
} {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -69,29 +14,26 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
|
||||
};
|
||||
|
||||
ui.audio.audio_stream = unsafe {
|
||||
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
|
||||
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
|
||||
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&game_audio_spec,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ui.audio.audio_stream.is_null() {
|
||||
return;
|
||||
panic!("Could not create audio stream");
|
||||
}
|
||||
if !unsafe {
|
||||
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
|
||||
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
|
||||
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
|
||||
} {
|
||||
panic!("Could not bind audio stream");
|
||||
panic!("Could not resume audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_game_audio(ui: &mut ui::Ui) {
|
||||
@@ -110,7 +52,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 +62,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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
+46
-9
@@ -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,10 +106,12 @@ 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,
|
||||
4 => 2,
|
||||
8 => 3,
|
||||
_ => 0,
|
||||
};
|
||||
app.set_resolution(combobox_value);
|
||||
@@ -200,6 +212,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 +222,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,7 +249,8 @@ 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();
|
||||
let upscale_values = [1, 2, 4];
|
||||
config.emulation.usb = app.get_emulate_usb();
|
||||
let upscale_values = [1, 2, 4, 8];
|
||||
config.video.upscale = upscale_values[app.get_resolution() as usize];
|
||||
|
||||
config.input.emulate_vru = app.get_emulate_vru();
|
||||
@@ -280,6 +300,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 +314,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 +379,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 +402,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 +445,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 +466,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 +537,7 @@ fn open_rom(app: &AppWindow) {
|
||||
}
|
||||
},
|
||||
None,
|
||||
usb,
|
||||
weak,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export { NetplayCreate } from "netplay_create.slint";
|
||||
export { NetplayJoin } from "netplay_join.slint";
|
||||
export { NetplayWait } from "netplay_wait.slint";
|
||||
export { CustomNetplayServer } from "netplay_page.slint";
|
||||
export { DispatcherDialog } from "netplay_create.slint";
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
callback wiki_button_clicked;
|
||||
@@ -42,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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,6 @@ export component NetplayCreate inherits Window {
|
||||
in-out property server_names <=> NetplayData.server_names;
|
||||
in-out property server_urls <=> NetplayData.server_urls;
|
||||
in-out property custom_server_url <=> NetplayData.custom_server_url;
|
||||
in-out property ping <=> NetplayData.ping;
|
||||
in-out property game_name <=> NetplayData.game_name;
|
||||
in-out property game_hash <=> NetplayData.game_hash;
|
||||
in-out property game_cheats <=> NetplayData.game_cheats;
|
||||
@@ -14,7 +13,6 @@ export component NetplayCreate inherits Window {
|
||||
in-out property rom_path <=> NetplayData.rom_path;
|
||||
in-out property peer_addr <=> NetplayData.peer_addr;
|
||||
in-out property rom_dir <=> NetplayData.rom_dir;
|
||||
callback get_ping(string);
|
||||
callback get_custom_url();
|
||||
callback select_rom(string);
|
||||
callback create_session(string, string, string, string, string, string, string);
|
||||
@@ -73,17 +71,10 @@ export component NetplayCreate inherits Window {
|
||||
if (self.current-index >= 0) {
|
||||
if (self.current-value == "Custom") {
|
||||
get_custom_url();
|
||||
} else {
|
||||
get_ping(NetplayData.server_urls[self.current-index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
vertical-alignment: center;
|
||||
text: NetplayData.ping;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,3 +121,10 @@ export component NetplayCreate inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component DispatcherDialog inherits Dialog {
|
||||
title: @tr("Creating Server");
|
||||
Text {
|
||||
text: "Creating server, please wait...This may take about 30 seconds.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,22 @@ export component NetplayJoin inherits Window {
|
||||
in-out property server_names <=> NetplayData.server_names;
|
||||
in-out property server_urls <=> NetplayData.server_urls;
|
||||
in-out property custom_server_url <=> NetplayData.custom_server_url;
|
||||
in-out property ping <=> NetplayData.ping;
|
||||
in-out property sessions <=> NetplayData.sessions;
|
||||
in-out property current_session <=> NetplayData.current_session;
|
||||
in-out property ports <=> NetplayData.ports;
|
||||
in-out property room_urls <=> NetplayData.room_urls;
|
||||
in-out property room_ports <=> NetplayData.room_ports;
|
||||
in-out property game_name <=> NetplayData.game_name;
|
||||
in-out property game_hash <=> NetplayData.game_hash;
|
||||
in-out property game_cheats <=> NetplayData.game_cheats;
|
||||
in-out property peer_addr <=> NetplayData.peer_addr;
|
||||
in-out property pending_session <=> NetplayData.pending_session;
|
||||
in-out property pending_refresh <=> NetplayData.pending_refresh;
|
||||
in-out property rom_path <=> NetplayData.rom_path;
|
||||
in-out property rom_dir <=> NetplayData.rom_dir;
|
||||
callback get_ping(string);
|
||||
callback get_custom_url();
|
||||
callback refresh_session(string);
|
||||
callback refresh_session();
|
||||
callback select_rom(string);
|
||||
callback join_session(string, string, string, int);
|
||||
callback join_session(string, string, string, string, int);
|
||||
preferred-width: 700px;
|
||||
|
||||
VerticalBox {
|
||||
HorizontalBox {
|
||||
@@ -40,43 +40,37 @@ export component NetplayJoin inherits Window {
|
||||
|
||||
VerticalBox {
|
||||
Text {
|
||||
text: @tr("Server:");
|
||||
text: @tr("Custom Server URL (optional):");
|
||||
}
|
||||
|
||||
HorizontalBox {
|
||||
server := ComboBox {
|
||||
model: NetplayData.server_names;
|
||||
selected => {
|
||||
if (self.current-index >= 0) {
|
||||
if (self.current-value == "Custom") {
|
||||
get_custom_url();
|
||||
} else {
|
||||
get_ping(NetplayData.server_urls[self.current-index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
vertical-alignment: center;
|
||||
text: NetplayData.ping;
|
||||
LineEdit {
|
||||
placeholder-text: @tr("example.com:45000");
|
||||
text: NetplayData.custom_server_url;
|
||||
edited(text) => {
|
||||
NetplayData.custom_server_url = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if NetplayData.sessions.length > 0: Text {
|
||||
if NetplayData.pending_refresh: Text {
|
||||
text: @tr("Refreshing session list, please wait...");
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if !NetplayData.pending_refresh && NetplayData.sessions.length > 0: Text {
|
||||
text: @tr("Click on a session below to select it");
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if NetplayData.sessions.length == 0: Text {
|
||||
text: @tr("No sessions available. Please refresh or try a different server.");
|
||||
if !NetplayData.pending_refresh && NetplayData.sessions.length == 0: Text {
|
||||
text: @tr("No sessions available. Please refresh to check again.");
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
StandardTableView {
|
||||
columns: [
|
||||
{ title: "Server" },
|
||||
{ title: "Session Name" },
|
||||
{ title: "Game Name" },
|
||||
{ title: "Password" },
|
||||
@@ -93,13 +87,15 @@ export component NetplayJoin inherits Window {
|
||||
Rectangle { }
|
||||
|
||||
Button {
|
||||
enabled: !NetplayData.pending_refresh;
|
||||
text: @tr("Refresh Session List");
|
||||
clicked => {
|
||||
if (server.current-value == "Custom") {
|
||||
refresh_session("ws://" + NetplayData.custom_server_url);
|
||||
} else {
|
||||
refresh_session(NetplayData.server_urls[server.current-index]);
|
||||
}
|
||||
NetplayData.pending_refresh = true;
|
||||
NetplayData.current_session = -1;
|
||||
NetplayData.sessions = [];
|
||||
NetplayData.room_urls = [];
|
||||
NetplayData.room_ports = [];
|
||||
refresh_session();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +130,8 @@ export component NetplayJoin inherits Window {
|
||||
NetplayData.player_name,
|
||||
NetplayData.game_hash,
|
||||
NetplayData.password,
|
||||
NetplayData.ports[NetplayData.current_session]);
|
||||
NetplayData.room_urls[NetplayData.current_session],
|
||||
NetplayData.room_ports[NetplayData.current_session]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Page } from "page.slint";
|
||||
export global NetplayData {
|
||||
in-out property <[string]> server_names;
|
||||
in-out property <[string]> server_urls;
|
||||
in-out property <string> ping: "Ping: Unknown";
|
||||
in-out property <string> player_name;
|
||||
in-out property <string> game_name;
|
||||
in-out property <string> game_hash;
|
||||
@@ -16,9 +15,11 @@ export global NetplayData {
|
||||
in-out property <string> password;
|
||||
in-out property <string> peer_addr;
|
||||
in-out property <bool> pending_session;
|
||||
in-out property <bool> pending_refresh;
|
||||
in-out property <string> session_name;
|
||||
in-out property <[[StandardListViewItem]]> sessions;
|
||||
in-out property <[int]> ports;
|
||||
in-out property <[string]> room_urls;
|
||||
in-out property <[int]> room_ports;
|
||||
in-out property <int> current_session: -1;
|
||||
in-out property <string> rom_dir;
|
||||
in-out property <string> custom_server_url;
|
||||
|
||||
@@ -7,6 +7,7 @@ export global NetplayWaitData {
|
||||
in-out property <int> port;
|
||||
in-out property <bool> can_start;
|
||||
in-out property <string> chat_text;
|
||||
in-out property <string> ping;
|
||||
}
|
||||
|
||||
export component NetplayWait inherits Window {
|
||||
@@ -21,6 +22,7 @@ export component NetplayWait inherits Window {
|
||||
in-out property port <=> NetplayWaitData.port;
|
||||
in-out property can_start <=> NetplayWaitData.can_start;
|
||||
in-out property chat_text <=> NetplayWaitData.chat_text;
|
||||
in-out property ping <=> NetplayWaitData.ping;
|
||||
callback send_chat_message(string);
|
||||
callback begin_game;
|
||||
|
||||
@@ -37,6 +39,11 @@ export component NetplayWait inherits Window {
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: @tr("Ping: ") + NetplayWaitData.ping;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
|
||||
GridBox {
|
||||
Row {
|
||||
Text {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ export component Settings inherits Page {
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
model: ["1x", "2x", "4x"];
|
||||
model: ["1x", "2x", "4x", "8x"];
|
||||
current-index: SettingsData.resolution;
|
||||
selected => {
|
||||
SettingsData.resolution = self.current-index;
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+315
-234
@@ -1,8 +1,8 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use crate::ui::gui::{
|
||||
AppWindow, CustomNetplayServer, ErrorDialog, GameSettings, GbPaths, NetplayCreate,
|
||||
NetplayDevice, NetplayJoin, NetplayWait, VruChannel, run_rom, save_settings,
|
||||
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths,
|
||||
NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, VruChannel, run_rom, save_settings,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -47,17 +47,14 @@ pub struct NetplayMessage {
|
||||
trait NetplayPages {
|
||||
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>);
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>);
|
||||
fn set_ping(&self, ping: slint::SharedString);
|
||||
fn set_game_name(&self, game_name: slint::SharedString);
|
||||
fn set_game_hash(&self, game_hash: slint::SharedString);
|
||||
fn set_game_cheats(&self, game_cheats: slint::SharedString);
|
||||
fn set_rom_path(&self, rom_path: slint::SharedString);
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString);
|
||||
fn refresh_sessions(&self, _server: slint::SharedString) {
|
||||
fn refresh_sessions(&self) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
fn invoke_get_ping(&self, server_url: slint::SharedString);
|
||||
fn set_custom_server_url(&self, server_url: slint::SharedString);
|
||||
}
|
||||
|
||||
impl NetplayPages for NetplayCreate {
|
||||
@@ -67,9 +64,6 @@ impl NetplayPages for NetplayCreate {
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_urls(urls);
|
||||
}
|
||||
fn set_ping(&self, ping: slint::SharedString) {
|
||||
self.set_ping(ping);
|
||||
}
|
||||
fn set_game_name(&self, game_name: slint::SharedString) {
|
||||
self.set_game_name(game_name);
|
||||
}
|
||||
@@ -85,12 +79,6 @@ impl NetplayPages for NetplayCreate {
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString) {
|
||||
self.set_peer_addr(peer_addr);
|
||||
}
|
||||
fn invoke_get_ping(&self, server_url: slint::SharedString) {
|
||||
self.invoke_get_ping(server_url);
|
||||
}
|
||||
fn set_custom_server_url(&self, server_url: slint::SharedString) {
|
||||
self.set_custom_server_url(server_url);
|
||||
}
|
||||
}
|
||||
|
||||
impl NetplayPages for NetplayJoin {
|
||||
@@ -100,11 +88,8 @@ impl NetplayPages for NetplayJoin {
|
||||
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
|
||||
self.set_server_urls(urls);
|
||||
}
|
||||
fn set_ping(&self, ping: slint::SharedString) {
|
||||
self.set_ping(ping);
|
||||
}
|
||||
fn refresh_sessions(&self, server: slint::SharedString) {
|
||||
self.invoke_refresh_session(server);
|
||||
fn refresh_sessions(&self) {
|
||||
self.invoke_refresh_session();
|
||||
}
|
||||
fn set_game_name(&self, game_name: slint::SharedString) {
|
||||
self.set_game_name(game_name);
|
||||
@@ -121,16 +106,13 @@ impl NetplayPages for NetplayJoin {
|
||||
fn set_peer_addr(&self, peer_addr: slint::SharedString) {
|
||||
self.set_peer_addr(peer_addr);
|
||||
}
|
||||
fn invoke_get_ping(&self, server_url: slint::SharedString) {
|
||||
self.invoke_get_ping(server_url);
|
||||
}
|
||||
fn set_custom_server_url(&self, server_url: slint::SharedString) {
|
||||
self.set_custom_server_url(server_url);
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
|
||||
let task = reqwest::get("https://cdn.gopher64.com/servers-gopher64.json");
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/getRegions")
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.send();
|
||||
tokio::spawn(async move {
|
||||
let mut local_servers: Vec<(String, String)> = vec![];
|
||||
|
||||
@@ -158,10 +140,9 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
|
||||
}
|
||||
|
||||
let response = task.await;
|
||||
if let Ok(response) = response {
|
||||
let servers: std::collections::HashMap<String, String> = response.json().await.unwrap();
|
||||
|
||||
let weak2 = weak.clone();
|
||||
if let Ok(response) = response
|
||||
&& let Ok(servers) = response.json::<Vec<String>>().await
|
||||
{
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
let server_urls: slint::VecModel<slint::SharedString> = slint::VecModel::default();
|
||||
@@ -170,12 +151,11 @@ fn populate_server_names<T: ComponentHandle + NetplayPages + 'static>(weak: slin
|
||||
server_urls.push(local_server.1.into());
|
||||
}
|
||||
for server in servers {
|
||||
server_names.push(server.0.into());
|
||||
server_urls.push(server.1.into());
|
||||
server_names.push(server.clone().into());
|
||||
server_urls.push(format!("dispatcher:{server}").into());
|
||||
}
|
||||
server_names.push("Custom".into());
|
||||
update_ping(weak2, server_urls.row_data(0).unwrap().into());
|
||||
handle.refresh_sessions(server_urls.row_data(0).unwrap());
|
||||
handle.refresh_sessions();
|
||||
let server_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(server_names);
|
||||
let server_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
@@ -207,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();
|
||||
@@ -216,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());
|
||||
@@ -245,48 +233,13 @@ fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
|
||||
});
|
||||
}
|
||||
|
||||
fn update_ping<T: ComponentHandle + NetplayPages + 'static>(
|
||||
weak: slint::Weak<T>,
|
||||
server_url: String,
|
||||
) {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping("Ping: Unknown".into());
|
||||
})
|
||||
.unwrap();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(Ok((mut sock, _response))) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
tokio_tungstenite::connect_async(server_url),
|
||||
)
|
||||
.await
|
||||
{
|
||||
sock.send(Message::Ping(Vec::new().into())).await.unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
if let Some(Ok(_response)) = sock.next().await {
|
||||
let elapsed = start.elapsed();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping(format!("Ping: {:.0} ms", elapsed.as_millis()).into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
sock.close(None).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_custom_url_dialog<T: ComponentHandle + NetplayPages + 'static>(
|
||||
weak: slint::Weak<T>,
|
||||
server_url: slint::SharedString,
|
||||
) {
|
||||
fn show_custom_url_dialog(weak: slint::Weak<NetplayCreate>, server_url: slint::SharedString) {
|
||||
let url_dialog = CustomNetplayServer::new().unwrap();
|
||||
url_dialog.set_custom_server_url(server_url);
|
||||
let weak_dialog = url_dialog.as_weak();
|
||||
url_dialog.on_ok_clicked(move |server_url| {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_custom_server_url(server_url.clone());
|
||||
let prefix: slint::SharedString = "ws://".into();
|
||||
handle.invoke_get_ping(prefix + &server_url);
|
||||
})
|
||||
.unwrap();
|
||||
weak_dialog.unwrap().window().hide().unwrap();
|
||||
@@ -313,14 +266,9 @@ pub fn setup_create_window(
|
||||
create_window.set_rom_dir(rom_dir);
|
||||
populate_server_names(create_window.as_weak());
|
||||
let weak = create_window.as_weak();
|
||||
create_window.on_get_ping(move |server_url| {
|
||||
update_ping(weak.clone(), server_url.to_string());
|
||||
});
|
||||
let weak = create_window.as_weak();
|
||||
create_window.on_get_custom_url(move || {
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping("Ping: Unknown".into());
|
||||
show_custom_url_dialog(weak2, handle.get_custom_server_url());
|
||||
})
|
||||
.unwrap();
|
||||
@@ -340,26 +288,87 @@ pub fn setup_create_window(
|
||||
game_cheats,
|
||||
password| {
|
||||
let _ = netplay_write_sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
if server_url.starts_with("dispatcher:") {
|
||||
let message_dialog = DispatcherDialog::new().unwrap();
|
||||
let weak_dialog = message_dialog.as_weak();
|
||||
message_dialog.show().unwrap();
|
||||
|
||||
create_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
session_name.to_string(),
|
||||
player_name.to_string(),
|
||||
game_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings.clone(),
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/createServer")
|
||||
.query(&[("region", server_url.strip_prefix("dispatcher:").unwrap())])
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.send();
|
||||
let netplay_read_sender = netplay_read_sender.clone();
|
||||
let netplay_write_receiver = netplay_write_receiver.resubscribe();
|
||||
let netplay_write_sender = netplay_write_sender.clone();
|
||||
let netplay_read_receiver = netplay_read_receiver.resubscribe();
|
||||
let game_settings = game_settings.clone();
|
||||
let weak = weak.clone();
|
||||
let weak_app = weak_app.clone();
|
||||
tokio::spawn(async move {
|
||||
let response = task.await;
|
||||
weak_dialog
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
handle.window().hide().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
if let Ok(response) = response
|
||||
&& let Ok(server) = response
|
||||
.json::<std::collections::HashMap<String, String>>()
|
||||
.await
|
||||
{
|
||||
let server_url = server.values().next().unwrap();
|
||||
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender,
|
||||
netplay_write_receiver,
|
||||
weak.clone(),
|
||||
);
|
||||
|
||||
create_session(
|
||||
netplay_write_sender,
|
||||
netplay_read_receiver,
|
||||
session_name.to_string(),
|
||||
player_name.to_string(),
|
||||
game_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings,
|
||||
weak_app,
|
||||
weak.clone(),
|
||||
);
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(|handle| {
|
||||
handle.set_pending_session(false);
|
||||
show_netplay_error("Server could not be created".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
|
||||
create_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
session_name.to_string(),
|
||||
player_name.to_string(),
|
||||
game_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
game_cheats.to_string(),
|
||||
password.to_string(),
|
||||
game_settings.clone(),
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -412,8 +421,11 @@ fn manage_websocket<T: ComponentHandle + NetplayPages + 'static>(
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("netplay_write_receiver error: {err}");
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
panic!("netplay_write_receiver lagged");
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break; // exit the loop if the receiver is closed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,68 +445,114 @@ fn show_netplay_error(message: String) {
|
||||
message_dialog.show().unwrap();
|
||||
}
|
||||
|
||||
fn clear_sessions(handle: &NetplayJoin, message: Option<String>) {
|
||||
handle.set_sessions(slint::ModelRc::default());
|
||||
handle.set_ports(slint::ModelRc::default());
|
||||
handle.set_current_session(-1);
|
||||
if let Some(message) = message {
|
||||
show_netplay_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_sessions(
|
||||
netplay_write_sender: tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
|
||||
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
fn update_sessions(weak: slint::Weak<NetplayJoin>) {
|
||||
let task = reqwest::Client::new()
|
||||
.get("https://dispatch.gopher64.com/getServers")
|
||||
.header("netplay-id", EMU_NAME)
|
||||
.send();
|
||||
tokio::spawn(async move {
|
||||
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
|
||||
let request_rooms = NetplayMessage {
|
||||
message_type: "request_get_rooms".to_string(),
|
||||
player_name: None,
|
||||
client_sha: None,
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
player_names: None,
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
rooms: None,
|
||||
message: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
room: None,
|
||||
};
|
||||
|
||||
netplay_write_sender.send(Some(request_rooms)).unwrap();
|
||||
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
netplay_read_receiver.recv(),
|
||||
)
|
||||
.await
|
||||
let mut dispatcher_servers = std::collections::HashMap::new();
|
||||
let response = task.await;
|
||||
if let Ok(response) = response
|
||||
&& let Ok(servers) = response
|
||||
.json::<std::collections::HashMap<String, String>>()
|
||||
.await
|
||||
{
|
||||
Ok(Ok(message)) => {
|
||||
if message.accept.unwrap() == 0 {
|
||||
if let Some(rooms) = message.rooms {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let sessions_vec = slint::VecModel::default();
|
||||
let ports_vec = slint::VecModel::default();
|
||||
dispatcher_servers = servers;
|
||||
}
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let mut servers: std::collections::HashMap<String, String> =
|
||||
std::collections::HashMap::new();
|
||||
let server_names = handle.get_server_names();
|
||||
let server_urls = handle.get_server_urls();
|
||||
for (i, server_name) in server_names.iter().enumerate() {
|
||||
if server_name == "Custom" {
|
||||
let custom_server_url = handle.get_custom_server_url();
|
||||
if !custom_server_url.is_empty() {
|
||||
servers.insert(
|
||||
"Custom".to_string(),
|
||||
"ws://".to_string() + &custom_server_url,
|
||||
);
|
||||
}
|
||||
} else if let Some(url) = server_urls.row_data(i)
|
||||
&& url.starts_with("dispatcher:")
|
||||
{
|
||||
continue;
|
||||
} else {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
server_urls.row_data(i).unwrap().to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
servers.extend(dispatcher_servers);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut sessions = vec![];
|
||||
let mut room_urls = vec![];
|
||||
let mut room_ports = vec![];
|
||||
for (server_name, server_url) in servers.iter() {
|
||||
if let Ok(Ok((socket, _response))) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
tokio_tungstenite::connect_async(server_url.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let (mut write, mut read) = socket.split();
|
||||
|
||||
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
|
||||
let request_rooms = NetplayMessage {
|
||||
message_type: "request_get_rooms".to_string(),
|
||||
player_name: None,
|
||||
client_sha: None,
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
player_names: None,
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
rooms: None,
|
||||
message: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
room: None,
|
||||
};
|
||||
write
|
||||
.send(Message::Binary(Bytes::from(
|
||||
serde_json::to_vec(&request_rooms).unwrap(),
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(Ok(response)) = read.next().await
|
||||
&& let Ok(message) =
|
||||
serde_json::from_slice::<NetplayMessage>(&response.into_data())
|
||||
&& message.message_type == "reply_get_rooms"
|
||||
&& message.accept.unwrap() == 0
|
||||
&& let Some(rooms) = message.rooms
|
||||
{
|
||||
for room in rooms {
|
||||
let session_vec = slint::VecModel::default();
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
let mut session = vec![];
|
||||
room_urls.push(server_url.into());
|
||||
room_ports.push(room.port.unwrap());
|
||||
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(server_name),
|
||||
));
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.room_name.unwrap()),
|
||||
));
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(room.game_name.unwrap()),
|
||||
));
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(if room.protected.unwrap() {
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
}),
|
||||
));
|
||||
session_vec.push(slint::StandardListViewItem::from(
|
||||
session.push(slint::StandardListViewItem::from(
|
||||
slint::SharedString::from(
|
||||
if room.features.unwrap_or_default().contains_key("cheats")
|
||||
{
|
||||
@@ -504,45 +562,43 @@ fn update_sessions(
|
||||
},
|
||||
),
|
||||
));
|
||||
let session_model: std::rc::Rc<
|
||||
slint::VecModel<slint::StandardListViewItem>,
|
||||
> = std::rc::Rc::new(session_vec);
|
||||
sessions_vec.push(slint::ModelRc::from(session_model));
|
||||
ports_vec.push(room.port.unwrap());
|
||||
sessions.push(session);
|
||||
}
|
||||
let rooms_model: std::rc::Rc<
|
||||
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
|
||||
> = std::rc::Rc::new(sessions_vec);
|
||||
let ports_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(ports_vec);
|
||||
handle.set_sessions(slint::ModelRc::from(rooms_model));
|
||||
handle.set_ports(slint::ModelRc::from(ports_model));
|
||||
handle.set_current_session(-1);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, None);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, message.message);
|
||||
}
|
||||
weak2
|
||||
.upgrade_in_event_loop(move |handle| {
|
||||
let sessions_vec = slint::VecModel::default();
|
||||
for session in sessions.iter() {
|
||||
let session_vec = slint::VecModel::from(session.to_vec());
|
||||
let session_model: std::rc::Rc<
|
||||
slint::VecModel<slint::StandardListViewItem>,
|
||||
> = std::rc::Rc::new(session_vec);
|
||||
sessions_vec.push(slint::ModelRc::from(session_model));
|
||||
}
|
||||
let rooms_model: std::rc::Rc<
|
||||
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
|
||||
> = std::rc::Rc::new(sessions_vec);
|
||||
handle.set_sessions(slint::ModelRc::from(rooms_model));
|
||||
|
||||
let room_urls_vec = slint::VecModel::from(room_urls.to_vec());
|
||||
let room_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
|
||||
std::rc::Rc::new(room_urls_vec);
|
||||
handle.set_room_urls(slint::ModelRc::from(room_urls_model));
|
||||
|
||||
let room_ports_vec = slint::VecModel::from(room_ports.to_vec());
|
||||
let room_ports_model: std::rc::Rc<slint::VecModel<i32>> =
|
||||
std::rc::Rc::new(room_ports_vec);
|
||||
handle.set_room_ports(slint::ModelRc::from(room_ports_model));
|
||||
|
||||
handle.set_current_session(-1);
|
||||
handle.set_pending_refresh(false);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
panic!("netplay_read_receiver error: {err}");
|
||||
}
|
||||
Err(_) => {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
clear_sessions(&handle, Some("Server did not respond".to_string()));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,7 +664,7 @@ fn create_session(
|
||||
.await
|
||||
{
|
||||
Ok(Ok(message)) => {
|
||||
if message.accept.unwrap() == 0 {
|
||||
if message.message_type == "reply_create_room" && message.accept.unwrap() == 0 {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let session = message.room.as_ref().unwrap();
|
||||
let features_default = "false".to_string();
|
||||
@@ -683,31 +739,34 @@ fn join_session(
|
||||
player_name: String,
|
||||
game_hash: String,
|
||||
password: String,
|
||||
port: i32,
|
||||
room_port: i32,
|
||||
fullscreen: bool,
|
||||
weak_app: slint::Weak<AppWindow>,
|
||||
weak: slint::Weak<NetplayJoin>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let now_utc = chrono::Utc::now().timestamp_millis().to_string();
|
||||
let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME);
|
||||
|
||||
let join_room = NetplayMessage {
|
||||
message_type: "request_join_room".to_string(),
|
||||
player_name: Some(player_name),
|
||||
client_sha: Some(env!("GIT_HASH").to_string()),
|
||||
netplay_version: None,
|
||||
emulator: None,
|
||||
netplay_version: Some(NETPLAY_VERSION),
|
||||
emulator: Some(EMU_NAME.to_string()),
|
||||
accept: None,
|
||||
message: None,
|
||||
rooms: None,
|
||||
auth_time: None,
|
||||
player_names: None,
|
||||
auth: None,
|
||||
auth_time: Some(now_utc),
|
||||
auth: Some(format!("{:x}", hasher.finalize())),
|
||||
room: Some(NetplayRoom {
|
||||
room_name: None,
|
||||
password: Some(password),
|
||||
game_name: None,
|
||||
md5: Some(game_hash),
|
||||
protected: None,
|
||||
port: Some(port),
|
||||
port: Some(room_port),
|
||||
features: None,
|
||||
buffer_target: None,
|
||||
}),
|
||||
@@ -722,7 +781,7 @@ fn join_session(
|
||||
.await
|
||||
{
|
||||
Ok(Ok(message)) => {
|
||||
if message.accept.unwrap() == 0 {
|
||||
if message.message_type == "reply_join_room" && message.accept.unwrap() == 0 {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
let session = message.room.as_ref().unwrap();
|
||||
let features_default = "false".to_string();
|
||||
@@ -814,6 +873,7 @@ fn setup_wait_window(
|
||||
wait.set_rom_path(rom_path);
|
||||
wait.set_port(port);
|
||||
wait.set_can_start(can_start);
|
||||
wait.set_ping("Unknown".into());
|
||||
|
||||
let sender = netplay_write_sender.clone();
|
||||
wait.on_send_chat_message(move |message| {
|
||||
@@ -894,11 +954,44 @@ fn setup_wait_window(
|
||||
|
||||
netplay_write_sender.send(Some(motd_message)).unwrap();
|
||||
|
||||
let ping_message = NetplayMessage {
|
||||
message_type: "request_ping".to_string(),
|
||||
player_name: None,
|
||||
client_sha: None,
|
||||
netplay_version: None,
|
||||
emulator: None,
|
||||
accept: None,
|
||||
rooms: None,
|
||||
player_names: None,
|
||||
message: None,
|
||||
auth_time: None,
|
||||
auth: None,
|
||||
room: None,
|
||||
};
|
||||
|
||||
netplay_write_sender
|
||||
.send(Some(ping_message.clone()))
|
||||
.unwrap();
|
||||
|
||||
let weak = wait.as_weak();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match netplay_read_receiver.recv().await {
|
||||
Ok(response) => match response.message_type.as_str() {
|
||||
"reply_ping" => {
|
||||
if let Some(message) = response.message {
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping((message + " ms").into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let ping_message = ping_message.clone();
|
||||
let ping_writer = netplay_write_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
let _ = ping_writer.send(Some(ping_message));
|
||||
});
|
||||
}
|
||||
"reply_motd" => {
|
||||
let request_players = NetplayMessage {
|
||||
message_type: "request_players".to_string(),
|
||||
@@ -996,6 +1089,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,
|
||||
);
|
||||
})
|
||||
@@ -1044,26 +1141,10 @@ pub fn setup_join_window(
|
||||
tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
|
||||
) = tokio::sync::broadcast::channel(5);
|
||||
|
||||
join_window.set_pending_refresh(true);
|
||||
join_window.set_rom_dir(rom_dir);
|
||||
populate_server_names(join_window.as_weak());
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_get_ping(move |server_url| {
|
||||
update_ping(weak.clone(), server_url.to_string());
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.invoke_refresh_session(server_url);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_get_custom_url(move || {
|
||||
let weak2 = weak.clone();
|
||||
weak.upgrade_in_event_loop(move |handle| {
|
||||
handle.set_ping("Ping: Unknown".into());
|
||||
show_custom_url_dialog(weak2, handle.get_custom_server_url());
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_select_rom(move |rom_dir| {
|
||||
select_rom(weak.clone(), rom_dir);
|
||||
});
|
||||
@@ -1073,34 +1154,34 @@ pub fn setup_join_window(
|
||||
let _ = sender.send(None); // close current websocket if any
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_refresh_session(move || {
|
||||
update_sessions(weak.clone());
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_join_session(
|
||||
move |player_name, game_hash, password, room_url, room_port| {
|
||||
let _ = netplay_write_sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
room_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
|
||||
let weak = join_window.as_weak();
|
||||
let sender = netplay_write_sender.clone();
|
||||
let receiver = netplay_read_receiver.resubscribe();
|
||||
join_window.on_refresh_session(move |server_url| {
|
||||
let _ = sender.send(None); // close current websocket if any
|
||||
manage_websocket(
|
||||
server_url.to_string(),
|
||||
netplay_read_sender.clone(),
|
||||
netplay_write_receiver.resubscribe(),
|
||||
weak.clone(),
|
||||
);
|
||||
update_sessions(sender.clone(), receiver.resubscribe(), weak.clone());
|
||||
});
|
||||
let weak = join_window.as_weak();
|
||||
join_window.on_join_session(move |player_name, game_hash, password, port| {
|
||||
join_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
player_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
port,
|
||||
fullscreen,
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
});
|
||||
join_session(
|
||||
netplay_write_sender.clone(),
|
||||
netplay_read_receiver.resubscribe(),
|
||||
player_name.to_string(),
|
||||
game_hash.to_string(),
|
||||
password.to_string(),
|
||||
room_port,
|
||||
fullscreen,
|
||||
weak_app.clone(),
|
||||
weak.clone(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
join_window.show().unwrap();
|
||||
}
|
||||
|
||||
+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();
|
||||
}
|
||||
+63
-58
@@ -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,34 @@ pub fn process_rdp_list() -> u64 {
|
||||
unsafe { rdp_process_commands() }
|
||||
}
|
||||
|
||||
pub fn check_framebuffers(address: u32, length: u32) {
|
||||
unsafe { rdp_check_framebuffers(address, length) }
|
||||
}
|
||||
|
||||
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