From 67979a03eaca130046651b9285d2ff5a74f69532 Mon Sep 17 00:00:00 2001
From: Alula <6276139+alula@users.noreply.github.com>
Date: Thu, 1 Dec 2022 14:30:59 +0100
Subject: [PATCH] Mostly-working Horizon port
---
.gitignore | 3 +
Cargo.toml | 9 +-
drshorizon/Cargo.toml | 15 +-
drshorizon/aarch64-nintendo-switch.json | 9 +-
drshorizon/build_debug.sh | 20 +-
drshorizon/build_release.sh | 33 +
drshorizon/src/main.rs | 30 +-
drshorizon/symbolize.js | 51 +
rustfmt.toml | 2 +-
src/framework/backend.rs | 17 +-
src/framework/backend_horizon.rs | 1378 +++++++++++++++++
src/framework/backend_null.rs | 18 -
src/framework/backend_sdl2.rs | 45 +-
src/framework/mod.rs | 2 +
src/framework/render_opengl.rs | 149 +-
.../shaders/deko3d/fragment_color.glsl | 11 +
.../shaders/deko3d/fragment_textured.glsl | 13 +
.../shaders/deko3d/vertex_basic.glsl | 20 +
.../shaders/opengl/fragment_color_110.glsl | 9 +
.../shaders/opengl/fragment_textured_110.glsl | 10 +
.../shaders/opengl/fragment_water_110.glsl | 42 +
.../shaders/opengl/vertex_basic_110.glsl | 15 +
.../shaders/opengles/fragment_color_100.glsl | 11 +
.../opengles/fragment_textured_100.glsl | 12 +
.../shaders/opengles/vertex_basic_100.glsl | 17 +
src/framework/vfs.rs | 19 +-
src/game/mod.rs | 30 +-
src/game/settings.rs | 29 +-
src/scene/no_data_scene.rs | 43 +-
src/util/browser.rs | 7 +-
30 files changed, 1838 insertions(+), 231 deletions(-)
create mode 100755 drshorizon/build_release.sh
create mode 100644 drshorizon/symbolize.js
create mode 100644 src/framework/backend_horizon.rs
create mode 100644 src/framework/shaders/deko3d/fragment_color.glsl
create mode 100644 src/framework/shaders/deko3d/fragment_textured.glsl
create mode 100644 src/framework/shaders/deko3d/vertex_basic.glsl
create mode 100644 src/framework/shaders/opengl/fragment_color_110.glsl
create mode 100644 src/framework/shaders/opengl/fragment_textured_110.glsl
create mode 100644 src/framework/shaders/opengl/fragment_water_110.glsl
create mode 100644 src/framework/shaders/opengl/vertex_basic_110.glsl
create mode 100644 src/framework/shaders/opengles/fragment_color_100.glsl
create mode 100644 src/framework/shaders/opengles/fragment_textured_100.glsl
create mode 100644 src/framework/shaders/opengles/vertex_basic_100.glsl
diff --git a/.gitignore b/.gitignore
index ab742d1..7d3e872 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@
debug/
target/
+# Shader binary files
+*.dksh
+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
index b2f0fad..958676d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,13 +21,14 @@ panic = "abort"
[profile.dev.package."*"]
opt-level = 3
+overflow-checks = false
[package.metadata.bundle]
name = "doukutsu-rs"
identifier = "io.github.doukutsu_rs"
version = "0.100.0"
resources = ["data"]
-copyright = "Copyright (c) 2020-2022 doukutsu-rs dev team"
+copyright = "Copyright (c) 2020-2022 doukutsu-rs contributors"
category = "Game"
osx_minimum_system_version = "10.12"
@@ -37,6 +38,7 @@ default-base = ["ogg-playback"]
ogg-playback = ["lewton"]
backend-sdl = ["sdl2", "sdl2-sys"]
backend-glutin = ["winit", "glutin", "render-opengl"]
+backend-horizon = []
render-opengl = []
scripting-lua = ["lua-ffi"]
netplay = ["serde_cbor"]
@@ -50,10 +52,11 @@ android = []
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
+cpal = { path = "./3rdparty/cpal" }
byteorder = "1.4"
case_insensitive_hashmap = "1.0.0"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
-cpal = "0.14"
+#cpal = "0.14"
directories = "3"
downcast = "0.11"
#glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "8dd457b9adb7dbac7ade337246b6356c784272d9", optional = true, default_features = false, features = ["x11"] }
@@ -102,3 +105,5 @@ ndk-sys = "0.4"
jni = "0.20"
[target.'cfg(target_os = "horizon")'.dependencies]
+#deko3d = { path = "./3rdparty/deko3d" }
+deko3d = { git = "https://github.com/doukutsu-rs/deko3d-rs", branch = "master" }
diff --git a/drshorizon/Cargo.toml b/drshorizon/Cargo.toml
index effae96..7c62b00 100644
--- a/drshorizon/Cargo.toml
+++ b/drshorizon/Cargo.toml
@@ -4,7 +4,18 @@ description = "doukutsu-rs targeted for Nintendo Switch"
version = "0.1.0"
edition = "2021"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[profile.release]
+opt-level = 3
+#incremental = true
+
+[profile.dev.package."*"]
+opt-level = 3
+overflow-checks = false
+incremental = true
+[profile.dev.package."doukutsu-rs"]
+opt-level = 3
+overflow-checks = false
+incremental = true
[dependencies]
-doukutsu-rs = { path = "../", default-features = false, features = ["default-base"] }
+doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-horizon"] }
diff --git a/drshorizon/aarch64-nintendo-switch.json b/drshorizon/aarch64-nintendo-switch.json
index 28a30a5..c1b1062 100644
--- a/drshorizon/aarch64-nintendo-switch.json
+++ b/drshorizon/aarch64-nintendo-switch.json
@@ -8,6 +8,7 @@
"exe-suffix": ".elf",
"features": "+a57,+strict-align,+crc,+crypto",
"has-rpath": false,
+ "has-thread-local": false,
"linker": "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc",
"linker-flavor": "gcc",
"llvm-target": "aarch64-unknown-none",
@@ -26,14 +27,18 @@
"-L",
"/opt/devkitpro/portlibs/switch/lib",
"-L",
- "/opt/devkitpro/libnx/lib"
+ "/opt/devkitpro/libnx/lib",
+ "-I",
+ "/opt/devkitpro/libnx/include"
]
},
"relocation-model": "pic",
"requires-uwtable": true,
"target-c-int-width": "32",
"target-endian": "little",
- "target-family": ["unix"],
+ "target-family": [
+ "unix"
+ ],
"target-pointer-width": "64",
"trap-unreachable": true,
"vendor": "nintendo"
diff --git a/drshorizon/build_debug.sh b/drshorizon/build_debug.sh
index 0fa9bf8..aab50c5 100755
--- a/drshorizon/build_debug.sh
+++ b/drshorizon/build_debug.sh
@@ -3,17 +3,31 @@
cd "$(dirname "$0")" || exit
set -e
+DARK_GRAY=$(tput setaf 8)
+YELLOW=$(tput bold)$(tput setaf 3)
+RESET=$(tput sgr0)
+
+function message() {
+ echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
+}
+
+message "Compiling shaders..."
+uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
+uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
+uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
+
+message "Building crate..."
rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json
rm -f target/aarch64-nintendo-switch/debug/drshorizon.nro
rm -f target/aarch64-nintendo-switch/debug/drshorizon.nacp
-echo "Creating NACP..."
+message "Creating NACP..."
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.100.0' target/aarch64-nintendo-switch/debug/drshorizon.nacp
-echo "Running elf2nro..."
+message "Running elf2nro..."
elf2nro target/aarch64-nintendo-switch/debug/drshorizon.elf target/aarch64-nintendo-switch/debug/drshorizon.nro \
--icon=../res/nx_icon.jpg \
--nacp=target/aarch64-nintendo-switch/debug/drshorizon.nacp
-echo "done."
+message "done!"
diff --git a/drshorizon/build_release.sh b/drshorizon/build_release.sh
new file mode 100755
index 0000000..5841ee2
--- /dev/null
+++ b/drshorizon/build_release.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+cd "$(dirname "$0")" || exit
+set -e
+
+DARK_GRAY=$(tput setaf 8)
+YELLOW=$(tput bold)$(tput setaf 3)
+RESET=$(tput sgr0)
+
+function message() {
+ echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
+}
+
+message "Compiling shaders..."
+uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
+uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
+uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
+
+message "Building crate..."
+rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json --release
+
+rm -f target/aarch64-nintendo-switch/release/drshorizon.nro
+rm -f target/aarch64-nintendo-switch/release/drshorizon.nacp
+
+message "Creating NACP..."
+nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.100.0' target/aarch64-nintendo-switch/release/drshorizon.nacp
+
+message "Running elf2nro..."
+elf2nro target/aarch64-nintendo-switch/release/drshorizon.elf target/aarch64-nintendo-switch/release/drshorizon.nro \
+ --icon=../res/nx_icon.jpg \
+ --nacp=target/aarch64-nintendo-switch/release/drshorizon.nacp
+
+message "done!"
diff --git a/drshorizon/src/main.rs b/drshorizon/src/main.rs
index 28cf722..2c39f95 100644
--- a/drshorizon/src/main.rs
+++ b/drshorizon/src/main.rs
@@ -3,15 +3,36 @@
#[repr(C)]
pub struct PrintConsole {}
-extern "C" {
- pub fn consoleInit(unk: *mut PrintConsole) -> *mut PrintConsole;
+#[repr(C)]
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum ApmCpuBoostMode {
+ Normal = 0,
+ FastLoad = 1,
+}
- pub fn consoleUpdate(unk: *mut PrintConsole);
+extern "C" {
+ fn consoleInit(unk: *mut PrintConsole) -> *mut PrintConsole;
+ fn consoleUpdate(unk: *mut PrintConsole);
+
+ fn socketInitialize(unk: *const std::ffi::c_void) -> u32;
+ fn nxlinkConnectToHost(redir_stdout: bool, redir_stderr: bool) -> i32;
+
+ fn appletSetCpuBoostMode(mode: ApmCpuBoostMode) -> u32;
+
+ static __text_start: u32;
}
fn main() {
unsafe {
- consoleInit(std::ptr::null_mut());
+ if socketInitialize(std::ptr::null()) == 0 {
+ nxlinkConnectToHost(true, true);
+ }
+
+ appletSetCpuBoostMode(ApmCpuBoostMode::FastLoad);
+
+ std::env::set_var("RUST_BACKTRACE", "full");
+
+ println!("__text_start = {:#x}", (&__text_start) as *const _ as usize);
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
let result = doukutsu_rs::game::init(options);
@@ -19,7 +40,6 @@ fn main() {
if let Err(e) = result {
println!("Initialization error: {}", e);
loop {
- consoleUpdate(std::ptr::null_mut());
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
diff --git a/drshorizon/symbolize.js b/drshorizon/symbolize.js
new file mode 100644
index 0000000..06ae04c
--- /dev/null
+++ b/drshorizon/symbolize.js
@@ -0,0 +1,51 @@
+//
+
+const readline = require('readline');
+const childProcess = require('child_process');
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ terminal: false
+});
+
+let textStart = 0;
+const textStartRegex = /__text_start = 0x([0-9a-f]+)/i;
+let symbolize = false;
+
+if (process.argv.length <= 2) {
+ console.error('Usage: node symbolize.js ');
+ process.exit(1);
+}
+
+const elfPath = process.argv[2];
+
+rl.on('line', (line) => {
+ if (textStart === 0) {
+ const match = textStartRegex.exec(line);
+ if (match) {
+ textStart = parseInt(match[1], 16);
+ }
+ }
+
+ if (line.includes("stack backtrace:")) {
+ symbolize = true;
+ }
+
+ if (symbolize) {
+ const match = /0x([0-9a-f]+) - \/.exec(line);
+ if (match) {
+ const addr = parseInt(match[1], 16);
+ const relative = addr - textStart;
+ // run addr2line on the address
+ const addr2line = childProcess.spawnSync('addr2line', ['-e', elfPath, '-j', '.text', '-f', '-C', '0x' + relative.toString(16)]);
+ if (addr2line.status === 0) {
+ const output = addr2line.stdout.toString();
+ const lines = output.split('\n');
+ const [func, file] = lines;
+ line = line.replace(match[0], `0x${addr.toString(16)} - ${func} (${file})`);
+ }
+ }
+ }
+
+ console.log(line);
+});
\ No newline at end of file
diff --git a/rustfmt.toml b/rustfmt.toml
index f645dcf..7811f89 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,4 +1,4 @@
-edition = "2018"
+edition = "2021"
max_width = 120
use_small_heuristics = "Max"
newline_style = "Unix"
diff --git a/src/framework/backend.rs b/src/framework/backend.rs
index cad83d5..b94d10d 100644
--- a/src/framework/backend.rs
+++ b/src/framework/backend.rs
@@ -107,15 +107,20 @@ pub fn init_backend(headless: bool, size_hint: (u16, u16)) -> GameResult *mut NWindow;
+
+ fn padInitializeWithMask(pad_state: *mut PadState, mask: u64);
+
+ fn padConfigureInput(max_players: u32, style_set: u32);
+
+ fn padUpdate(pad_state: *mut PadState);
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct HidAnalogStickState {
+ pub x: i32,
+ pub y: i32,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct PadState {
+ pub id_mask: u8,
+ pub active_id_mask: u8,
+ pub read_handheld: bool,
+ pub active_handheld: bool,
+ pub style_set: u32,
+ pub attributes: u32,
+ pub buttons_cur: u64,
+ pub buttons_old: u64,
+ pub sticks: [HidAnalogStickState; 2],
+ pub gc_triggers: [u32; 2],
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct PadRepeater {
+ pub button_mask: u64,
+ pub counter: i32,
+ pub delay: u16,
+ pub repeat: u16,
+ }
+
+ pub fn pad_configure(max_players: u32, style_set: u32) {
+ unsafe {
+ padConfigureInput(max_players, style_set);
+ }
+ }
+
+ impl PadState {
+ pub fn initialize(mask: u64) -> Self {
+ let mut state = Self {
+ id_mask: 0,
+ active_id_mask: 0,
+ read_handheld: false,
+ active_handheld: false,
+ style_set: 0,
+ attributes: 0,
+ buttons_cur: 0,
+ buttons_old: 0,
+ sticks: [HidAnalogStickState { x: 0, y: 0 }; 2],
+ gc_triggers: [0; 2],
+ };
+
+ unsafe {
+ padInitializeWithMask(&mut state, mask);
+ }
+
+ state
+ }
+
+ pub fn initialize_any() -> Self {
+ Self::initialize(0x1000100FF)
+ }
+
+ pub fn initialize_default() -> Self {
+ Self::initialize(HID_PAD_NO1 | HID_PAD_HANDHELD)
+ }
+
+ pub fn update(&mut self) {
+ unsafe {
+ padUpdate(self);
+ }
+ }
+
+ pub fn is_connected(&self) -> bool {
+ self.id_mask != 0
+ }
+
+ pub fn get_buttons_down(&self) -> u64 {
+ self.buttons_cur & !self.buttons_old
+ }
+
+ pub fn get_buttons_up(&self) -> u64 {
+ !self.buttons_cur & self.buttons_old
+ }
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct Service {
+ session: u32,
+ own_handle: u32,
+ object_id: u32,
+ pointer_buffer_size: u16,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct Event {
+ revent: u32,
+ wevent: u32,
+ auto_clear: bool,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone, PartialEq, Eq)]
+ pub enum AppletId {
+ None = 0,
+ Application = 1,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone, PartialEq, Eq)]
+ pub enum LibAppletMode {
+ AllForeground = 0,
+ Background = 1,
+ NoUi = 2,
+ BackgroundIndirect = 3,
+ AllForegroundInitiallyHidden = 4,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone, PartialEq, Eq)]
+ pub enum LibAppletExitReason {
+ Normal = 0,
+ Canceled = 1,
+ Abnormal = 2,
+ Unexpected = 10,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct WebCommonTLVStorage {
+ data: [u8; 0x2000],
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct AppletHolder {
+ pub s: Service,
+ pub StateChangedEvent: Event,
+ pub PopInteractiveOutDataEvent: Event,
+ pub mode: LibAppletMode,
+ pub layer_handle: u64,
+ pub creating_self: bool,
+ pub exitreason: LibAppletExitReason,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ pub struct WebCommonConfig {
+ arg: WebCommonTLVStorage,
+ applet_id: AppletId,
+ version: u32,
+ holder: AppletHolder,
+ }
+
+ extern "C" {
+ pub fn webPageCreate(config: *mut WebCommonConfig, url: *const std::ffi::c_char) -> u32;
+
+ pub fn webConfigSetWhitelist(config: *mut WebCommonConfig, whitelist: *const std::ffi::c_char) -> u32;
+
+ pub fn webConfigShow(config: *mut WebCommonConfig, out: *mut u32) -> u32;
+ }
+
+ impl WebCommonConfig {
+ pub fn new() -> WebCommonConfig {
+ unsafe { std::mem::zeroed() }
+ }
+ }
+
+ extern "C" {
+ pub fn romfsMountDataStorageFromProgram(program_id: u64, name: *const std::ffi::c_char) -> u32;
+
+ pub fn romfsMountFromCurrentProcess(name: *const std::ffi::c_char) -> u32;
+ }
+}
+
+pub struct HorizonBackend;
+
+impl HorizonBackend {
+ pub fn new() -> GameResult> {
+ Ok(Box::new(HorizonBackend))
+ }
+}
+
+impl Backend for HorizonBackend {
+ fn create_event_loop(&self, _ctx: &Context) -> GameResult> {
+ nx::pad_configure(8, nx::HID_PAD_STYLE_SET_STANDARD);
+
+ let mut gamepads = [
+ nx::PadState::initialize_default(),
+ nx::PadState::initialize(nx::HID_PAD_NO2),
+ nx::PadState::initialize(nx::HID_PAD_NO3),
+ nx::PadState::initialize(nx::HID_PAD_NO4),
+ nx::PadState::initialize(nx::HID_PAD_NO5),
+ nx::PadState::initialize(nx::HID_PAD_NO6),
+ nx::PadState::initialize(nx::HID_PAD_NO7),
+ nx::PadState::initialize(nx::HID_PAD_NO8),
+ ];
+
+ for pad in gamepads.iter_mut() {
+ pad.update();
+ }
+
+ Ok(Box::new(HorizonEventLoop { gamepads, active: [false; 8] }))
+ }
+}
+
+pub struct HorizonEventLoop {
+ gamepads: [nx::PadState; 8],
+ active: [bool; 8],
+}
+
+const GAMEPAD_KEYMAP: [Button; 16] = [
+ Button::South,
+ Button::East,
+ Button::North,
+ Button::West,
+ Button::LeftStick,
+ Button::RightStick,
+ Button::LeftShoulder,
+ Button::RightShoulder,
+ Button::LeftShoulder,
+ Button::RightShoulder,
+ Button::Back,
+ Button::Start,
+ Button::DPadLeft,
+ Button::DPadUp,
+ Button::DPadRight,
+ Button::DPadDown,
+];
+
+const fn align(size: u32, align: u32) -> u32 {
+ (size + align - 1) & !(align - 1)
+}
+
+impl HorizonEventLoop {
+ fn gamepad_update(&mut self, state: &SharedGameState, ctx: &mut Context) {
+ for (id, pad) in self.gamepads.iter_mut().enumerate() {
+ pad.update();
+
+ let connected = pad.is_connected();
+ if connected != self.active[id] {
+ if connected {
+ // connected
+ log::info!("Gamepad {} connected", id);
+
+ let axis_sensitivity = state.settings.get_gamepad_axis_sensitivity(id as u32);
+ ctx.gamepad_context.add_gamepad(HorizonGamepad::new(id as u32), axis_sensitivity);
+
+ ctx.gamepad_context.set_gamepad_type(id as u32, GamepadType::NintendoSwitchJoyConPair);
+ } else {
+ // disconnected
+ log::info!("Gamepad {} disconnected", id);
+
+ ctx.gamepad_context.remove_gamepad(id as u32);
+ }
+
+ self.active[id] = connected;
+ }
+ }
+
+ for (id, pad) in self.gamepads.iter().enumerate() {
+ if !pad.is_connected() {
+ continue;
+ }
+
+ let buttons_down = pad.get_buttons_down();
+ let buttons_up = pad.get_buttons_up();
+
+ for i in 0..GAMEPAD_KEYMAP.len() {
+ let button = GAMEPAD_KEYMAP[i];
+ let mask = 1 << i;
+
+ if buttons_down & mask != 0 {
+ ctx.gamepad_context.set_button(id as u32, button, true);
+ }
+
+ if buttons_up & mask != 0 {
+ ctx.gamepad_context.set_button(id as u32, button, false);
+ }
+ }
+
+ let analog_x = pad.sticks[0].x as f64 / 32768.0;
+ let analog_y = -pad.sticks[0].y as f64 / 32768.0;
+
+ ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftX, analog_x.clamp(0.0, 1.0));
+ ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftY, analog_y.clamp(0.0, 1.0));
+ ctx.gamepad_context.set_axis_value(id as u32, Axis::RightX, (-analog_x).clamp(0.0, 1.0));
+ ctx.gamepad_context.set_axis_value(id as u32, Axis::RightY, (-analog_y).clamp(0.0, 1.0));
+ }
+ }
+}
+
+impl BackendEventLoop for HorizonEventLoop {
+ fn run(&mut self, game: &mut Game, ctx: &mut Context) {
+ let state_ref = unsafe { &mut *game.state.get() };
+
+ let scale = 1.0;
+ ctx.screen_size = (854.0 * scale, 480.0 * scale);
+ state_ref.handle_resize(ctx).unwrap();
+
+ loop {
+ self.gamepad_update(state_ref, ctx);
+
+ game.update(ctx).unwrap();
+
+ if state_ref.shutdown {
+ log::info!("Shutting down...");
+ break;
+ }
+
+ if state_ref.next_scene.is_some() {
+ mem::swap(&mut game.scene, &mut state_ref.next_scene);
+ state_ref.next_scene = None;
+ game.scene.as_mut().unwrap().init(state_ref, ctx).unwrap();
+ game.loops = 0;
+ state_ref.frame_time = 0.0;
+ }
+
+ game.draw(ctx).unwrap();
+ }
+ }
+
+ fn new_renderer(&self, ctx: *mut Context) -> GameResult> {
+ let mut imgui = imgui::Context::create();
+ let ctx = unsafe { &mut *ctx };
+ imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1];
+ imgui.fonts().build_alpha8_texture();
+
+ let device = DeviceMaker::new().create();
+
+ Deko3DRenderer::new(device, imgui)
+ }
+}
+
+pub struct HorizonGamepad {
+ pub id: u32,
+}
+
+impl HorizonGamepad {
+ fn new(id: u32) -> Box {
+ Box::new(HorizonGamepad { id })
+ }
+}
+
+impl BackendGamepad for HorizonGamepad {
+ fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult {
+ Ok(())
+ }
+
+ fn instance_id(&self) -> u32 {
+ self.id
+ }
+}
+
+lazy_static! {
+ static ref VERTEX_ATTRIB_STATE: [VtxAttribState; 3] = [
+ *VtxAttribState::new()
+ .set_offset(field_offset::(|v| &v.position) as u16)
+ .set_size(VtxAttribSize::_2x32)
+ .set_type(VtxAttribType::Float),
+ *VtxAttribState::new()
+ .set_offset(field_offset::(|v| &v.uv) as u16)
+ .set_size(VtxAttribSize::_2x32)
+ .set_type(VtxAttribType::Float),
+ *VtxAttribState::new()
+ .set_offset(field_offset::(|v| &v.color) as u16)
+ .set_size(VtxAttribSize::_4x8)
+ .set_type(VtxAttribType::Unorm),
+ ];
+ static ref VERTEX_BUFFER_STATE: [VtxBufferState; 1] =
+ [VtxBufferState { stride: mem::size_of::() as u32, divisor: 0 }];
+}
+
+struct Deko3DVertexBuffer {
+ buffer: deko3d::MemBlock,
+ capacity: usize, // those two are in bytes
+ allocated: usize,
+}
+
+impl Deko3DVertexBuffer {
+ pub fn new(device: &deko3d::Device) -> GameResult {
+ let capacity = 2 * 16 * DK_MEMBLOCK_ALIGNMENT;
+
+ let buffer = MemBlockMaker::new(device, capacity as u32)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ Ok(Deko3DVertexBuffer { buffer, capacity: capacity as usize, allocated: 0 })
+ }
+
+ pub fn transfer(&mut self, vertices: &[VertexData], device: &deko3d::Device) -> GameResult<()> {
+ let allocated = vertices.len() * mem::size_of::();
+ let size = allocated.max(16 * DK_MEMBLOCK_ALIGNMENT as usize);
+ let size = align(2 * size as u32, DK_MEMBLOCK_ALIGNMENT) as usize;
+
+ if size > u32::MAX as usize {
+ return Err(GameError::ResourceLoadError("Vertex buffer too large".to_string()));
+ }
+
+ if size > self.capacity {
+ self.buffer = MemBlockMaker::new(device, size as u32)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+ self.capacity = size;
+ }
+
+ unsafe {
+ (self.buffer.get_cpu_addr() as *mut VertexData).copy_from_nonoverlapping(vertices.as_ptr(), vertices.len());
+ }
+
+ self.allocated = allocated;
+
+ Ok(())
+ }
+}
+
+struct Deko3DShader {
+ ubo_mem_block: deko3d::MemBlock,
+ code_mem_block: deko3d::MemBlock,
+ vtx_shader: deko3d::Shader,
+ frag_shader: deko3d::Shader,
+ data: UBO,
+}
+
+impl Deko3DShader {
+ pub fn new(
+ device: &deko3d::Device,
+ vertex_shader_binary: &[u8],
+ fragment_shader_binary: &[u8],
+ ) -> GameResult {
+ let ubo_size = mem::size_of::();
+ let ubo_size = align(ubo_size as u32, DK_MEMBLOCK_ALIGNMENT) as usize;
+
+ let ubo_mem_block = MemBlockMaker::new(device, ubo_size as u32)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ let vtx_binary_len = align(vertex_shader_binary.len() as u32, DK_SHADER_CODE_ALIGNMENT) as usize;
+ let frag_binary_len = align(fragment_shader_binary.len() as u32, DK_SHADER_CODE_ALIGNMENT) as usize;
+
+ let code_size = vtx_binary_len + frag_binary_len + DK_SHADER_CODE_UNUSABLE_SIZE as usize;
+ let code_size = align(code_size as u32, DK_MEMBLOCK_ALIGNMENT) as usize;
+ let code_mem_block = MemBlockMaker::new(&device, code_size as u32)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached | MemBlockFlags::Code)
+ .create();
+
+ unsafe {
+ let buf = code_mem_block.get_cpu_addr() as *mut u8;
+
+ let vtx_code = buf;
+ let frag_code = buf.add(vtx_binary_len);
+
+ vtx_code.copy_from_nonoverlapping(vertex_shader_binary.as_ptr(), vertex_shader_binary.len());
+ frag_code.copy_from_nonoverlapping(fragment_shader_binary.as_ptr(), fragment_shader_binary.len());
+ }
+
+ let mut vtx_shader = Shader::new();
+ let mut frag_shader = Shader::new();
+
+ ShaderMaker::new(&code_mem_block, 0).initialize(&mut vtx_shader);
+ ShaderMaker::new(&code_mem_block, vtx_binary_len as u32).initialize(&mut frag_shader);
+
+ Ok(Deko3DShader { ubo_mem_block, code_mem_block, vtx_shader, frag_shader, data: Default::default() })
+ }
+
+ pub fn update_uniforms(&mut self, data: UBO) {
+ self.data = data;
+ }
+
+ pub fn bind(&self, cmd_buf: &deko3d::CmdBuf) {
+ cmd_buf.bind_shaders(StageFlag::GraphicsMask, &[&self.vtx_shader, &self.frag_shader]);
+ cmd_buf.bind_uniform_buffer(Stage::Vertex, 0, self.ubo_mem_block.get_gpu_addr(), self.ubo_mem_block.get_size());
+ cmd_buf.push_constants(
+ self.ubo_mem_block.get_gpu_addr(),
+ self.ubo_mem_block.get_size(),
+ 0,
+ mem::size_of::() as u32,
+ &self.data as *const _ as *const std::ffi::c_void,
+ );
+ cmd_buf.bind_vtx_attrib_state(&*VERTEX_ATTRIB_STATE);
+ cmd_buf.bind_vtx_buffer_state(&*VERTEX_BUFFER_STATE);
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct VertUBO {
+ proj_mtx: [[f32; 4]; 4],
+}
+
+impl Default for VertUBO {
+ fn default() -> Self {
+ VertUBO { proj_mtx: [[0.0; 4]; 4] }
+ }
+}
+
+struct Deko3DTextureDesc {
+ image: deko3d::ImageDescriptor,
+ sampler: deko3d::SamplerDescriptor,
+}
+
+pub struct Deko3DTexture {
+ dimensions: (u16, u16),
+ desc_memory: deko3d::MemBlock,
+ memory: deko3d::MemBlock,
+ image: deko3d::Image,
+ vertices: Vec,
+ vbo: Deko3DVertexBuffer,
+ renderer: *mut Deko3DRenderer,
+}
+
+impl Deko3DTexture {
+ unsafe fn renderer<'a, 'b: 'a>(&'a self) -> &'b mut Deko3DRenderer {
+ unsafe { &mut *self.renderer }
+ }
+}
+
+impl BackendTexture for Deko3DTexture {
+ fn dimensions(&self) -> (u16, u16) {
+ self.dimensions
+ }
+
+ fn add(&mut self, command: SpriteBatchCommand) {
+ let (width, height) = self.dimensions;
+ let (tex_scale_x, tex_scale_y) = (1.0 / width as f32, 1.0 / height as f32);
+
+ match command {
+ SpriteBatchCommand::DrawRect(src, dest) => {
+ let vertices = [
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.left, dest.top),
+ uv: (src.left * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.bottom),
+ uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ ];
+ self.vertices.extend_from_slice(&vertices);
+ }
+ SpriteBatchCommand::DrawRectFlip(mut src, dest, flip_x, flip_y) => {
+ if flip_x {
+ std::mem::swap(&mut src.left, &mut src.right);
+ }
+
+ if flip_y {
+ std::mem::swap(&mut src.top, &mut src.bottom);
+ }
+
+ let vertices = [
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.left, dest.top),
+ uv: (src.left * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ VertexData {
+ position: (dest.right, dest.bottom),
+ uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
+ color: (255, 255, 255, 255),
+ },
+ ];
+ self.vertices.extend_from_slice(&vertices);
+ }
+ SpriteBatchCommand::DrawRectTinted(src, dest, color) => {
+ let color = color.to_rgba();
+ let vertices = [
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.left, dest.top),
+ uv: (src.left * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.bottom),
+ uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ ];
+ self.vertices.extend_from_slice(&vertices);
+ }
+ SpriteBatchCommand::DrawRectFlipTinted(mut src, dest, flip_x, flip_y, color) => {
+ if flip_x {
+ std::mem::swap(&mut src.left, &mut src.right);
+ }
+
+ if flip_y {
+ std::mem::swap(&mut src.top, &mut src.bottom);
+ }
+
+ let color = color.to_rgba();
+
+ let vertices = [
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.left, dest.top),
+ uv: (src.left * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.left, dest.bottom),
+ uv: (src.left * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.top),
+ uv: (src.right * tex_scale_x, src.top * tex_scale_y),
+ color,
+ },
+ VertexData {
+ position: (dest.right, dest.bottom),
+ uv: (src.right * tex_scale_x, src.bottom * tex_scale_y),
+ color,
+ },
+ ];
+ self.vertices.extend_from_slice(&vertices);
+ }
+ }
+ }
+
+ fn clear(&mut self) {
+ self.vertices.clear();
+ }
+
+ fn draw(&mut self) -> GameResult<()> {
+ let renderer = unsafe { self.renderer() };
+
+ self.vbo.transfer(&self.vertices, &renderer.device)?;
+
+ let cmdbuf = &renderer.cmdbuf[renderer.slot as usize];
+
+ cmdbuf.bind_vtx_buffer(0, self.vbo.buffer.get_gpu_addr(), self.vbo.buffer.get_size());
+
+ let img_offset = field_offset::(|d| &d.image);
+ let sampler_offset = field_offset::(|d| &d.sampler);
+
+ let desc_gpu = self.desc_memory.get_gpu_addr();
+ cmdbuf.bind_sampler_descriptor_set(desc_gpu + sampler_offset as u64, 1);
+ cmdbuf.bind_image_descriptor_set(desc_gpu + img_offset as u64, 1);
+ cmdbuf.bind_textures(Stage::Fragment, 0, &[make_texture_handle(0, 0)]);
+
+ renderer.texture_shader.update_uniforms(VertUBO { proj_mtx: renderer.curr_mtx });
+ renderer.texture_shader.bind(cmdbuf);
+
+ cmdbuf.draw(Primitive::Triangles, self.vertices.len() as u32, 1, 0, 0);
+ cmdbuf.barrier(Barrier::Fragments, InvalidateFlags::None);
+
+ renderer.queue.submit_commands(cmdbuf.finish_list());
+ renderer.queue.wait_idle();
+ cmdbuf.clear();
+
+ Ok(())
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+const BUFFER_COUNT: u32 = 2; // double buffering
+
+pub struct Deko3DRenderer {
+ device: deko3d::Device,
+ queue: deko3d::Queue,
+
+ fb_mem_block: deko3d::MemBlock,
+ framebuffers: [deko3d::Image; BUFFER_COUNT as usize],
+ swapchain: deko3d::Swapchain,
+
+ depth_mem_block: deko3d::MemBlock,
+ depthbuffer: deko3d::Image,
+
+ cmdbuf_mem_block: [deko3d::MemBlock; BUFFER_COUNT as usize],
+ cmdbuf: [deko3d::CmdBuf; BUFFER_COUNT as usize],
+
+ cmdbuf_ctrl_mem_block: deko3d::MemBlock,
+ cmdbuf_ctrl: deko3d::CmdBuf,
+ vbo: Deko3DVertexBuffer,
+
+ texture_shader: Deko3DShader,
+ color_shader: Deko3DShader,
+
+ curr_mtx: [[f32; 4]; 4],
+ width: u32,
+ height: u32,
+ fb_width: u32,
+ fb_height: u32,
+ slot: i32,
+
+ imgui: UnsafeCell,
+}
+
+impl Deko3DRenderer {
+ fn new(device: deko3d::Device, imgui: imgui::Context) -> GameResult> {
+ let fb_width = 854;
+ let fb_height = 480;
+
+ let queue = QueueMaker::new(&device).set_flags(QueueFlags::Graphics).create();
+
+ let mut depth_layout = ImageLayout::new();
+ ImageLayoutMaker::new(&device)
+ .set_flags(ImageFlags::UsageRender | ImageFlags::HwCompression)
+ .set_format(ImageFormat::Z24S8)
+ .set_dimensions(fb_width, fb_height, 0)
+ .initialize(&mut depth_layout);
+
+ let depth_size =
+ align(align(depth_layout.get_size() as u32, depth_layout.get_alignment()), DK_MEMBLOCK_ALIGNMENT);
+
+ let depth_mem_block =
+ MemBlockMaker::new(&device, depth_size).set_flags(MemBlockFlags::Image | MemBlockFlags::GpuCached).create();
+
+ let mut depthbuffer = Image::new();
+ depthbuffer.initialize(&depth_layout, &depth_mem_block, 0);
+
+ let mut fb_layout: ImageLayout = ImageLayout::new();
+ ImageLayoutMaker::new(&device)
+ .set_flags(ImageFlags::UsageRender | ImageFlags::UsagePresent | ImageFlags::HwCompression)
+ .set_format(ImageFormat::RGBA8Unorm)
+ .set_dimensions(fb_width, fb_height, 0)
+ .initialize(&mut fb_layout);
+
+ let mut framebuffers: [Image; BUFFER_COUNT as usize] = [Image::new(), Image::new()];
+ let fb_size = align(align(fb_layout.get_size() as u32, fb_layout.get_alignment()), DK_MEMBLOCK_ALIGNMENT);
+
+ let fb_mem_block = MemBlockMaker::new(&device, framebuffers.len() as u32 * fb_size)
+ .set_flags(MemBlockFlags::Image | MemBlockFlags::GpuCached)
+ .create();
+
+ for (i, fb) in framebuffers.iter_mut().enumerate() {
+ fb.initialize(&fb_layout, &fb_mem_block, i as u32 * fb_size);
+ }
+
+ let native_window = unsafe { nx::nwindowGetDefault() };
+ let swapchain = SwapchainMaker::new(&device, native_window, &framebuffers).create();
+
+ let cmd_mem_size = 16 * 1024;
+ let cmdbuf_size = align(cmd_mem_size, DK_MEMBLOCK_ALIGNMENT);
+ let cmdbuf_mem_block: [MemBlock; BUFFER_COUNT as usize] = [
+ MemBlockMaker::new(&device, cmdbuf_size)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create(),
+ MemBlockMaker::new(&device, cmdbuf_size)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create(),
+ ];
+
+ let cmdbuf: [CmdBuf; BUFFER_COUNT as usize] =
+ [CmdBufMaker::new(&device).create(), CmdBufMaker::new(&device).create()];
+
+ for (i, cmdbuf) in cmdbuf.iter().enumerate() {
+ cmdbuf.add_memory(&cmdbuf_mem_block[i], 0, cmd_mem_size);
+ }
+
+ let cmdbuf_ctrl_mem_block = MemBlockMaker::new(&device, cmdbuf_size)
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ let cmdbuf_ctrl = CmdBufMaker::new(&device).create();
+ cmdbuf_ctrl.add_memory(&cmdbuf_ctrl_mem_block, 0, cmd_mem_size);
+
+ let vbo = Deko3DVertexBuffer::new(&device)?;
+
+ let texture_shader = Deko3DShader::::new(
+ &device,
+ include_bytes!("shaders/deko3d/vertex_basic.dksh"),
+ include_bytes!("shaders/deko3d/fragment_textured.dksh"),
+ )?;
+ let color_shader = Deko3DShader::::new(
+ &device,
+ include_bytes!("shaders/deko3d/vertex_basic.dksh"),
+ include_bytes!("shaders/deko3d/fragment_color.dksh"),
+ )?;
+
+ Ok(Box::new(Deko3DRenderer {
+ device,
+ queue,
+ fb_mem_block,
+ framebuffers,
+ swapchain,
+ depth_mem_block,
+ depthbuffer,
+ cmdbuf_mem_block,
+ cmdbuf,
+ cmdbuf_ctrl_mem_block,
+ cmdbuf_ctrl,
+ vbo,
+ texture_shader,
+ color_shader,
+ curr_mtx: [[0.0; 4]; 4],
+ width: fb_width,
+ height: fb_height,
+ fb_width,
+ fb_height,
+ slot: 0,
+ imgui: UnsafeCell::new(imgui),
+ }))
+ }
+}
+
+impl BackendRenderer for Deko3DRenderer {
+ fn renderer_name(&self) -> String {
+ "deko3d".to_owned()
+ }
+
+ fn clear(&mut self, color: Color) {
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ cmdbuf.clear_color_float(0, ColorMask::RGBA, color.r, color.g, color.b, color.a);
+ }
+
+ fn present(&mut self) -> GameResult {
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ cmdbuf.barrier(Barrier::Fragments, InvalidateFlags::None);
+ cmdbuf.discard_depth_stencil();
+
+ self.queue.submit_commands(cmdbuf.finish_list());
+ self.queue.wait_idle();
+ self.queue.present_image(&self.swapchain, self.slot);
+
+ Ok(())
+ }
+
+ fn prepare_draw(&mut self, width: f32, height: f32) -> GameResult {
+ self.slot = self.queue.acquire_image(&self.swapchain);
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+ cmdbuf.clear();
+
+ let image_view = ImageView::new(&self.framebuffers[self.slot as usize]);
+ let depth_view = ImageView::new(&self.depthbuffer);
+ cmdbuf.bind_render_targets(&[&image_view], Some(&depth_view));
+ cmdbuf.set_viewports(0, &[Viewport { x: 0.0, y: 0.0, width, height, near: -1000.0, far: 1000.0 }]);
+ cmdbuf.set_scissors(0, &[Scissor { x: 0, y: 0, width: width as u32, height: height as u32 }]);
+ cmdbuf.clear_color_float(0, ColorMask::RGBA, 0.0, 0.0, 0.0, 1.0);
+ cmdbuf.clear_depth_stencil(true, 1.0, 0xff, 0);
+ cmdbuf.bind_rasterizer_state(&RasterizerState::new().set_cull_mode(Face::None));
+ cmdbuf.bind_color_state(&ColorState::new().set_blend_enable(0, true));
+ cmdbuf.bind_color_write_state(&ColorWriteState::new());
+ cmdbuf.bind_depth_stencil_state(&DepthStencilState::new().set_depth_test_enable(false));
+ cmdbuf.bind_blend_states(0, &[BlendState::new()]);
+
+ self.curr_mtx = [
+ [2.0 / width, 0.0, 0.0, 0.0],
+ [0.0, 2.0 / -height, 0.0, 0.0],
+ [0.0, 0.0, -1.0, 0.0],
+ [-1.0, 1.0, 0.0, 1.0],
+ ];
+
+ Ok(())
+ }
+
+ fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult> {
+ let img_total = width as u32 * height as u32 * 4;
+ let desc_memory = MemBlockMaker::new(
+ &self.device,
+ align(std::mem::size_of::() as u32, DK_MEMBLOCK_ALIGNMENT),
+ )
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ let mut desc_cpu = unsafe { &mut *(desc_memory.get_cpu_addr() as *mut Deko3DTextureDesc) };
+ desc_cpu.sampler = SamplerDescriptor::new();
+ desc_cpu.image = ImageDescriptor::new();
+
+ let mut layout = ImageLayout::new();
+ ImageLayoutMaker::new(&self.device)
+ .set_flags(ImageFlags::UsageRender | ImageFlags::BlockLinear)
+ .set_format(ImageFormat::RGBA8Unorm)
+ .set_dimensions(width as u32, height as u32, 0)
+ .initialize(&mut layout);
+
+ let memory = MemBlockMaker::new(
+ &self.device,
+ align(layout.get_size() as u32, DK_MEMBLOCK_ALIGNMENT.max(layout.get_alignment())),
+ )
+ .set_flags(MemBlockFlags::Image | MemBlockFlags::GpuCached)
+ .create();
+
+ let mut image = Image::new();
+ image.initialize(&layout, &memory, 0);
+
+ desc_cpu.image.initialize(&ImageView::new(&image), false, false);
+ desc_cpu.sampler.initialize(
+ &Sampler::new().set_filter(Filter::Nearest, Filter::Nearest, MipFilter::None).set_wrap_mode(
+ WrapMode::ClampToEdge,
+ WrapMode::ClampToEdge,
+ WrapMode::ClampToEdge,
+ ),
+ );
+
+ let vbo = Deko3DVertexBuffer::new(&self.device)?;
+
+ Ok(Box::new(Deko3DTexture {
+ dimensions: (width, height),
+ desc_memory,
+ memory,
+ image,
+ vertices: Vec::new(),
+ vbo,
+ renderer: self,
+ }))
+ }
+
+ fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult> {
+ let img_total = width as u32 * height as u32 * 4;
+ let desc_memory = MemBlockMaker::new(
+ &self.device,
+ align(std::mem::size_of::() as u32, DK_MEMBLOCK_ALIGNMENT),
+ )
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ let scratch_memory = MemBlockMaker::new(&self.device, align(img_total, DK_MEMBLOCK_ALIGNMENT))
+ .set_flags(MemBlockFlags::CpuUncached | MemBlockFlags::GpuCached)
+ .create();
+
+ unsafe {
+ let len = data.len().min(img_total as usize);
+ (scratch_memory.get_cpu_addr() as *mut u8).copy_from_nonoverlapping(data.as_ptr(), len);
+ }
+
+ let mut desc_cpu = unsafe { &mut *(desc_memory.get_cpu_addr() as *mut Deko3DTextureDesc) };
+ desc_cpu.sampler = SamplerDescriptor::new();
+ desc_cpu.image = ImageDescriptor::new();
+
+ let desc_gpu = desc_memory.get_gpu_addr();
+
+ let img_offset = field_offset::(|d| &d.image);
+ let sampler_offset = field_offset::(|d| &d.sampler);
+
+ let mut layout = ImageLayout::new();
+ ImageLayoutMaker::new(&self.device)
+ .set_flags(ImageFlags::UsageRender | ImageFlags::BlockLinear)
+ .set_format(ImageFormat::RGBA8Unorm)
+ .set_dimensions(width as u32, height as u32, 0)
+ .initialize(&mut layout);
+
+ let memory = MemBlockMaker::new(
+ &self.device,
+ align(layout.get_size() as u32, DK_MEMBLOCK_ALIGNMENT.max(layout.get_alignment())),
+ )
+ .set_flags(MemBlockFlags::Image | MemBlockFlags::GpuCached)
+ .create();
+
+ let mut image = Image::new();
+ image.initialize(&layout, &memory, 0);
+
+ desc_cpu.image.initialize(&ImageView::new(&image), false, false);
+ desc_cpu.sampler.initialize(
+ &Sampler::new().set_filter(Filter::Nearest, Filter::Nearest, MipFilter::None).set_wrap_mode(
+ WrapMode::ClampToEdge,
+ WrapMode::ClampToEdge,
+ WrapMode::ClampToEdge,
+ ),
+ );
+
+ let cmdbuf = &self.cmdbuf_ctrl;
+ // let cmdbuf = &self.cmdbuf[self.slot as usize];
+ cmdbuf.bind_sampler_descriptor_set(desc_gpu + sampler_offset as u64, 1);
+ cmdbuf.bind_image_descriptor_set(desc_gpu + img_offset as u64, 1);
+
+ cmdbuf.copy_buffer_to_image(
+ &CopyBuf { addr: scratch_memory.get_gpu_addr(), rowLength: 0, imageHeight: 0 },
+ &ImageView::new(&image),
+ &ImageRect { x: 0, y: 0, z: 0, width: width as u32, height: height as u32, depth: 1 },
+ 0,
+ );
+ cmdbuf.barrier(Barrier::None, InvalidateFlags::Descriptors);
+ self.queue.submit_commands(cmdbuf.finish_list());
+ self.queue.wait_idle();
+ cmdbuf.clear();
+
+ let vbo = Deko3DVertexBuffer::new(&self.device)?;
+
+ Ok(Box::new(Deko3DTexture {
+ dimensions: (width, height),
+ desc_memory,
+ memory,
+ image,
+ vertices: Vec::new(),
+ vbo,
+ renderer: self,
+ }))
+ }
+
+ fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult {
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ match blend {
+ BlendMode::None => {
+ cmdbuf.bind_blend_states(0, &[BlendState::new()]);
+ }
+ BlendMode::Add => {
+ cmdbuf.bind_blend_states(
+ 0,
+ &[*BlendState::new()
+ .set_src_color_blend_factor(BlendFactor::One)
+ .set_dst_color_blend_factor(BlendFactor::One)
+ .set_src_alpha_blend_factor(BlendFactor::One)
+ .set_dst_alpha_blend_factor(BlendFactor::One)],
+ );
+ }
+ BlendMode::Alpha => {
+ cmdbuf.bind_blend_states(0, &[BlendState::new()]);
+ }
+ BlendMode::Multiply => {
+ cmdbuf.bind_blend_states(
+ 0,
+ &[*BlendState::new()
+ .set_src_color_blend_factor(BlendFactor::Zero)
+ .set_dst_color_blend_factor(BlendFactor::SrcColor)
+ .set_src_alpha_blend_factor(BlendFactor::Zero)
+ .set_dst_alpha_blend_factor(BlendFactor::SrcAlpha)],
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ fn set_render_target(&mut self, texture: Option<&Box>) -> GameResult {
+ if let Some(texture) = texture {
+ let deko_texture = texture
+ .as_any()
+ .downcast_ref::()
+ .ok_or_else(|| GameError::RenderError("This texture was not created by deko3d backend.".to_string()))?;
+
+ let width = deko_texture.dimensions.0 as f32;
+ let height = deko_texture.dimensions.1 as f32;
+
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+ let image_view = ImageView::new(&deko_texture.image);
+ cmdbuf.bind_render_targets(&[&image_view], None);
+ cmdbuf.set_viewports(0, &[Viewport { x: 0.0, y: 0.0, width, height, near: -1000.0, far: 1000.0 }]);
+ cmdbuf.set_scissors(0, &[Scissor { x: 0, y: 0, width: width as u32, height: height as u32 }]);
+
+ self.width = width as u32;
+ self.height = height as u32;
+ self.curr_mtx = [
+ [2.0 / width, 0.0, 0.0, 0.0],
+ [0.0, 2.0 / -height, 0.0, 0.0],
+ [0.0, 0.0, -1.0, 0.0],
+ [-1.0, 1.0, 0.0, 1.0],
+ ];
+ } else {
+ let width = self.fb_width as f32;
+ let height = self.fb_height as f32;
+
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+ let image_view = ImageView::new(&self.framebuffers[self.slot as usize]);
+ let depth_view = ImageView::new(&self.depthbuffer);
+ cmdbuf.bind_render_targets(&[&image_view], Some(&depth_view));
+ cmdbuf.set_viewports(0, &[Viewport { x: 0.0, y: 0.0, width, height, near: -1000.0, far: 1000.0 }]);
+ cmdbuf.set_scissors(0, &[Scissor { x: 0, y: 0, width: width as u32, height: height as u32 }]);
+
+ self.width = self.fb_width;
+ self.height = self.fb_height;
+ self.curr_mtx = [
+ [2.0 / width, 0.0, 0.0, 0.0],
+ [0.0, 2.0 / -height, 0.0, 0.0],
+ [0.0, 0.0, -1.0, 0.0],
+ [-1.0, 1.0, 0.0, 1.0],
+ ];
+ }
+ Ok(())
+ }
+
+ fn draw_rect(&mut self, rect: Rect, color: Color) -> GameResult {
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ let color = color.to_rgba();
+ let uv = (0.0, 0.0);
+
+ let vertices = [
+ VertexData { position: (rect.left as _, rect.bottom as _), uv, color },
+ VertexData { position: (rect.left as _, rect.top as _), uv, color },
+ VertexData { position: (rect.right as _, rect.top as _), uv, color },
+ VertexData { position: (rect.left as _, rect.bottom as _), uv, color },
+ VertexData { position: (rect.right as _, rect.top as _), uv, color },
+ VertexData { position: (rect.right as _, rect.bottom as _), uv, color },
+ ];
+
+ self.vbo.transfer(&vertices, &self.device);
+ cmdbuf.bind_vtx_buffer(0, self.vbo.buffer.get_gpu_addr(), self.vbo.buffer.get_size());
+
+ self.color_shader.update_uniforms(VertUBO { proj_mtx: self.curr_mtx });
+ self.color_shader.bind(cmdbuf);
+
+ cmdbuf.draw(Primitive::Triangles, vertices.len() as u32, 1, 0, 0);
+
+ cmdbuf.barrier(Barrier::Fragments, InvalidateFlags::None);
+ self.queue.submit_commands(cmdbuf.finish_list());
+ self.queue.wait_idle();
+ cmdbuf.clear();
+
+ Ok(())
+ }
+
+ fn draw_outline_rect(&mut self, _rect: Rect, _line_width: usize, _color: Color) -> GameResult {
+ Ok(())
+ }
+
+ fn set_clip_rect(&mut self, rect: Option) -> GameResult {
+ let width = self.width;
+ let height = self.height;
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ if let Some(rect) = rect {
+ let x = rect.left.max(0);
+ let y = rect.top.max(0);
+ let width = (rect.right - x).min(width as isize);
+ let height = (rect.bottom - y).min(height as isize);
+
+ let (x, y, width, height) = (x as u32, y as u32, width as u32, height as u32);
+
+ cmdbuf.set_scissors(0, &[Scissor { x, y, width, height }]);
+ } else {
+ cmdbuf.set_scissors(0, &[Scissor { x: 0, y: 0, width, height }]);
+ }
+ Ok(())
+ }
+
+ fn imgui(&self) -> GameResult<&mut imgui::Context> {
+ unsafe { Ok(&mut *self.imgui.get()) }
+ }
+
+ fn imgui_texture_id(&self, _texture: &Box) -> GameResult {
+ Ok(TextureId::from(0))
+ }
+
+ fn prepare_imgui(&mut self, _ui: &Ui) -> GameResult {
+ Ok(())
+ }
+
+ fn render_imgui(&mut self, _draw_data: &DrawData) -> GameResult {
+ Ok(())
+ }
+
+ fn supports_vertex_draw(&self) -> bool {
+ true
+ }
+
+ fn draw_triangle_list(
+ &mut self,
+ vertices: &[VertexData],
+ texture: Option<&Box>,
+ shader: BackendShader,
+ ) -> GameResult<()> {
+ let cmdbuf = &self.cmdbuf[self.slot as usize];
+
+ self.vbo.transfer(vertices, &self.device);
+ cmdbuf.bind_vtx_buffer(0, self.vbo.buffer.get_gpu_addr(), self.vbo.buffer.get_size());
+
+ match shader {
+ BackendShader::Fill | BackendShader::WaterFill(_, _, _) => {
+ self.color_shader.update_uniforms(VertUBO { proj_mtx: self.curr_mtx });
+ self.color_shader.bind(cmdbuf);
+ }
+ BackendShader::Texture => {
+ self.texture_shader.update_uniforms(VertUBO { proj_mtx: self.curr_mtx });
+ self.texture_shader.bind(cmdbuf);
+ }
+ }
+
+ cmdbuf.draw(Primitive::Triangles, vertices.len() as u32, 1, 0, 0);
+
+ cmdbuf.barrier(Barrier::Fragments, InvalidateFlags::None);
+ self.queue.submit_commands(cmdbuf.finish_list());
+ self.queue.wait_idle();
+ cmdbuf.clear();
+
+ Ok(())
+ }
+}
+
+pub fn web_open(url: &str) -> std::io::Result<()> {
+ use std::io::{Error, ErrorKind};
+
+ let mut config = nx::WebCommonConfig::new();
+ unsafe {
+ let curl = std::ffi::CString::new(url).unwrap();
+ let ret = nx::webPageCreate(&mut config, curl.as_ptr());
+ if ret != 0 {
+ return Err(Error::new(ErrorKind::Other, "webPageCreate failed"));
+ }
+
+ let whitelist = std::ffi::CString::new("^http*").unwrap();
+ let ret = nx::webConfigSetWhitelist(&mut config, whitelist.as_ptr());
+ if ret != 0 {
+ return Err(Error::new(ErrorKind::Other, "webConfigSetWhitelist failed"));
+ }
+
+ let ret = nx::webConfigShow(&mut config, std::ptr::null_mut());
+ if ret != 0 {
+ return Err(Error::new(ErrorKind::Other, "webConfigShow failed"));
+ }
+ }
+
+ Ok(())
+}
+
+pub fn mount_romfs() -> bool {
+ // let title_ids = [
+ // (0x01000D9007C28000u64, "Cave Story+ (Japan)"),
+ // (0x0100B7D0022EE000u64, "Cave Story+ (US)"),
+ // (0x0100A55003B5C000u64, "Cave Story+ (EU)"),
+ // ];
+ // // romfsMountDataStorageFromProgram
+ let romfs_partition = std::ffi::CString::new("romfs").unwrap();
+ //
+ // for &(tid, name) in title_ids.iter() {
+ // log::info!("Trying to mount RomFS for {} [{:016X}]", name, tid);
+ // let ret = unsafe { nx::romfsMountDataStorageFromProgram(tid, romfs_partition.as_ptr()) };
+ // log::info!("romfsMountDataStorageFromProgram returned {:#04x}", ret);
+ // if ret == 0 {
+ // log::info!("RomFS mounted for {} [{:016X}]", name, tid);
+ // return true;
+ // }
+ // }
+ let ret = unsafe { nx::romfsMountFromCurrentProcess(romfs_partition.as_ptr()) };
+ log::info!("romfsMountFromCurrentProcess returned {:#04x}", ret);
+ if ret == 0 {
+ return true;
+ }
+
+ false
+}
diff --git a/src/framework/backend_null.rs b/src/framework/backend_null.rs
index d33e71a..4b41b6b 100644
--- a/src/framework/backend_null.rs
+++ b/src/framework/backend_null.rs
@@ -27,23 +27,10 @@ impl Backend for NullBackend {
}
}
-#[cfg(target_os = "horizon")]
-#[repr(C)]
-pub struct PrintConsole {}
-
-#[cfg(target_os = "horizon")]
-extern "C" { fn consoleUpdate(unk: *mut PrintConsole); }
-
pub struct NullEventLoop;
impl BackendEventLoop for NullEventLoop {
fn run(&mut self, game: &mut Game, ctx: &mut Context) {
- println!("BackendEventLoop::run");
- #[cfg(target_os = "horizon")]
- unsafe {
- consoleUpdate(std::ptr::null_mut());
- }
-
let state_ref = unsafe { &mut *game.state.get() };
ctx.screen_size = (640.0, 480.0);
@@ -67,11 +54,6 @@ impl BackendEventLoop for NullEventLoop {
std::thread::sleep(std::time::Duration::from_millis(10));
game.draw(ctx).unwrap();
-
- #[cfg(target_os = "horizon")]
- unsafe {
- consoleUpdate(std::ptr::null_mut());
- }
}
}
diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs
index 1d122c2..d470dac 100644
--- a/src/framework/backend_sdl2.rs
+++ b/src/framework/backend_sdl2.rs
@@ -7,10 +7,9 @@ use std::ptr::{null, null_mut};
use std::rc::Rc;
use std::time::{Duration, Instant};
-use imgui::{ConfigFlags, DrawCmd, DrawData, DrawIdx, DrawVert, Key, MouseCursor, TextureId, Ui};
use imgui::internal::RawWrapper;
use imgui::sys::{ImGuiKey_Backspace, ImGuiKey_Delete, ImGuiKey_Enter};
-use sdl2::{controller, EventPump, GameControllerSubsystem, keyboard, pixels, Sdl, VideoSubsystem};
+use imgui::{ConfigFlags, DrawCmd, DrawData, DrawIdx, DrawVert, Key, MouseCursor, TextureId, Ui};
use sdl2::controller::GameController;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Scancode;
@@ -20,9 +19,13 @@ use sdl2::render::{Texture, TextureCreator, TextureQuery, WindowCanvas};
use sdl2::video::GLProfile;
use sdl2::video::Window;
use sdl2::video::WindowContext;
+use sdl2::{controller, keyboard, pixels, EventPump, GameControllerSubsystem, Sdl, VideoSubsystem};
use crate::common::{Color, Rect};
-use crate::framework::backend::{Backend, BackendEventLoop, BackendGamepad, BackendRenderer, BackendShader, BackendTexture, SpriteBatchCommand, VertexData};
+use crate::framework::backend::{
+ Backend, BackendEventLoop, BackendGamepad, BackendRenderer, BackendShader, BackendTexture, SpriteBatchCommand,
+ VertexData,
+};
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem;
@@ -31,9 +34,9 @@ use crate::framework::graphics::BlendMode;
use crate::framework::keyboard::ScanCode;
use crate::framework::render_opengl::{GLContext, OpenGLRenderer};
use crate::framework::ui::init_imgui;
+use crate::game::shared_game_state::WindowMode;
use crate::game::Game;
use crate::game::GAME_SUSPENDED;
-use crate::game::shared_game_state::WindowMode;
pub struct SDL2Backend {
context: Sdl,
@@ -165,7 +168,7 @@ impl SDL2EventLoop {
window.resizable();
#[cfg(feature = "render-opengl")]
- window.opengl();
+ window.opengl();
let window = window.build().map_err(|e| GameError::WindowError(e.to_string()))?;
let opengl_available = if let Ok(v) = std::env::var("CAVESTORY_NO_OPENGL") { v != "1" } else { true };
@@ -207,7 +210,7 @@ impl BackendEventLoop for SDL2EventLoop {
loop {
#[cfg(target_os = "macos")]
- unsafe {
+ unsafe {
use objc::*;
// no UB: fields are initialized by SDL_GetWindowWMInfo
@@ -321,7 +324,8 @@ impl BackendEventLoop for SDL2EventLoop {
ctx.gamepad_context.add_gamepad(SDL2Gamepad::new(controller), axis_sensitivity);
unsafe {
- let controller_type = get_game_controller_type(sdl2_sys::SDL_GameControllerTypeForIndex(id as _));
+ let controller_type =
+ get_game_controller_type(sdl2_sys::SDL_GameControllerTypeForIndex(id as _));
ctx.gamepad_context.set_gamepad_type(id, controller_type);
}
}
@@ -404,19 +408,19 @@ impl BackendEventLoop for SDL2EventLoop {
fn new_renderer(&self, ctx: *mut Context) -> GameResult> {
#[cfg(feature = "render-opengl")]
- {
- let mut refs = self.refs.borrow_mut();
- match refs.window.window().gl_create_context() {
- Ok(gl_ctx) => {
- refs.window.window().gl_make_current(&gl_ctx).map_err(|e| GameError::RenderError(e.to_string()))?;
- refs.gl_context = Some(gl_ctx);
- }
- Err(err) => {
- *self.opengl_available.borrow_mut() = false;
- log::error!("Failed to initialize OpenGL context, falling back to SDL2 renderer: {}", err);
- }
+ {
+ let mut refs = self.refs.borrow_mut();
+ match refs.window.window().gl_create_context() {
+ Ok(gl_ctx) => {
+ refs.window.window().gl_make_current(&gl_ctx).map_err(|e| GameError::RenderError(e.to_string()))?;
+ refs.gl_context = Some(gl_ctx);
+ }
+ Err(err) => {
+ *self.opengl_available.borrow_mut() = false;
+ log::error!("Failed to initialize OpenGL context, falling back to SDL2 renderer: {}", err);
}
}
+ }
#[cfg(feature = "render-opengl")]
if *self.opengl_available.borrow() {
@@ -493,15 +497,14 @@ struct SDL2Gamepad {
}
impl SDL2Gamepad {
- pub fn new(inner: GameController) -> Box {
+ fn new(inner: GameController) -> Box {
Box::new(SDL2Gamepad { inner })
}
}
impl BackendGamepad for SDL2Gamepad {
fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult {
- self.inner.set_rumble(low_freq, high_freq, duration_ms)
- .map_err(|e| GameError::GamepadError(e.to_string()))
+ self.inner.set_rumble(low_freq, high_freq, duration_ms).map_err(|e| GameError::GamepadError(e.to_string()))
}
fn instance_id(&self) -> u32 {
diff --git a/src/framework/mod.rs b/src/framework/mod.rs
index a2ed9b0..fdfb362 100644
--- a/src/framework/mod.rs
+++ b/src/framework/mod.rs
@@ -3,6 +3,8 @@
pub mod backend;
#[cfg(feature = "backend-glutin")]
pub mod backend_glutin;
+#[cfg(feature = "backend-horizon")]
+pub mod backend_horizon;
pub mod backend_null;
#[cfg(feature = "backend-sdl")]
pub mod backend_sdl2;
diff --git a/src/framework/render_opengl.rs b/src/framework/render_opengl.rs
index f62d4f7..53b1ae0 100644
--- a/src/framework/render_opengl.rs
+++ b/src/framework/render_opengl.rs
@@ -301,149 +301,14 @@ fn check_shader_compile_status(shader: u32, gl: &Gl) -> GameResult {
Ok(())
}
-const VERTEX_SHADER_BASIC: &str = r"
-#version 110
+const VERTEX_SHADER_BASIC: &str = include_str!("shaders/opengl/vertex_basic_110.glsl");
+const FRAGMENT_SHADER_TEXTURED: &str = include_str!("shaders/opengl/fragment_textured_110.glsl");
+const FRAGMENT_SHADER_COLOR: &str = include_str!("shaders/opengl/fragment_color_110.glsl");
+const FRAGMENT_SHADER_WATER: &str = include_str!("shaders/opengl/fragment_water_110.glsl");
-uniform mat4 ProjMtx;
-attribute vec2 Position;
-attribute vec2 UV;
-attribute vec4 Color;
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- Frag_UV = UV;
- Frag_Color = Color;
- gl_Position = ProjMtx * vec4(Position.xy, 0.0, 1.0);
-}
-
-";
-
-const FRAGMENT_SHADER_TEXTURED: &str = r"
-#version 110
-
-uniform sampler2D Texture;
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);
-}
-
-";
-
-const FRAGMENT_SHADER_COLOR: &str = r"
-#version 110
-
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- gl_FragColor = Frag_Color;
-}
-
-";
-
-const FRAGMENT_SHADER_WATER: &str = r"
-#version 110
-
-uniform mat4 ProjMtx;
-uniform sampler2D Texture;
-uniform float Time;
-uniform float Scale;
-uniform vec2 FrameOffset;
-varying vec4 Frag_Color;
-
-void main()
-{
- vec2 resolution_inv = vec2(ProjMtx[0][0], -ProjMtx[1][1]) * 0.5;
- vec2 uv = gl_FragCoord.xy * resolution_inv;
- uv.y += 1.0;
- vec2 wave = uv;
- wave.x += sin((-FrameOffset.y * resolution_inv.y + uv.x * 16.0) + Time / 20.0) * Scale * resolution_inv.x;
- wave.y -= cos((-FrameOffset.x * resolution_inv.x + uv.y * 16.0) + Time / 5.0) * Scale * resolution_inv.y;
- float off = 0.35 * Scale * resolution_inv.y;
- float off2 = 2.0 * off;
-
- vec3 color = texture2D(Texture, wave).rgb * 0.25;
- color += texture2D(Texture, wave + vec2(0, off)).rgb * 0.125;
- color += texture2D(Texture, wave + vec2(0, -off)).rgb * 0.125;
-
- color.rg += texture2D(Texture, wave + vec2(-off, -off)).rg * 0.0625;
- color.rg += texture2D(Texture, wave + vec2(-off, 0)).rg * 0.125;
- color.rg += texture2D(Texture, wave + vec2(-off, off)).rg * 0.0625;
- color.b += texture2D(Texture, wave + vec2(-off2, -off)).b * 0.0625;
- color.b += texture2D(Texture, wave + vec2(-off2, 0)).b * 0.125;
- color.b += texture2D(Texture, wave + vec2(-off2, off)).b * 0.0625;
-
- color.rg += texture2D(Texture, wave + vec2(off, off)).gb * 0.0625;
- color.rg += texture2D(Texture, wave + vec2(off, 0)).gb * 0.125;
- color.rg += texture2D(Texture, wave + vec2(off, -off)).gb * 0.0625;
- color.b += texture2D(Texture, wave + vec2(off2, off)).r * 0.0625;
- color.b += texture2D(Texture, wave + vec2(off2, 0)).r * 0.125;
- color.b += texture2D(Texture, wave + vec2(off2, -off)).r * 0.0625;
-
- color *= (1.0 - Frag_Color.a);
- color += Frag_Color.rgb * Frag_Color.a;
- gl_FragColor = vec4(color, 1.0);
-}
-
-";
-
-const VERTEX_SHADER_BASIC_GLES: &str = r"
-#version 100
-
-precision mediump float;
-
-uniform mat4 ProjMtx;
-attribute vec2 Position;
-attribute vec2 UV;
-attribute vec4 Color;
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- Frag_UV = UV;
- Frag_Color = Color;
- gl_Position = ProjMtx * vec4(Position.xy, 0.0, 1.0);
-}
-
-";
-
-const FRAGMENT_SHADER_TEXTURED_GLES: &str = r"
-#version 100
-
-precision mediump float;
-
-uniform sampler2D Texture;
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);
-}
-
-";
-
-const FRAGMENT_SHADER_COLOR_GLES: &str = r"
-#version 100
-
-precision mediump float;
-
-varying vec2 Frag_UV;
-varying vec4 Frag_Color;
-
-void main()
-{
- gl_FragColor = Frag_Color;
-}
-
-";
+const VERTEX_SHADER_BASIC_GLES: &str = include_str!("shaders/opengles/vertex_basic_100.glsl");
+const FRAGMENT_SHADER_TEXTURED_GLES: &str = include_str!("shaders/opengles/fragment_textured_100.glsl");
+const FRAGMENT_SHADER_COLOR_GLES: &str = include_str!("shaders/opengles/fragment_color_100.glsl");
#[derive(Copy, Clone)]
struct RenderShader {
diff --git a/src/framework/shaders/deko3d/fragment_color.glsl b/src/framework/shaders/deko3d/fragment_color.glsl
new file mode 100644
index 0000000..5c74cf3
--- /dev/null
+++ b/src/framework/shaders/deko3d/fragment_color.glsl
@@ -0,0 +1,11 @@
+#version 460
+
+layout (location = 0) in vec2 Frag_UV;
+layout (location = 1) in vec4 Frag_Color;
+
+layout (location = 0) out vec4 outColor;
+
+void main()
+{
+ outColor = Frag_Color;
+}
diff --git a/src/framework/shaders/deko3d/fragment_textured.glsl b/src/framework/shaders/deko3d/fragment_textured.glsl
new file mode 100644
index 0000000..60dbffb
--- /dev/null
+++ b/src/framework/shaders/deko3d/fragment_textured.glsl
@@ -0,0 +1,13 @@
+#version 460
+
+layout (location = 0) in vec2 Frag_UV;
+layout (location = 1) in vec4 Frag_Color;
+
+layout (binding = 0) uniform sampler2D Texture;
+
+layout (location = 0) out vec4 outColor;
+
+void main()
+{
+ outColor = Frag_Color * texture(Texture, Frag_UV);
+}
diff --git a/src/framework/shaders/deko3d/vertex_basic.glsl b/src/framework/shaders/deko3d/vertex_basic.glsl
new file mode 100644
index 0000000..3d7fa72
--- /dev/null
+++ b/src/framework/shaders/deko3d/vertex_basic.glsl
@@ -0,0 +1,20 @@
+#version 460
+
+layout (location = 0) in vec2 Position;
+layout (location = 1) in vec2 UV;
+layout (location = 2) in vec4 Color;
+
+layout (location = 0) out vec2 Frag_UV;
+layout (location = 1) out vec4 Frag_Color;
+
+layout (std140, binding = 0) uniform VertUBO
+{
+ mat4 proj;
+} ProjMtx;
+
+void main()
+{
+ Frag_UV = UV;
+ Frag_Color = Color;
+ gl_Position = ProjMtx.proj * vec4(Position.xy, 0.0, 1.0);
+}
diff --git a/src/framework/shaders/opengl/fragment_color_110.glsl b/src/framework/shaders/opengl/fragment_color_110.glsl
new file mode 100644
index 0000000..e2c8938
--- /dev/null
+++ b/src/framework/shaders/opengl/fragment_color_110.glsl
@@ -0,0 +1,9 @@
+#version 110
+
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ gl_FragColor = Frag_Color;
+}
diff --git a/src/framework/shaders/opengl/fragment_textured_110.glsl b/src/framework/shaders/opengl/fragment_textured_110.glsl
new file mode 100644
index 0000000..341ff2c
--- /dev/null
+++ b/src/framework/shaders/opengl/fragment_textured_110.glsl
@@ -0,0 +1,10 @@
+#version 110
+
+uniform sampler2D Texture;
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);
+}
diff --git a/src/framework/shaders/opengl/fragment_water_110.glsl b/src/framework/shaders/opengl/fragment_water_110.glsl
new file mode 100644
index 0000000..3c53cfb
--- /dev/null
+++ b/src/framework/shaders/opengl/fragment_water_110.glsl
@@ -0,0 +1,42 @@
+#version 110
+
+uniform mat4 ProjMtx;
+uniform sampler2D Texture;
+uniform float Time;
+uniform float Scale;
+uniform vec2 FrameOffset;
+varying vec4 Frag_Color;
+
+void main()
+{
+ vec2 resolution_inv = vec2(ProjMtx[0][0], -ProjMtx[1][1]) * 0.5;
+ vec2 uv = gl_FragCoord.xy * resolution_inv;
+ uv.y += 1.0;
+ vec2 wave = uv;
+ wave.x += sin((-FrameOffset.y * resolution_inv.y + uv.x * 16.0) + Time / 20.0) * Scale * resolution_inv.x;
+ wave.y -= cos((-FrameOffset.x * resolution_inv.x + uv.y * 16.0) + Time / 5.0) * Scale * resolution_inv.y;
+ float off = 0.35 * Scale * resolution_inv.y;
+ float off2 = 2.0 * off;
+
+ vec3 color = texture2D(Texture, wave).rgb * 0.25;
+ color += texture2D(Texture, wave + vec2(0, off)).rgb * 0.125;
+ color += texture2D(Texture, wave + vec2(0, -off)).rgb * 0.125;
+
+ color.rg += texture2D(Texture, wave + vec2(-off, -off)).rg * 0.0625;
+ color.rg += texture2D(Texture, wave + vec2(-off, 0)).rg * 0.125;
+ color.rg += texture2D(Texture, wave + vec2(-off, off)).rg * 0.0625;
+ color.b += texture2D(Texture, wave + vec2(-off2, -off)).b * 0.0625;
+ color.b += texture2D(Texture, wave + vec2(-off2, 0)).b * 0.125;
+ color.b += texture2D(Texture, wave + vec2(-off2, off)).b * 0.0625;
+
+ color.rg += texture2D(Texture, wave + vec2(off, off)).gb * 0.0625;
+ color.rg += texture2D(Texture, wave + vec2(off, 0)).gb * 0.125;
+ color.rg += texture2D(Texture, wave + vec2(off, -off)).gb * 0.0625;
+ color.b += texture2D(Texture, wave + vec2(off2, off)).r * 0.0625;
+ color.b += texture2D(Texture, wave + vec2(off2, 0)).r * 0.125;
+ color.b += texture2D(Texture, wave + vec2(off2, -off)).r * 0.0625;
+
+ color *= (1.0 - Frag_Color.a);
+ color += Frag_Color.rgb * Frag_Color.a;
+ gl_FragColor = vec4(color, 1.0);
+}
diff --git a/src/framework/shaders/opengl/vertex_basic_110.glsl b/src/framework/shaders/opengl/vertex_basic_110.glsl
new file mode 100644
index 0000000..72bec0f
--- /dev/null
+++ b/src/framework/shaders/opengl/vertex_basic_110.glsl
@@ -0,0 +1,15 @@
+#version 110
+
+uniform mat4 ProjMtx;
+attribute vec2 Position;
+attribute vec2 UV;
+attribute vec4 Color;
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ Frag_UV = UV;
+ Frag_Color = Color;
+ gl_Position = ProjMtx * vec4(Position.xy, 0.0, 1.0);
+}
diff --git a/src/framework/shaders/opengles/fragment_color_100.glsl b/src/framework/shaders/opengles/fragment_color_100.glsl
new file mode 100644
index 0000000..7576d7b
--- /dev/null
+++ b/src/framework/shaders/opengles/fragment_color_100.glsl
@@ -0,0 +1,11 @@
+#version 100
+
+precision mediump float;
+
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ gl_FragColor = Frag_Color;
+}
diff --git a/src/framework/shaders/opengles/fragment_textured_100.glsl b/src/framework/shaders/opengles/fragment_textured_100.glsl
new file mode 100644
index 0000000..d3ca3a5
--- /dev/null
+++ b/src/framework/shaders/opengles/fragment_textured_100.glsl
@@ -0,0 +1,12 @@
+#version 100
+
+precision mediump float;
+
+uniform sampler2D Texture;
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);
+}
diff --git a/src/framework/shaders/opengles/vertex_basic_100.glsl b/src/framework/shaders/opengles/vertex_basic_100.glsl
new file mode 100644
index 0000000..f62d4b1
--- /dev/null
+++ b/src/framework/shaders/opengles/vertex_basic_100.glsl
@@ -0,0 +1,17 @@
+#version 100
+
+precision mediump float;
+
+uniform mat4 ProjMtx;
+attribute vec2 Position;
+attribute vec2 UV;
+attribute vec4 Color;
+varying vec2 Frag_UV;
+varying vec4 Frag_Color;
+
+void main()
+{
+ Frag_UV = UV;
+ Frag_Color = Color;
+ gl_Position = ProjMtx * vec4(Position.xy, 0.0, 1.0);
+}
diff --git a/src/framework/vfs.rs b/src/framework/vfs.rs
index ce09442..efe9f2f 100644
--- a/src/framework/vfs.rs
+++ b/src/framework/vfs.rs
@@ -128,7 +128,7 @@ pub trait VFS: Debug {
fn metadata(&self, path: &Path) -> GameResult>;
/// Retrieve all file and directory entries in the given directory.
- fn read_dir(&self, path: &Path) -> GameResult>>>;
+ fn read_dir(&self, path: &Path) -> GameResult>>>;
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option;
@@ -156,6 +156,7 @@ pub trait VMetadata {
pub struct PhysicalFS {
root: PathBuf,
readonly: bool,
+ lowercase: bool,
}
#[derive(Debug, Clone)]
@@ -214,7 +215,11 @@ fn sanitize_path(path: &path::Path) -> Option {
impl PhysicalFS {
/// Creates a new PhysicalFS
pub fn new(root: &Path, readonly: bool) -> Self {
- PhysicalFS { root: root.into(), readonly }
+ PhysicalFS { root: root.into(), readonly, lowercase: false }
+ }
+
+ pub fn new_lowercase(root: &Path) -> Self {
+ PhysicalFS { root: root.into(), readonly: true, lowercase: true }
}
/// Takes a given path (&str) and returns
@@ -222,7 +227,11 @@ impl PhysicalFS {
/// absolute path you get when appending it
/// to this filesystem's root.
fn to_absolute(&self, p: &Path) -> GameResult {
- if let Some(safe_path) = sanitize_path(p) {
+ if let Some(mut safe_path) = sanitize_path(p) {
+ if self.lowercase {
+ safe_path = PathBuf::from(p.to_string_lossy().to_lowercase())
+ }
+
let mut root_path = self.root.clone();
root_path.push(safe_path.clone());
@@ -376,7 +385,7 @@ impl VFS for PhysicalFS {
}
/// Retrieve the path entries in this path
- fn read_dir(&self, path: &Path) -> GameResult>>> {
+ fn read_dir(&self, path: &Path) -> GameResult>>> {
self.create_root()?;
let p = self.to_absolute(path)?;
// This is inconvenient because path() returns the full absolute
@@ -511,7 +520,7 @@ impl VFS for OverlayFS {
}
/// Retrieve the path entries in this path
- fn read_dir(&self, path: &Path) -> GameResult>>> {
+ fn read_dir(&self, path: &Path) -> GameResult>>> {
// This is tricky 'cause we have to actually merge iterators together...
// Doing it the simple and stupid way works though.
let mut v = Vec::new();
diff --git a/src/game/mod.rs b/src/game/mod.rs
index b8e69b2..427835d 100644
--- a/src/game/mod.rs
+++ b/src/game/mod.rs
@@ -265,7 +265,7 @@ pub fn init(options: LaunchOptions) -> GameResult {
log::info!("Resource directory: {:?}", resource_dir);
log::info!("Initializing engine...");
- let mut context = Context::new();
+ let mut context = Box::pin(Context::new());
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
mount_vfs(&mut context, Box::new(PhysicalFS::new(&resource_dir, true)));
@@ -294,6 +294,23 @@ pub fn init(options: LaunchOptions) -> GameResult {
mount_vfs(&mut context, Box::new(PhysicalFS::new(&data_path, true)));
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_path, false)));
}
+ #[cfg(target_os = "horizon")]
+ {
+ let mut data_path = PathBuf::from("sdmc:/switch/doukutsu-rs/data");
+ let mut user_path = PathBuf::from("sdmc:/switch/doukutsu-rs/user");
+
+ let _ = std::fs::create_dir_all(&data_path);
+ let _ = std::fs::create_dir_all(&user_path);
+
+ log::info!("Mounting VFS");
+ mount_vfs(&mut context, Box::new(PhysicalFS::new(&data_path, true)));
+ if crate::framework::backend_horizon::mount_romfs() {
+ mount_vfs(&mut context, Box::new(PhysicalFS::new_lowercase(&PathBuf::from("romfs:/data"))));
+ }
+ log::info!("Mounting user VFS");
+ mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_path, false)));
+ log::info!("ok");
+ }
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
{
@@ -308,6 +325,7 @@ pub fn init(options: LaunchOptions) -> GameResult {
}
}
+ log::info!("Mounting built-in FS");
mount_vfs(&mut context, Box::new(BuiltinFS::new()));
if options.server_mode {
@@ -315,15 +333,15 @@ pub fn init(options: LaunchOptions) -> GameResult {
context.headless = true;
}
- let game = UnsafeCell::new(Game::new(&mut context)?);
- let state_ref = unsafe { &mut *((&mut *game.get()).state.get()) };
+ let mut game = Box::pin(Game::new(&mut context)?);
#[cfg(feature = "scripting-lua")]
{
- state_ref.lua.update_refs(unsafe { (&*game.get()).state.get() }, &mut context as *mut Context);
+ game.state.get().lua.update_refs(unsafe { &mut *game.state.get() }, &mut context as *mut Context);
}
- state_ref.next_scene = Some(Box::new(LoadingScene::new()));
- context.run(unsafe { &mut *game.get() })?;
+ game.state.get_mut().next_scene = Some(Box::new(LoadingScene::new()));
+ log::info!("Starting main loop...");
+ context.run(game.as_mut().get_mut())?;
Ok(())
}
diff --git a/src/game/settings.rs b/src/game/settings.rs
index ca9a55b..437ee57 100644
--- a/src/game/settings.rs
+++ b/src/game/settings.rs
@@ -39,9 +39,9 @@ pub struct Settings {
pub pause_on_focus_loss: bool,
#[serde(default = "default_interpolation")]
pub organya_interpolation: InterpolationMode,
- #[serde(default = "default_controller_type")]
+ #[serde(default = "default_p1_controller_type")]
pub player1_controller_type: ControllerType,
- #[serde(default = "default_controller_type")]
+ #[serde(default = "default_p2_controller_type")]
pub player2_controller_type: ControllerType,
#[serde(default = "p1_default_keymap")]
pub player1_key_map: PlayerKeyMap,
@@ -133,8 +133,21 @@ fn default_screen_shake_intensity() -> ScreenShakeIntensity {
}
#[inline(always)]
-fn default_controller_type() -> ControllerType {
- ControllerType::Keyboard
+fn default_p1_controller_type() -> ControllerType {
+ if cfg!(any(target_os = "horizon")) {
+ ControllerType::Gamepad(0)
+ } else {
+ ControllerType::Keyboard
+ }
+}
+
+#[inline(always)]
+fn default_p2_controller_type() -> ControllerType {
+ if cfg!(any(target_os = "horizon")) {
+ ControllerType::Gamepad(1)
+ } else {
+ ControllerType::Keyboard
+ }
}
#[inline(always)]
@@ -216,8 +229,8 @@ impl Settings {
if self.version == 11 {
self.version = 12;
- self.player1_controller_type = default_controller_type();
- self.player2_controller_type = default_controller_type();
+ self.player1_controller_type = default_p1_controller_type();
+ self.player2_controller_type = default_p2_controller_type();
self.player1_controller_button_map = player_default_controller_button_map();
self.player2_controller_button_map = player_default_controller_button_map();
self.player1_controller_axis_sensitivity = default_controller_axis_sensitivity();
@@ -396,8 +409,8 @@ impl Default for Settings {
timing_mode: default_timing(),
pause_on_focus_loss: default_pause_on_focus_loss(),
organya_interpolation: InterpolationMode::Linear,
- player1_controller_type: default_controller_type(),
- player2_controller_type: default_controller_type(),
+ player1_controller_type: default_p1_controller_type(),
+ player2_controller_type: default_p2_controller_type(),
player1_key_map: p1_default_keymap(),
player2_key_map: p2_default_keymap(),
player1_controller_button_map: player_default_controller_button_map(),
diff --git a/src/scene/no_data_scene.rs b/src/scene/no_data_scene.rs
index 9c4a209..4b8299d 100644
--- a/src/scene/no_data_scene.rs
+++ b/src/scene/no_data_scene.rs
@@ -1,5 +1,7 @@
+use crate::common::Color;
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
+use crate::framework::graphics;
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::Font;
use crate::scene::Scene;
@@ -49,6 +51,8 @@ impl Scene for NoDataScene {
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
+ graphics::clear(ctx, Color::from_rgb(30, 0, 0));
+
state.font.builder().center(state.canvas_size.0).y(10.0).color((255, 100, 100, 255)).draw(
"doukutsu-rs internal error",
ctx,
@@ -63,36 +67,57 @@ impl Scene for NoDataScene {
&mut state.texture_set,
)?;
+ let mut y = 60.0;
#[cfg(target_os = "android")]
{
let yellow = (255, 255, 0, 255);
- state.font.builder().center(state.canvas_size.0).y(60.0).color(yellow).draw(
+ state.font.builder().center(state.canvas_size.0).y(y).color(yellow).draw(
"It's likely that you haven't extracted the game data properly.",
ctx,
&state.constants,
&mut state.texture_set,
)?;
- state.font.builder().center(state.canvas_size.0).y(80.0).color(yellow).draw(
+ y += 20.0;
+ state.font.builder().center(state.canvas_size.0).y(y).color(yellow).draw(
"Click here to open the guide.",
ctx,
&state.constants,
&mut state.texture_set,
)?;
- state.font.builder().center(state.canvas_size.0).y(100.0).color(yellow).draw(
+ y += 20.0;
+ state.font.builder().center(state.canvas_size.0).y(y).color(yellow).draw(
REL_URL,
ctx,
&state.constants,
&mut state.texture_set,
)?;
+ y += 20.0;
}
{
- state.font.builder().center(state.canvas_size.0).y(140.0).draw(
- &self.err,
- ctx,
- &state.constants,
- &mut state.texture_set,
- )?;
+ // put max 80 chars per line
+ let mut lines = Vec::new();
+ let mut line = String::new();
+
+ for word in self.err.split(' ') {
+ if line.len() + word.len() > 80 {
+ lines.push(line);
+ line = String::new();
+ }
+ line.push_str(word);
+ line.push(' ');
+ }
+ lines.push(line);
+
+ for line in lines {
+ state.font.builder().center(state.canvas_size.0).y(y).draw(
+ &line,
+ ctx,
+ &state.constants,
+ &mut state.texture_set,
+ )?;
+ y += 20.0;
+ }
}
Ok(())
diff --git a/src/util/browser.rs b/src/util/browser.rs
index 25366ee..114f49d 100644
--- a/src/util/browser.rs
+++ b/src/util/browser.rs
@@ -2,7 +2,12 @@
pub use webbrowser::open;
// stub for platforms webbrowser doesn't support, such as Horizon OS
-#[cfg(not(feature = "webbrowser"))]
+#[cfg(not(any(feature = "webbrowser", target_os = "horizon")))]
pub fn open(_url: &str) -> std::io::Result<()> {
Ok(())
}
+
+#[cfg(target_os = "horizon")]
+pub fn open(url: &str) -> std::io::Result<()> {
+ crate::framework::backend_horizon::web_open(url)
+}