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) +}