├── rust-toolchain ├── .gitignore ├── internal ├── flowergal-buildtools │ ├── src │ │ ├── music │ │ │ ├── mod.rs │ │ │ └── pcm_conv.rs │ │ ├── main.rs │ │ ├── lib.rs │ │ ├── user_interface.rs │ │ ├── tile_generation │ │ │ ├── mod.rs │ │ │ ├── sdl_support.rs │ │ │ ├── tile_pixels.rs │ │ │ ├── pattern.rs │ │ │ ├── palette.rs │ │ │ ├── map.rs │ │ │ └── image_bank.rs │ │ ├── compression.rs │ │ ├── license_text.rs │ │ └── world_data_gen.rs │ ├── Cargo.toml │ └── COPYING ├── flowergal-proj-config │ ├── src │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── world_info.rs │ │ ├── sound_info.rs │ │ └── resources │ │ │ ├── blend.rs │ │ │ └── mod.rs │ └── Cargo.toml ├── flowergal-proj-assets │ ├── src │ │ └── lib.rs │ ├── Cargo.toml │ └── build.rs └── flowergal-runtime │ ├── Cargo.toml │ ├── COPYING-simpleflac │ ├── src │ ├── timers.rs │ ├── interrupt_service.rs │ ├── lib.rs │ ├── audio │ │ ├── raw_pcm.rs │ │ └── mod.rs │ ├── logging.rs │ ├── render │ │ ├── palette.rs │ │ └── mod.rs │ └── memory.rs │ └── COPYING ├── assets └── gfx │ ├── hud.png │ ├── hud.xcf │ ├── font.png │ ├── overlay.png │ ├── overlay.xcf │ ├── textbox.png │ ├── font_white_shadowed_ascii.xcf │ ├── aaker-4gvSHtHgOx4-unsplash.png │ ├── aaker-4gvSHtHgOx4-unsplash.xcf │ ├── font_white_shadowed_ascii_ibm.xcf │ ├── amanda-dalbjorn-fvInY-Gh7sc-unsplash.png │ └── amanda-dalbjorn-fvInY-Gh7sc-unsplash.xcf ├── keymap.toml ├── .gitmodules ├── .cargo └── config.toml ├── armv4t-none-eabi.json ├── armv5te-none-eabi.json ├── Makefile ├── rt0.s ├── Cargo.toml ├── COPYING ├── scratch.txt ├── linker_script.ld ├── src ├── main.rs ├── hud.rs └── world.rs └── README.md /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2021-01-15 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /.idea 3 | /assets/mp3/* 4 | decomp.wav 5 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/music/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pcm_conv; 2 | -------------------------------------------------------------------------------- /assets/gfx/hud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/hud.png -------------------------------------------------------------------------------- /assets/gfx/hud.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/hud.xcf -------------------------------------------------------------------------------- /assets/gfx/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/font.png -------------------------------------------------------------------------------- /assets/gfx/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/overlay.png -------------------------------------------------------------------------------- /assets/gfx/overlay.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/overlay.xcf -------------------------------------------------------------------------------- /assets/gfx/textbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/textbox.png -------------------------------------------------------------------------------- /assets/gfx/font_white_shadowed_ascii.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/font_white_shadowed_ascii.xcf -------------------------------------------------------------------------------- /assets/gfx/aaker-4gvSHtHgOx4-unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/aaker-4gvSHtHgOx4-unsplash.png -------------------------------------------------------------------------------- /assets/gfx/aaker-4gvSHtHgOx4-unsplash.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/aaker-4gvSHtHgOx4-unsplash.xcf -------------------------------------------------------------------------------- /assets/gfx/font_white_shadowed_ascii_ibm.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/font_white_shadowed_ascii_ibm.xcf -------------------------------------------------------------------------------- /assets/gfx/amanda-dalbjorn-fvInY-Gh7sc-unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/amanda-dalbjorn-fvInY-Gh7sc-unsplash.png -------------------------------------------------------------------------------- /assets/gfx/amanda-dalbjorn-fvInY-Gh7sc-unsplash.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifning/gba-flac-demo/HEAD/assets/gfx/amanda-dalbjorn-fvInY-Gh7sc-unsplash.xcf -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate flowergal_buildtools; 2 | //use flowergal_buildtools::*; 3 | use std::error::Error; 4 | 5 | fn main() -> Result<(), Box> { 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /keymap.toml: -------------------------------------------------------------------------------- 1 | # See https://wiki.libsdl.org/SDL_Keycode for key names 2 | 3 | [general] 4 | fastforward = "Space" 5 | reset = "F9" 6 | fullscreen = "F10" 7 | 8 | [gba] 9 | a = "X" 10 | b = "Z" 11 | l = "A" 12 | r = "S" 13 | start = "Return" 14 | select = "Backspace" 15 | up = "Up" 16 | down = "Down" 17 | left = "Left" 18 | right = "Right" 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/gba"] 2 | path = external/gba 3 | url = https://github.com/lifning/gba 4 | [submodule "gba-compression"] 5 | path = external/gba-compression 6 | url = https://github.com/lifning/gba-compression 7 | [submodule "external/lossywav"] 8 | path = external/lossywav 9 | url = https://github.com/lifning/lossywav 10 | [submodule "external/flac"] 11 | path = external/flac 12 | url = https://github.com/lifning/flac 13 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_arch = "arm", no_std)] 2 | #![feature(const_fn)] 3 | #![feature(const_fn_transmute)] 4 | #![feature(const_raw_ptr_deref)] 5 | #![feature(const_slice_from_raw_parts)] 6 | 7 | #[macro_use] 8 | extern crate static_assertions; 9 | 10 | pub mod resources; 11 | 12 | pub mod world_info; 13 | pub use world_info::*; 14 | 15 | pub mod sound_info; 16 | 17 | #[macro_use] 18 | pub mod macros; 19 | -------------------------------------------------------------------------------- /internal/flowergal-proj-assets/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![feature(const_fn_transmute)] 3 | 4 | #[macro_use] 5 | extern crate build_const; 6 | 7 | #[macro_use] 8 | extern crate flowergal_proj_config; 9 | 10 | use flowergal_proj_config::resources::*; 11 | use flowergal_proj_config::sound_info::{MusicId, TrackList}; 12 | use flowergal_proj_config::world_info::WorldId::*; 13 | 14 | build_const!("world_gfx_bc"); 15 | build_const!("ui_gfx_bc"); 16 | build_const!("sound_data_bc"); 17 | build_const!("license_text_bc"); 18 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | #![feature(seek_convenience)] 4 | #![feature(generators)] 5 | #![feature(drain_filter)] 6 | #![feature(command_access)] 7 | 8 | #![allow(clippy::comparison_chain)] 9 | #![allow(clippy::new_without_default)] 10 | #![allow(clippy::ptr_arg)] 11 | 12 | pub mod compression; 13 | pub mod music; 14 | pub mod tile_generation; 15 | pub mod user_interface; 16 | pub mod world_data_gen; 17 | pub mod license_text; 18 | -------------------------------------------------------------------------------- /internal/flowergal-proj-assets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flowergal-proj-assets" 3 | version = "0.1.0" 4 | authors = ["lif "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [build-dependencies] 10 | flowergal-buildtools = { path = "../flowergal-buildtools" } 11 | 12 | [dependencies] 13 | flowergal-proj-config = { path = "../flowergal-proj-config" } 14 | build_const = { version = "0.2", default-features = false } 15 | gba = { path = "../../external/gba" } 16 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flowergal-runtime" 3 | version = "0.1.0" 4 | authors = ["lif "] 5 | edition = "2018" 6 | license = "BSD-3-Clause AND MIT" 7 | # license-file = "COPYING" 8 | 9 | [dependencies] 10 | flowergal-proj-config = { path = "../flowergal-proj-config" } 11 | gba = { path = "../../external/gba" } 12 | heapless = { version = "0.5", default-features = false } 13 | 14 | [features] 15 | bench_audio = [] 16 | bench_video = [] 17 | bench_flac = [] 18 | debug_bitbuffer = [] 19 | flexible_flac = [] 20 | detect_silence = [] 21 | verify_asm = [] 22 | supercard = [] 23 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flowergal-proj-config" 3 | version = "0.1.0" 4 | authors = ["lif "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | #serde = { version = "^1", default-features = false, features = ["derive"] } 9 | #smallvec = { path = "../../external/rust-smallvec", default-features = false, features = ["serde"] } 10 | #arraystring = { path = "../../external/arraystring", default-features = false, features = ["impl-all", "serde-traits"] } 11 | heapless = { version = "0.5", default-features = false } 12 | gba = { path = "../../external/gba" } 13 | static_assertions = "1.1" 14 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// shoutouts to https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049 2 | 3 | #[repr(C)] // guarantee 'bytes' comes after '_align' 4 | pub struct AlignedAs { 5 | pub _align: [Align; 0], 6 | pub bytes: Bytes, 7 | } 8 | 9 | #[macro_export] 10 | macro_rules! include_bytes_align_as { 11 | ($align_ty:ty, $path:literal) => {{ 12 | // const block expression to encapsulate the static 13 | use $crate::macros::AlignedAs; 14 | 15 | // this assignment is made possible by CoerceUnsized 16 | const ALIGNED: &AlignedAs<$align_ty, [u8]> = &AlignedAs { 17 | _align: [], 18 | bytes: *include_bytes!($path), 19 | }; 20 | 21 | &ALIGNED.bytes 22 | }}; 23 | } 24 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flowergal-buildtools" 3 | version = "0.1.0" 4 | authors = ["lif "] 5 | edition = "2018" 6 | license = "AGPL-3.0" 7 | 8 | [dependencies] 9 | flowergal-proj-config = { path = "../flowergal-proj-config" } 10 | gba = { path = "../../external/gba" } 11 | gba-compression = { path = "../../external/gba-compression" } 12 | build_const = { version = "0.2" } 13 | cargo-about = "0.2.3" 14 | itertools = "0.10" 15 | spdx = "0.3.4" # must match cargo-about's version 16 | textwrap = { version = "0.13", features = ["hyphenation"] } 17 | hyphenation = { version = "0.8", features = ["embed_en-us"] } # must match textwrap's version 18 | regex = "1" 19 | rayon = "1" 20 | gen-iter = "0.2" 21 | tempfile = "3.1" 22 | 23 | # level layout 24 | sdl2 = { version = "0.34", features = ["image"] } 25 | 26 | [features] 27 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.release] 2 | rustflags = ["-Cinline-threshold=275"] 3 | 4 | [build] 5 | #target = "thumbv4t-none-eabi" 6 | target = "armv4t-none-eabi.json" 7 | 8 | [target.thumbv4t-none-eabi] 9 | runner = "mgba-qt -3 -C interframeBlending=1 --log-level 15" 10 | rustflags = ["--emit=asm", "-Clink-arg=-Tlinker_script.ld"] 11 | 12 | [target.armv4t-none-eabi] 13 | runner = "mgba-qt -3 -C interframeBlending=1 --log-level 15" 14 | rustflags = ["--emit=asm", "-Clink-arg=-Tlinker_script.ld"] 15 | 16 | # we might eventually... *eventually* target the DS too 17 | [target.armv5te-none-eabi] 18 | rustflags = ["--emit=asm", "-Clink-arg=-Tlinker_script.ld"] 19 | 20 | [unstable] 21 | build-std = ["core", "compiler_builtins"] 22 | build-std-features = ["compiler-builtins-mem", "backtrace"] 23 | # prevent dev-dependencies' feature settings polluting the target dependencies 24 | features = ["host_dep", "build_dep"] 25 | -------------------------------------------------------------------------------- /internal/flowergal-proj-assets/build.rs: -------------------------------------------------------------------------------- 1 | use flowergal_buildtools::music::pcm_conv; 2 | use flowergal_buildtools::{user_interface, world_data_gen}; 3 | use flowergal_buildtools::license_text; 4 | 5 | trait NiceExpect { 6 | fn nice_expect(self, msg: &str); 7 | } 8 | 9 | impl NiceExpect for Result { 10 | fn nice_expect(self, msg: &str) { 11 | if let Err(e) = self { 12 | eprintln!("{}", e); 13 | panic!("{}", msg); 14 | } 15 | } 16 | } 17 | 18 | fn main() { 19 | pcm_conv::convert_songs_and_sfx().nice_expect("Couldn't convert audio files"); 20 | license_text::generate_text().nice_expect("Couldn't collect dependency license files"); 21 | user_interface::convert_assets().nice_expect("Couldn't convert assets for UI"); 22 | world_data_gen::convert_world_maps().nice_expect("Couldn't convert level maps from original assets"); 23 | } 24 | -------------------------------------------------------------------------------- /armv4t-none-eabi.json: -------------------------------------------------------------------------------- 1 | { 2 | "arch": "arm", 3 | "asm-args": [ 4 | "-mthumb-interwork", 5 | "-march=armv4t", 6 | "-mcpu=arm7tdmi", 7 | "-mlittle-endian" 8 | ], 9 | "atomic-cas": false, 10 | "data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64", 11 | "eliminate-frame-pointer": false, 12 | "emit-debug-gdb-scripts": false, 13 | "executables": true, 14 | "features": "+soft-float,+strict-align", 15 | "has-thumb-interworking": true, 16 | "is-builtin": true, 17 | "linker": "arm-none-eabi-ld", 18 | "linker-flavor": "ld", 19 | "linker-is-gnu": true, 20 | "llvm-target": "armv4t-none-eabi", 21 | "main-needs-argc-argv": false, 22 | "panic-strategy": "abort", 23 | "relocation-model": "static", 24 | "target-pointer-width": "32", 25 | "unsupported-abis": [ 26 | "stdcall", 27 | "fastcall", 28 | "vectorcall", 29 | "thiscall", 30 | "win64", 31 | "sysv64" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /armv5te-none-eabi.json: -------------------------------------------------------------------------------- 1 | { 2 | "arch": "arm", 3 | "asm-args": [ 4 | "-mthumb-interwork", 5 | "-march=armv5te", 6 | "-mcpu=arm946e-s", 7 | "-mlittle-endian" 8 | ], 9 | "atomic-cas": false, 10 | "data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64", 11 | "eliminate-frame-pointer": false, 12 | "emit-debug-gdb-scripts": false, 13 | "executables": true, 14 | "features": "+soft-float,+strict-align", 15 | "has-thumb-interworking": true, 16 | "is-builtin": true, 17 | "linker": "arm-none-eabi-ld", 18 | "linker-flavor": "ld", 19 | "linker-is-gnu": true, 20 | "llvm-target": "armv5te-none-eabi", 21 | "main-needs-argc-argv": false, 22 | "panic-strategy": "abort", 23 | "relocation-model": "static", 24 | "target-pointer-width": "32", 25 | "unsupported-abis": [ 26 | "stdcall", 27 | "fastcall", 28 | "vectorcall", 29 | "thiscall", 30 | "win64", 31 | "sysv64" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PATH := $(DEVKITARM)/bin:$(PATH) 2 | 3 | PROJECT_NAME=flac-demo 4 | BUILD_TYPE := release 5 | # or debug 6 | 7 | BUILD_ARG = 8 | ifeq ($(BUILD_TYPE),release) 9 | BUILD_ARG = --$(BUILD_TYPE) 10 | endif 11 | 12 | CARGO_TARGET_PATH=target 13 | TARGET=armv4t-none-eabi 14 | ELF_OUTPUT=$(CARGO_TARGET_PATH)/$(TARGET)/$(BUILD_TYPE)/$(PROJECT_NAME) 15 | ROM_OUTPUT=$(CARGO_TARGET_PATH)/$(PROJECT_NAME)-$(BUILD_TYPE).gba 16 | EMU := mgba-qt -3 -C interframeBlending=1 --log-level 15 17 | 18 | SOURCES=$(shell find src internal -name \*.rs) 19 | 20 | all: $(ROM_OUTPUT) 21 | 22 | test: $(ROM_OUTPUT) 23 | $(EMU) $(ROM_OUTPUT) 24 | 25 | debug: $(ROM_OUTPUT) 26 | cp $(ELF_OUTPUT) $(ROM_OUTPUT).elf 27 | $(EMU) $(ROM_OUTPUT).elf 28 | 29 | $(ELF_OUTPUT): $(SOURCES) rt0.s Cargo.toml linker_script.ld 30 | cargo build $(BUILD_ARG) 31 | 32 | $(ROM_OUTPUT): $(ELF_OUTPUT) 33 | arm-none-eabi-objcopy -O binary $(ELF_OUTPUT) $(ROM_OUTPUT) 34 | gbafix $(ROM_OUTPUT) 35 | 36 | clean: 37 | cargo clean 38 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/COPYING-simpleflac: -------------------------------------------------------------------------------- 1 | simple_flac.rs was ported to Rust + ARMv4 assembly by lifning, from: 2 | 3 | Simple FLAC decoder (Python) 4 | 5 | Copyright (c) 2020 Project Nayuki. (MIT License) 6 | https://www.nayuki.io/page/simple-flac-implementation 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | - The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | - The Software is provided "as is", without warranty of any kind, express or 17 | implied, including but not limited to the warranties of merchantability, 18 | fitness for a particular purpose and noninfringement. In no event shall the 19 | authors or copyright holders be liable for any claim, damages or other 20 | liability, whether in an action of contract, tort or otherwise, arising from, 21 | out of or in connection with the Software or the use or other dealings in the 22 | Software. 23 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/world_info.rs: -------------------------------------------------------------------------------- 1 | use crate::resources::ColorEffectType; 2 | use crate::sound_info::{MusicId, TrackList}; 3 | 4 | pub struct WorldResourceInfo { 5 | pub id: WorldId, 6 | pub name: &'static str, 7 | pub tilemap_path: &'static str, 8 | pub skybox_path: Option<&'static str>, 9 | pub effect_path: Option<&'static str>, 10 | pub effect_type: ColorEffectType, 11 | pub anim_path: Option<&'static str>, 12 | pub minimap_path: Option<&'static str>, 13 | pub songs: TrackList, 14 | } 15 | 16 | // indeces in the WORLD_INFO array, among others 17 | #[repr(usize)] 18 | #[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] 19 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 20 | pub enum WorldId { 21 | TomsDiner, 22 | } 23 | 24 | pub const WORLD_RESOURCE_INFO: &[WorldResourceInfo] = &[ 25 | WorldResourceInfo { 26 | id: WorldId::TomsDiner, 27 | name: "TOMS_DINER", 28 | tilemap_path: "aaker-4gvSHtHgOx4-unsplash.png", // https://unsplash.com/photos/4gvSHtHgOx4 29 | skybox_path: Some("amanda-dalbjorn-fvInY-Gh7sc-unsplash.png"), // https://unsplash.com/photos/fvInY-Gh7sc 30 | effect_path: Some("overlay.png"), 31 | effect_type: ColorEffectType::Overlay, 32 | anim_path: None, 33 | minimap_path: Some("Minimap_Apartment.csv"), 34 | songs: TrackList(&[MusicId::TomsDiner]), 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/timers.rs: -------------------------------------------------------------------------------- 1 | use gba::io::timers::{TM3CNT_H, TM2CNT_H, TimerControlSetting, TimerTickRate, TM3CNT_L, TM2CNT_L, TM1CNT_H, TM1CNT_L}; 2 | 3 | pub struct GbaTimer {} 4 | 5 | impl GbaTimer { 6 | pub const fn new() -> Self { 7 | GbaTimer{} 8 | } 9 | 10 | pub fn initialize(&self) { 11 | TM2CNT_H.write(TimerControlSetting::new()); 12 | TM3CNT_H.write(TimerControlSetting::new()); 13 | 14 | TM2CNT_L.write(0); 15 | TM3CNT_L.write(0); 16 | 17 | TM2CNT_H.write(TimerControlSetting::new() 18 | .with_enabled(true) 19 | .with_tick_rate(TimerTickRate::CPU64)); 20 | TM3CNT_H.write(TimerControlSetting::new() 21 | .with_enabled(true) 22 | .with_tick_rate(TimerTickRate::Cascade)); 23 | } 24 | 25 | pub fn setup_timer1_irq(cycles: u16) { 26 | TM1CNT_L.write(0u16.overflowing_sub(cycles).0); 27 | TM1CNT_H.write(TimerControlSetting::new() 28 | .with_tick_rate(TimerTickRate::CPU1) 29 | .with_overflow_irq(true) 30 | .with_enabled(true)); 31 | } 32 | 33 | pub fn timer1() { 34 | TM1CNT_H.write(TimerControlSetting::new()); 35 | } 36 | 37 | #[inline(always)] 38 | pub fn get_ticks() -> u32 { 39 | unsafe { asm!("", options(nostack)); } // prevent compiler memory reordering 40 | ((TM3CNT_L.read() as u32) << 16) | TM2CNT_L.read() as u32 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rt0.s: -------------------------------------------------------------------------------- 1 | @ linker entry point 2 | .global __start 3 | 4 | @ The linker script places this at the start of the rom. 5 | .arm 6 | __start: b init 7 | @ this is replaced with correct header info by `gbafix` 8 | @ note that for mGBA usage gbafix is not required. 9 | .space (192-4) 10 | @ 11 | 12 | @ Here we do startup housekeeping to prep for calling Rust 13 | init: 14 | @ We boot in Supervisor mode. 15 | @ There's little use for this mode right now, 16 | @ set System mode. 17 | mov r0, #0b11111 18 | msr CPSR_c, r0 19 | 20 | @ copy .data section (if any) to IWRAM 21 | ldr r0, =__data_rom_start 22 | ldr r1, =__data_iwram_start 23 | ldr r2, =__data_iwram_end 24 | subs r2, r1 @ r2 = data_iwram length (in bytes) 25 | addne r2, #3 @ round up 26 | lsrne r2, #2 @ convert r2 to the length in words 27 | @ addne r2, #(1<<26) @ set "words" flag 28 | @ swine 0xB0000 @ call bios::CpuSet 29 | swine 0xC0000 @ Call bios::CpuFastSet 30 | 31 | @ copy .ewram section (if any) to EWRAM 32 | ldr r0, =__ewram_rom_start 33 | ldr r1, =__ewram_start 34 | ldr r2, =__ewram_end 35 | subs r2, r1 @ r2 = ewram length (in bytes) 36 | addne r2, #3 @ round up 37 | lsrne r2, #2 @ convert r2 to the length in words 38 | swine 0xC0000 @ Call bios::CpuFastSet 39 | 40 | @ startup complete, branch-exchange to `main` 41 | ldr r0, =main 42 | bx r0 43 | 44 | @ `main` should never return, loop if it does. 45 | 1: b 1b 46 | @ 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flac-demo" 3 | version = "0.1.0" 4 | authors = ["lifning"] 5 | edition = "2018" 6 | 7 | [workspace] 8 | 9 | [profile.release] 10 | lto = true 11 | # we prob don't want "z" because that turns off loop vectorization (`stmia`, right?) 12 | # 3 might not account for slow ROM accesses generally meaning more code = slower 13 | opt-level = 3 14 | panic = "abort" 15 | incremental = false 16 | codegen-units = 1 17 | overflow-checks = false 18 | 19 | [profile.dev] 20 | opt-level = 2 21 | overflow-checks = false 22 | 23 | [profile.dev.package."*"] 24 | opt-level = 3 25 | incremental = false 26 | codegen-units = 1 27 | 28 | #[package.metadata.cargo-xbuild] 29 | #memcpy = false 30 | 31 | [build-dependencies] 32 | bindgen = "^0.53" 33 | flowergal-buildtools = { path = "internal/flowergal-buildtools" } 34 | 35 | [dependencies] 36 | gba = { path = "external/gba" } 37 | voladdress = "0.2" # version must match gba's 38 | flowergal-runtime = { path = "internal/flowergal-runtime" } 39 | flowergal-proj-config = { path = "internal/flowergal-proj-config" } 40 | flowergal-proj-assets = { path = "internal/flowergal-proj-assets" } 41 | heapless = { version = "0.5", default-features = false } 42 | bstr = { version = "0.2", default-features = false } 43 | 44 | [features] 45 | bench_audio = [ "flowergal-runtime/bench_audio" ] 46 | bench_video = [ "flowergal-runtime/bench_video" ] 47 | bench_flac = [ "flowergal-runtime/bench_flac" ] 48 | verify_asm = [ "flowergal-runtime/verify_asm" ] 49 | debug_bitbuffer = [ "flowergal-runtime/debug_bitbuffer" ] 50 | supercard = [ "flowergal-runtime/supercard" ] 51 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/user_interface.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::error::Error; 4 | use std::path::Path; 5 | 6 | use sdl2::image::LoadSurface; 7 | use sdl2::surface::Surface; 8 | 9 | use build_const::ConstWriter; 10 | 11 | use crate::tile_generation; 12 | use flowergal_proj_config::resources::TilePatterns; 13 | 14 | const ASSET_DIR: &str = "../../assets/gfx"; 15 | const ASSETS: &[&str] = &["hud.png", "font.png", "textbox.png"]; 16 | 17 | pub fn convert_assets() -> Result<(), Box> { 18 | let _sdl_context = sdl2::init()?; 19 | let _image_context = sdl2::image::init(sdl2::image::InitFlag::PNG)?; 20 | 21 | let mut bc_out = ConstWriter::for_build("ui_gfx_bc")?.finish_dependencies(); 22 | 23 | let mut surfs = Vec::new(); 24 | for asset_name in ASSETS { 25 | let surf_path = Path::new(ASSET_DIR).join(asset_name); 26 | println!("cargo:rerun-if-changed={}", surf_path.to_string_lossy()); 27 | surfs.push(Surface::from_file(&surf_path)?); 28 | } 29 | 30 | // TODO: eventually keep track of multi-palette / flip reduction etc.? not necessary yet 31 | let (_grids, bank) = tile_generation::process_basic_tilesets(surfs, 16)?; 32 | 33 | if let TilePatterns::Text(img) = bank.gba_patterns(false) { 34 | let pal = bank.gba_palette_full(); 35 | bc_out.add_array("UI_PAL", "Color", &pal.data()); 36 | bc_out.add_array("UI_IMG", "Tile4bpp", img); 37 | } else { 38 | return Err("Found UI assets in 8bpp format, unsupported".into()); 39 | } 40 | 41 | bc_out.finish(); 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | !!! 2 | [NOTE: the crate at internal/flowergal-buildtools is ACSL-1.4] 3 | !!! 4 | 5 | BSD 3-Clause License 6 | 7 | Copyright (c) 2021, lifning. 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | 3. Neither the name of the copyright holder nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/COPYING: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, lifning. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | Additional license terms for simple_flac.rs may be found in COPYING-simpleflac. 32 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/COPYING: -------------------------------------------------------------------------------- 1 | The following applies to the FlowerGAL Build Tools, *not* the Runtime (nor any games made with it). 2 | 3 | ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) 4 | 5 | Copyright (C) 2021 lifning 6 | 7 | This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. 8 | 9 | Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions: 10 | 11 | 1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software. 12 | 13 | 2. The User is one of the following: 14 | a. An individual person, laboring for themselves 15 | b. A non-profit organization 16 | c. An educational institution 17 | d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor 18 | 19 | 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. 20 | 21 | 4. If the User is an organization, then the User is not law enforcement or military, or working for or under either. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/interrupt_service.rs: -------------------------------------------------------------------------------- 1 | use crate::Driver; 2 | use gba::io::display::{DisplayStatusSetting, DISPSTAT}; 3 | use gba::io::irq::{IrqEnableSetting, IrqFlags, BIOS_IF, IE, IF, IME, USER_IRQ_FN}; 4 | use crate::timers::GbaTimer; 5 | 6 | pub fn irq_setup() { 7 | DISPSTAT.write( 8 | DisplayStatusSetting::new() 9 | .with_vblank_irq_enable(true) 10 | //.with_hblank_irq_enable(true) 11 | .with_vcounter_irq_enable(true) 12 | .with_vcount_setting(227), 13 | ); 14 | 15 | IE.write( 16 | IrqFlags::new() 17 | .with_vblank(true) 18 | .with_vcounter(true) 19 | .with_timer1(true), 20 | ); 21 | 22 | USER_IRQ_FN.write(Some(irq_handler)); 23 | IME.write(IrqEnableSetting::IRQ_YES); 24 | 25 | warn!("Enabled interrupts"); // FIXME: load-bearing log! 26 | } 27 | 28 | #[link_section = ".iwram"] 29 | #[instruction_set(arm::a32)] 30 | fn irq_handler() { 31 | let flags = IF.read(); 32 | //let mut handled = IF.read(); 33 | 34 | let driver = unsafe { Driver::instance_mut() }; 35 | 36 | if flags.vblank() { 37 | driver.audio().dsound_vblank(); 38 | // driver.audio().mixer(); // testing performance.. seems way faster when run here?? 39 | driver.video().vblank(); 40 | //handled = handled.with_vblank(true); 41 | } 42 | /* 43 | if flags.hblank() { 44 | driver.video().hblank(VCOUNT.read()); 45 | handled = handled.with_hblank(true); 46 | } 47 | */ 48 | if flags.vcounter() { 49 | driver.video().vcounter(); 50 | //handled = handled.with_vcounter(true); 51 | } 52 | if flags.timer1() { 53 | driver.video().timer1(); 54 | GbaTimer::timer1(); 55 | } 56 | IF.write(flags); 57 | BIOS_IF.write(flags); 58 | } 59 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::error::Error; 4 | 5 | use gen_iter::GenIter; 6 | 7 | use sdl2::surface::Surface; 8 | 9 | mod image_bank; 10 | mod map; 11 | mod palette; 12 | mod pattern; 13 | mod tile_pixels; 14 | 15 | pub mod sdl_support; 16 | 17 | pub use image_bank::*; 18 | pub use map::*; 19 | pub use palette::*; 20 | pub use pattern::*; 21 | 22 | const TILE_W: usize = 8; 23 | const TILE_H: usize = 8; 24 | 25 | // generalize so we can do 24x24, etc.? 26 | fn traverse_16_grid_as_8( 27 | width_16tiles: usize, 28 | height_16tiles: usize, 29 | ) -> impl Iterator { 30 | GenIter(move || { 31 | for y in 0..height_16tiles / 2 { 32 | for x in 0..width_16tiles / 2 { 33 | for suby in &[0, 1] { 34 | for subx in &[0, 1] { 35 | yield (x * 2 + subx, y * 2 + suby); 36 | } 37 | } 38 | } 39 | } 40 | }) 41 | } 42 | 43 | /* 44 | pub fn process_maps<'a>( 45 | map_surfs: impl IntoIterator>, 46 | max_colors: usize, 47 | is_4bpp: bool, 48 | ) -> Result<(Vec, ImageBank), Box> { 49 | let mut bank = ImageBank::new(max_colors, is_4bpp); 50 | 51 | let mut metamaps = Vec::new(); 52 | 53 | for surf in map_surfs.into_iter() { 54 | metamaps.push(bank.process_world_map(&surf, false)?); 55 | } 56 | 57 | Ok((metamaps, bank)) 58 | } 59 | */ 60 | 61 | pub fn process_basic_tilesets<'a>( 62 | surfaces: impl IntoIterator>, 63 | max_colors: usize, 64 | ) -> Result<(Vec, ImageBank), Box> { 65 | let mut bank = ImageBank::new(max_colors, true); 66 | 67 | let mut grids = Vec::new(); 68 | 69 | for surf in surfaces.into_iter() { 70 | grids.push(bank.process_image_region(&surf, None, false, false, false)?); 71 | } 72 | 73 | Ok((grids, bank)) 74 | } 75 | -------------------------------------------------------------------------------- /scratch.txt: -------------------------------------------------------------------------------- 1 | fn _count_golomb_rice_quotient_asm(&mut self) -> i32 { 2 | unsafe { 3 | #[cfg(feature = "debug_bitbuffer")] debug!("before: bb {:32b} bbl {} ep {}", self.bitbuffer, self.bitbufferlen, self.encoded_position); 4 | let mut quotient = 0; 5 | asm!( 6 | "mov {encpos}, {encpos}, lsl #2", // u32 slice index to byte offset 7 | "cmp {bitbuf}, #0", 8 | "moveq {quot}, {bitbuflen}", 9 | "moveq {bitbuflen}, #0", 10 | "sub {bitbuflen}, {bitbuflen}, #1", // when this overflows, it makes the lsl underflow 11 | "mov {mask}, #1", 12 | "movs {mask}, {mask}, lsl {bitbuflen}", 13 | "add {bitbuflen}, {bitbuflen}, #1", 14 | "b 2f", 15 | "1:", 16 | "movs {mask}, {mask}, lsr #1", 17 | "2:", 18 | // if mask hits 0, we need to replenish bitbuffer 19 | "ldreq {bitbuf}, [{data}, {encpos}]", // load & endian swap 20 | "addeq {encpos}, {encpos}, #4", 21 | "eoreq {mask}, {bitbuf}, {bitbuf}, ror #16", // using {mask} as tmp 'cause we overwrite it anyway 22 | "biceq {mask}, {mask}, #0xff, 16", 23 | "moveq {bitbuf}, {bitbuf}, ror #8", 24 | "eoreq {bitbuf}, {bitbuf}, {mask}, lsr #8", 25 | "moveq {bitbuflen}, #32", 26 | "moveq {mask}, #0x80000000", 27 | // endif 28 | "sub {bitbuflen}, #1", 29 | "tst {bitbuf}, {mask}", 30 | "addeq {quot}, #1", 31 | "beq 1b", 32 | "sub {mask}, {mask}, #1", 33 | "and {bitbuf}, {bitbuf}, {mask}", 34 | "mov {encpos}, {encpos}, lsr #2", // byte offset back to u32 slice index 35 | quot = inout(reg) quotient, 36 | bitbuf = inout(reg) self.bitbuffer, 37 | bitbuflen = inout(reg) self.bitbufferlen, 38 | data = inout(reg) self.data.as_ptr() => _, 39 | encpos = inout(reg) self.encoded_position, 40 | mask = out(reg) _, 41 | options(nostack)); 42 | 43 | #[cfg(feature = "debug_bitbuffer")] debug!("after: bb {:32b} bbl {} ep {} q {}", self.bitbuffer, self.bitbufferlen, self.encoded_position, quotient); 44 | 45 | quotient 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Drivers, codecs, platform abstractions. 3 | * Eventual goal is to have game-specific stuff only where pragmatic or necessary. 4 | */ 5 | 6 | #![no_std] 7 | 8 | #![feature(asm)] 9 | #![feature(unchecked_math)] 10 | #![feature(fixed_size_array)] 11 | #![feature(array_windows)] 12 | #![feature(slice_fill_with)] 13 | #![feature(const_fn_transmute)] 14 | #![feature(const_mut_refs)] 15 | #![feature(const_in_array_repeat_expressions)] 16 | #![feature(const_fn)] 17 | #![feature(const_raw_ptr_to_usize_cast)] 18 | #![feature(const_slice_from_raw_parts)] 19 | #![feature(const_raw_ptr_deref)] 20 | #![feature(iterator_fold_self)] 21 | #![feature(associated_type_defaults)] 22 | #![feature(isa_attribute)] 23 | #![feature(fmt_as_str)] 24 | 25 | #![allow(clippy::missing_safety_doc)] 26 | 27 | #[macro_use] 28 | pub mod logging; 29 | pub mod audio; 30 | pub mod interrupt_service; 31 | pub mod memory; 32 | pub mod render; 33 | pub mod timers; 34 | 35 | use crate::audio::AudioDriver; 36 | use crate::render::GbaRenderer; 37 | use crate::timers::GbaTimer; 38 | pub use memory::{BiosCalls, MemoryOps, CoreLib}; 39 | 40 | pub struct Driver { 41 | video: render::GbaRenderer, 42 | audio: audio::AudioDriver, 43 | timer: timers::GbaTimer, 44 | _input: (), 45 | _interrupt: (), 46 | } 47 | 48 | static mut DRIVER_SINGLETON: Driver = Driver::new(); 49 | 50 | impl Driver { 51 | pub const fn new() -> Self { 52 | Driver { 53 | video: render::GbaRenderer::new(), 54 | audio: audio::AudioDriver::new(), 55 | timer: timers::GbaTimer::new(), 56 | _input: (), 57 | _interrupt: (), 58 | } 59 | } 60 | 61 | // TODO: make safe w/ locking mechanism (per subsystem?) so ISR's don't screw things up 62 | #[inline(always)] 63 | pub unsafe fn instance_mut() -> &'static mut Self { 64 | &mut DRIVER_SINGLETON 65 | } 66 | 67 | #[inline(always)] 68 | pub fn video(&mut self) -> &mut GbaRenderer { 69 | &mut self.video 70 | } 71 | 72 | #[inline(always)] 73 | pub fn audio(&mut self) -> &mut AudioDriver { 74 | &mut self.audio 75 | } 76 | 77 | #[inline(always)] 78 | pub fn timer(&mut self) -> &mut GbaTimer { 79 | &mut self.timer 80 | } 81 | 82 | pub fn initialize(&mut self) { 83 | self.video.initialize(); 84 | self.audio.initialize(); 85 | self.timer.initialize(); 86 | interrupt_service::irq_setup(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /linker_script.ld: -------------------------------------------------------------------------------- 1 | ENTRY(__start) 2 | 3 | MEMORY { 4 | ewram : ORIGIN = 0x02000000, LENGTH = 256K 5 | iwram : ORIGIN = 0x03000000, LENGTH = 32K 6 | rom : ORIGIN = 0x08000000, LENGTH = 32M 7 | } 8 | 9 | SECTIONS { 10 | .text : { 11 | KEEP(rt0.o(.text)); 12 | *(.text .text.*); 13 | . = ALIGN(4); 14 | } >rom = 0xff 15 | 16 | .rodata : { 17 | KEEP(rt0.o(.rodata)); 18 | *(.rodata .rodata.*); 19 | . = ALIGN(4); 20 | } >rom = 0xff 21 | 22 | __data_rom_start = .; 23 | .data : { 24 | __data_iwram_start = ABSOLUTE(.); 25 | KEEP(rt0.o(.data)); 26 | *(.data .data.*); 27 | *(.iwram .iwram.*); 28 | . = ALIGN(4); 29 | __data_iwram_end = ABSOLUTE(.); 30 | } >iwram AT>rom = 0xff 31 | 32 | .bss : { 33 | __bss_iwram_start = ABSOLUTE(.); 34 | KEEP(rt0.o(.bss)); 35 | *(.bss .bss.*); 36 | . = ALIGN(4); 37 | __bss_iwram_end = ABSOLUTE(.); 38 | } >iwram 39 | 40 | __ewram_rom_start = .; 41 | .ewram : { 42 | __ewram_start = ABSOLUTE(.); 43 | *(.ewram .ewram.*); 44 | . = ALIGN(4); 45 | __ewram_end = ABSOLUTE(.); 46 | } >ewram AT>rom = 0xff 47 | 48 | /* debugging sections */ 49 | /* Stabs */ 50 | .stab 0 : { *(.stab) } 51 | .stabstr 0 : { *(.stabstr) } 52 | .stab.excl 0 : { *(.stab.excl) } 53 | .stab.exclstr 0 : { *(.stab.exclstr) } 54 | .stab.index 0 : { *(.stab.index) } 55 | .stab.indexstr 0 : { *(.stab.indexstr) } 56 | .comment 0 : { *(.comment) } 57 | /* DWARF 1 */ 58 | .debug 0 : { *(.debug) } 59 | .line 0 : { *(.line) } 60 | /* GNU DWARF 1 extensions */ 61 | .debug_srcinfo 0 : { *(.debug_srcinfo) } 62 | .debug_sfnames 0 : { *(.debug_sfnames) } 63 | /* DWARF 1.1 and DWARF 2 */ 64 | .debug_aranges 0 : { *(.debug_aranges) } 65 | .debug_pubnames 0 : { *(.debug_pubnames) } 66 | /* DWARF 2 */ 67 | .debug_info 0 : { *(.debug_info) } 68 | .debug_abbrev 0 : { *(.debug_abbrev) } 69 | .debug_line 0 : { *(.debug_line) } 70 | .debug_frame 0 : { *(.debug_frame) } 71 | .debug_str 0 : { *(.debug_str) } 72 | .debug_loc 0 : { *(.debug_loc) } 73 | .debug_macinfo 0 : { *(.debug_macinfo) } 74 | /* SGI/MIPS DWARF 2 extensions */ 75 | .debug_weaknames 0 : { *(.debug_weaknames) } 76 | .debug_funcnames 0 : { *(.debug_funcnames) } 77 | .debug_typenames 0 : { *(.debug_typenames) } 78 | .debug_varnames 0 : { *(.debug_varnames) } 79 | .got.plt 0 : { *(.got.plt) } 80 | 81 | /* discard anything not already mentioned */ 82 | /DISCARD/ : { *(*) } 83 | } 84 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/sound_info.rs: -------------------------------------------------------------------------------- 1 | /* https://deku.gbadev.org/program/sound1.html#buffer 2 | REG_TM0D frequency 3 | | | 4 | v v 5 | Timer = 62610 = 65536 - (16777216 / 5734), buf = 96 6 | Timer = 63408 = 65536 - (16777216 / 7884), buf = 132 <- the only buf that's not x8 7 | Timer = 63940 = 65536 - (16777216 / 10512), buf = 176 8 | Timer = 64282 = 65536 - (16777216 / 13379), buf = 224 9 | Timer = 64612 = 65536 - (16777216 / 18157), buf = 304 10 | Timer = 64738 = 65536 - (16777216 / 21024), buf = 352 11 | Timer = 64909 = 65536 - (16777216 / 26758), buf = 448 12 | Timer = 65004 = 65536 - (16777216 / 31536), buf = 528 13 | Timer = 65073 = 65536 - (16777216 / 36314), buf = 608 14 | Timer = 65118 = 65536 - (16777216 / 40137), buf = 672 15 | Timer = 65137 = 65536 - (16777216 / 42048), buf = 704 16 | */ 17 | 18 | /// must be one of 5734, 7884, 10512, 13379, 18157, 21024, 26758, 31536, 36314, 40137, 42048 19 | /// such that samples get divided evenly into vblank rate and cpu frequency 20 | pub const SAMPLE_RATE: u16 = 31536; 21 | 22 | pub const CPU_FREQ: u32 = 16777216; 23 | pub const CYCLES_PER_FRAME: u32 = 280896; // = 16777216 Hz / 59.7275 FPS 24 | /// this is the number of samples per frame. 25 | pub const PLAYBUF_SIZE: usize = 26 | (((CYCLES_PER_FRAME as f32 * SAMPLE_RATE as f32) / CPU_FREQ as f32) + 0.5) as usize; 27 | pub const SAMPLE_TIME: u32 = CYCLES_PER_FRAME / PLAYBUF_SIZE as u32; // = (16777216 / SAMPLE_RATE) 28 | pub const TIMER_VALUE: u16 = (0x10000 - SAMPLE_TIME) as u16; 29 | 30 | // must be a multiple of 8 for our handwritten ASM routines to work 31 | const_assert_eq!(PLAYBUF_SIZE & 0x7, 0); 32 | 33 | #[cfg_attr(not(target_arch = "arm"), derive(Clone))] 34 | pub struct TrackList(pub &'static [MusicId]); 35 | 36 | /// music index in the DEBUG jukebox 37 | #[repr(usize)] 38 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 39 | #[derive(Copy, Clone)] 40 | pub enum MusicId { 41 | TomsDiner, 42 | } 43 | 44 | pub const SONG_FILES: &[&str] = &[ 45 | "Tom's Diner [Long Version] DNA feat. Suzanne Vega (1990)-32ZTjFW2RYo.mkv", 46 | ]; 47 | 48 | /// sfx index in the jukebox 49 | #[allow(non_camel_case_types)] 50 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 51 | #[derive(Copy, Clone)] 52 | pub enum SfxId {} 53 | 54 | pub const SFX_FILES: &[&str] = &[]; 55 | 56 | #[cfg(not(target_arch = "arm"))] 57 | mod impl_debug_for_build_const { 58 | use super::*; 59 | use core::fmt::{Debug, Formatter, Result}; 60 | 61 | impl Debug for TrackList { 62 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 63 | write!(f, "TrackList(&[")?; 64 | for id in self.0 { 65 | write!(f, "MusicId::{:?}, ", id)?; 66 | } 67 | write!(f, "])") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/sdl_support.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::error::Error; 4 | 5 | use sdl2::pixels::Color as sdl_Color; 6 | use sdl2::pixels::PixelFormatEnum; 7 | use sdl2::rect::Rect; 8 | use sdl2::surface::Surface; 9 | 10 | use crate::tile_generation::{TILE_H, TILE_W}; 11 | 12 | pub fn pixels_of_rect_32bit( 13 | surf: &Surface, 14 | rect: Rect, 15 | solid_bg: Option, 16 | ) -> Result, Box> { 17 | let (width, height) = rect.size(); 18 | let dst_rect = Rect::new(0, 0, width, height); 19 | 20 | let mut dst = Surface::new(width, height, PixelFormatEnum::RGBA8888)?; 21 | assert_eq!(dst.pixel_format_enum(), PixelFormatEnum::RGBA8888); 22 | if let Some(c) = solid_bg { 23 | dst.fill_rect(dst_rect, c)?; 24 | } 25 | surf.blit(rect, &mut dst, dst_rect)?; 26 | let pitch = dst.pitch(); 27 | let pix_fmt = dst.pixel_format(); 28 | 29 | let color_vec = dst.with_lock(|pixels_raw| { 30 | let mut pixels_vec = Vec::new(); 31 | for y in 0..height { 32 | let subslice = unsafe { 33 | core::slice::from_raw_parts( 34 | (pixels_raw.as_ptr().offset((pitch * y) as isize)) as *const u32, 35 | width as usize, 36 | ) 37 | }; 38 | pixels_vec.extend_from_slice(subslice); 39 | } 40 | pixels_vec 41 | .into_iter() 42 | .map(|c| sdl_Color::from_u32(&pix_fmt, c)) 43 | .collect() 44 | }); 45 | Ok(color_vec) 46 | } 47 | 48 | pub fn pixels_of_rect_16bit(surf: &Surface, rect: Rect) -> Result, Box> { 49 | Ok(sdl_to_gba_colors(pixels_of_rect_32bit(surf, rect, None)?)) 50 | } 51 | 52 | pub fn sdl_to_gba_colors(sdl_p: impl IntoIterator) -> Vec { 53 | sdl_p 54 | .into_iter() 55 | .map(|c| { 56 | // GBA is XBGR1555 57 | let r = c.r as u16 >> 3; 58 | let g = c.g as u16 >> 3; 59 | let b = c.b as u16 >> 3; 60 | // GBA ignores highest bit, but we use it during our own processing 61 | let a = ((c.a > 127) as u16) << 15; 62 | gba::Color(gba::Color::from_rgb(r, g, b).0 | a) 63 | }) 64 | .collect() 65 | } 66 | 67 | pub fn pixels_of_tile( 68 | surf: &Surface, 69 | tx: usize, 70 | ty: usize, 71 | ) -> Result, Box> { 72 | let src_rect = Rect::new( 73 | (tx * TILE_W) as i32, 74 | (ty * TILE_H) as i32, 75 | TILE_W as u32, 76 | TILE_H as u32, 77 | ); 78 | let pixels = pixels_of_rect_16bit(&surf, src_rect)?; 79 | assert_eq!(pixels.len(), TILE_W * TILE_H); 80 | Ok(pixels) 81 | } 82 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/compression.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use flowergal_proj_config::resources::{RoomData, TextScreenblockEntry, TilePatterns, ROOM_SIZE}; 4 | use std::error::Error; 5 | use std::mem::size_of; 6 | use std::slice::from_raw_parts; 7 | 8 | pub trait CompressibleAsset: Sized { 9 | fn compress(self) -> Result>; 10 | } 11 | 12 | #[allow(clippy::size_of_in_element_count)] 13 | pub fn do_lz77_compression(input: &[T], vram_safe: bool) -> Result<&'static [u32], Box> { 14 | let input = 15 | unsafe { from_raw_parts(input.as_ptr() as *const u8, input.len() * size_of::()) }; 16 | let mut data = gba_compression::bios::compress_lz77(input, vram_safe)?; 17 | while data.len() & 3 != 0 { 18 | data.push(0); 19 | } 20 | let leaked_data = Box::leak(data.into_boxed_slice()); 21 | let aligned_leaked_data = unsafe { 22 | from_raw_parts( 23 | leaked_data.as_ptr() as *const u32, 24 | leaked_data.len() / size_of::(), 25 | ) 26 | }; 27 | Ok(aligned_leaked_data) 28 | } 29 | 30 | impl CompressibleAsset for TilePatterns { 31 | fn compress(self) -> Result> { 32 | match self { 33 | TilePatterns::Text(tiles) => { 34 | Ok(TilePatterns::TextLz77(do_lz77_compression(tiles, true)?)) 35 | } 36 | TilePatterns::Affine(tiles) => { 37 | Ok(TilePatterns::AffineLz77(do_lz77_compression(tiles, true)?)) 38 | } 39 | x => Ok(x), 40 | } 41 | } 42 | } 43 | 44 | impl CompressibleAsset for RoomData { 45 | fn compress(self) -> Result> { 46 | match self { 47 | RoomData::Text(rooms) => { 48 | let mut compressed_rooms = Vec::<&'static [u32]>::new(); 49 | for room in rooms { 50 | let mut padded = Vec::new(); 51 | for (i, row) in room.0.iter().enumerate() { 52 | for entry in row.iter() { 53 | padded.push(*entry); 54 | } 55 | if i < ROOM_SIZE.1 - 1 { 56 | for _ in (ROOM_SIZE.0)..32 { 57 | padded.push(TextScreenblockEntry::new()); 58 | } 59 | } 60 | } 61 | compressed_rooms.push(do_lz77_compression(&padded, true)?); 62 | } 63 | Ok(RoomData::TextLz77(Box::leak( 64 | compressed_rooms.into_boxed_slice(), 65 | ))) 66 | } 67 | // TODO: support. for now, passthrough is OK 68 | //RoomData::Affine(_) => Err("compressing affine RoomData not yet supported".into()), 69 | x => Ok(x), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/resources/blend.rs: -------------------------------------------------------------------------------- 1 | /// "a" is base layer, "b" is top layer 2 | /// special thanks to https://en.wikipedia.org/wiki/Blend_modes#Overlay for existing, 3 | /// as well as https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending 4 | 5 | #[cfg(not(target_arch = "arm"))] 6 | pub mod float { 7 | type Rgb888 = (u8, u8, u8); 8 | 9 | fn gba_channel_to_float(c: u16) -> f64 { 10 | c as f64 / 31.0 11 | } 12 | 13 | fn rgb888_channel_to_float(c: u8) -> f64 { 14 | c as f64 / 255.0 15 | } 16 | 17 | pub fn float_to_gba_channel(f: f64) -> u16 { 18 | let c = (f * 31.0).round() as u16; 19 | c.min(31) 20 | } 21 | 22 | pub fn gba_color_to_float(c: gba::Color) -> (f64, f64, f64) { 23 | ( 24 | gba_channel_to_float(c.red()), 25 | gba_channel_to_float(c.green()), 26 | gba_channel_to_float(c.blue()), 27 | ) 28 | } 29 | 30 | fn rgb888_color_to_float((r, g, b): Rgb888) -> (f64, f64, f64) { 31 | ( 32 | rgb888_channel_to_float(r), 33 | rgb888_channel_to_float(g), 34 | rgb888_channel_to_float(b), 35 | ) 36 | } 37 | 38 | pub fn blend_multiply(a: gba::Color, b: Rgb888) -> gba::Color { 39 | let (a_red, a_green, a_blue) = gba_color_to_float(a); 40 | let (b_red, b_green, b_blue) = rgb888_color_to_float(b); 41 | gba::Color::from_rgb( 42 | float_to_gba_channel(a_red * b_red), 43 | float_to_gba_channel(a_green * b_green), 44 | float_to_gba_channel(a_blue * b_blue), 45 | ) 46 | } 47 | 48 | pub fn channel_overlay(a: f64, b: f64) -> u16 { 49 | if a < 0.5 { 50 | float_to_gba_channel(2.0 * a * b) 51 | } else { 52 | float_to_gba_channel(1.0 - (2.0 * (1.0 - a) * (1.0 - b))) 53 | } 54 | } 55 | 56 | pub fn channel_alpha(a: f64, b: f64, alpha: f64) -> u16 { 57 | float_to_gba_channel((b * alpha) + (a * (1.0 - alpha))) 58 | } 59 | 60 | pub fn blend_overlay(a: gba::Color, b: Rgb888) -> gba::Color { 61 | let (a_red, a_green, a_blue) = gba_color_to_float(a); 62 | let (b_red, b_green, b_blue) = rgb888_color_to_float(b); 63 | gba::Color::from_rgb( 64 | channel_overlay(a_red, b_red), 65 | channel_overlay(a_green, b_green), 66 | channel_overlay(a_blue, b_blue), 67 | ) 68 | } 69 | 70 | pub fn blend_hardlight(a: gba::Color, b: Rgb888) -> gba::Color { 71 | let (a_red, a_green, a_blue) = gba_color_to_float(a); 72 | let (b_red, b_green, b_blue) = rgb888_color_to_float(b); 73 | gba::Color::from_rgb( 74 | // swapped b and a 75 | channel_overlay(b_red, a_red), 76 | channel_overlay(b_green, a_green), 77 | channel_overlay(b_blue, a_blue), 78 | ) 79 | } 80 | 81 | pub fn blend_alpha(a: gba::Color, b: Rgb888, alpha: f64) -> gba::Color { 82 | let (a_red, a_green, a_blue) = gba_color_to_float(a); 83 | let (b_red, b_green, b_blue) = rgb888_color_to_float(b); 84 | gba::Color::from_rgb( 85 | // swapped b and a 86 | channel_alpha(a_red, b_red, alpha), 87 | channel_alpha(a_green, b_green, alpha), 88 | channel_alpha(a_blue, b_blue, alpha), 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/audio/raw_pcm.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::PlayableSound; 2 | 3 | pub struct RawPcm8 { 4 | pub(crate) data: &'static [u8], 5 | /// decode position in 8-bit samples 6 | decode_position: usize, 7 | sample_count: usize, 8 | looping: bool, 9 | } 10 | 11 | impl RawPcm8 { 12 | pub const fn new(data: &'static [u8], looping: bool) -> Self { 13 | RawPcm8 { 14 | data, 15 | decode_position: 0, 16 | sample_count: data.len(), 17 | looping, 18 | } 19 | } 20 | } 21 | 22 | impl PlayableSound for RawPcm8 { 23 | #[link_section = ".iwram"] 24 | fn mix_into(&mut self, mixbuf: &mut [i32]) { 25 | let mut remaining = self.remaining_samples(); 26 | if remaining == 0 { 27 | if self.looping() { 28 | self.reset(); 29 | remaining = self.remaining_samples(); 30 | } else { 31 | return; 32 | } 33 | } 34 | let to_decode = mixbuf.len().min(remaining); 35 | for i in 0..(to_decode / 8) { 36 | unsafe { 37 | asm!( 38 | "ldmia r12, {{r0-r1}}", // load eight 8-bit samples 39 | "ldmia {mix}, {{r2-r5, r7-r10}}", // mixing with eight 32-bit samples 40 | "and r12, r0, #0xff", 41 | "add r2, r2, r12, lsl #8", 42 | "and r12, r0, #0xff00", 43 | "add r3, r3, r12", 44 | "and r12, r0, #0xff0000", 45 | "add r4, r4, r12, lsr #8", 46 | "and r12, r0, #0xff000000", 47 | "add r5, r5, r12, lsr #16", 48 | "and r12, r1, #0xff", 49 | "add r7, r7, r12, lsl #8", 50 | "and r12, r1, #0xff00", 51 | "add r8, r8, r12", 52 | "and r12, r1, #0xff0000", 53 | "add r9, r9, r12, lsr #8", 54 | "and r12, r1, #0xff000000", 55 | "add r10, r10, r12, lsr #16", 56 | "stmia {mix}, {{r2-r5, r7-r10}}", // write back eight 32-bit samples 57 | mix = inout(reg) mixbuf.as_ptr().add(i * 8) => _, 58 | // we reuse r12 for scratch space 59 | inout("r12") self.data.as_ptr().add(i * 8 + self.decode_position) => _, 60 | out("r0") _, 61 | out("r1") _, 62 | out("r2") _, 63 | out("r3") _, 64 | out("r4") _, 65 | out("r5") _, 66 | out("r7") _, 67 | out("r8") _, 68 | out("r9") _, 69 | out("r10") _, 70 | options(nostack)); 71 | } 72 | } 73 | self.decode_position += to_decode; 74 | /* 75 | let len = mixbuf.len().min(remaining); 76 | for mb in mixbuf[0..len].iter_mut() { 77 | *mb += self.decode_sample() as i32; 78 | } 79 | if len == remaining && self.looping() { 80 | // chance to loop a song 81 | self.mix_into(&mut mixbuf[len..]); 82 | } 83 | */ 84 | } 85 | 86 | fn remaining_samples(&self) -> usize { 87 | self.sample_count - self.decode_position 88 | } 89 | 90 | fn looping(&self) -> bool { 91 | self.looping 92 | } 93 | 94 | fn data_ptr(&self) -> *const u8 { 95 | self.data.as_ptr() as *const u8 96 | } 97 | 98 | fn reset(&mut self) { 99 | self.decode_position = 0; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(asm)] 4 | #![feature(panic_info_message)] 5 | #![feature(const_fn_transmute)] 6 | #![feature(const_in_array_repeat_expressions)] 7 | #![feature(const_fn)] 8 | 9 | #![allow(clippy::new_without_default)] 10 | 11 | #[macro_use] 12 | extern crate flowergal_runtime; 13 | 14 | pub mod hud; 15 | pub mod world; 16 | 17 | use core::fmt::Write; 18 | 19 | use gba::io::keypad::KeyInput; 20 | use gba::mgba::MGBADebugLevel; 21 | 22 | use bstr::ByteSlice; 23 | use heapless::consts::U80; 24 | 25 | use flowergal_runtime::Driver; 26 | use flowergal_proj_config::sound_info::{SAMPLE_RATE, CYCLES_PER_FRAME}; 27 | 28 | static mut G_HUD: Option = None; 29 | 30 | #[panic_handler] 31 | fn panic(info: &core::panic::PanicInfo) -> ! { 32 | if let Some(mut mgba) = gba::mgba::MGBADebug::new() { 33 | let _ = write!(mgba, "{}", info); 34 | mgba.send(MGBADebugLevel::Fatal); 35 | } else { 36 | unsafe { 37 | Driver::instance_mut().video().set_textbox_shown(true); 38 | if let Some(h) = &mut G_HUD { 39 | let mut buf: heapless::Vec = heapless::Vec::new(); 40 | let _ = write!(buf, "{}", info); 41 | h.clear_text_area(); 42 | h.draw_text(buf.to_str_unchecked()); 43 | } 44 | } 45 | } 46 | unsafe { Driver::instance_mut().audio().disable() }; 47 | loop { 48 | gba::bios::vblank_interrupt_wait(); 49 | } 50 | } 51 | 52 | #[no_mangle] 53 | fn main() -> ! { 54 | debug!("Initializing"); 55 | 56 | let driver = unsafe { Driver::instance_mut() }; 57 | driver.initialize(); 58 | 59 | debug!("Initialized. Drawing HUD..."); 60 | 61 | let h = unsafe { 62 | G_HUD.replace(hud::Hud::new()); 63 | G_HUD.as_mut().unwrap() 64 | }; 65 | 66 | h.draw_background(); 67 | 68 | debug!("HUD drawn. Loading World..."); 69 | 70 | let mut w = world::World::new(); 71 | w.load_world(&flowergal_proj_assets::TOMS_DINER_DATA); 72 | w.draw_room(0, 0); 73 | 74 | info!("World loaded."); 75 | 76 | let mut text_showing = true; 77 | driver.video().set_textbox_shown(text_showing); 78 | 79 | let mut prev_keys = KeyInput::new(); 80 | loop { 81 | let cur_keys = gba::io::keypad::read_key_input(); 82 | let new_keys = cur_keys.pressed_since(prev_keys); 83 | 84 | if new_keys.a() { 85 | text_showing = !text_showing; 86 | driver.video().set_textbox_shown(text_showing); 87 | h.clear_text_area(); 88 | } 89 | 90 | if text_showing { 91 | let mut buf: heapless::Vec = heapless::Vec::new(); 92 | let dec = driver.audio().ticks_decode * 100 / (CYCLES_PER_FRAME / 64); 93 | let mix = driver.audio().ticks_unmix * 100 / (CYCLES_PER_FRAME / 64); 94 | let _ = write!( 95 | buf, 96 | "9-bit FLAC @ {}Hz\n\ 97 | CPU: {:2}% dec,{:2}% mix\n\ 98 | Rust+ASM by lifning", 99 | SAMPLE_RATE, 100 | dec.min(99), // formatting gets screwed on GBARunner2 101 | mix.min(99)); 102 | h.clear_text_area(); 103 | h.draw_text(unsafe { buf.to_str_unchecked() }); 104 | } 105 | 106 | if new_keys.select() { 107 | if let Some(mut mgba) = gba::mgba::MGBADebug::new() { 108 | for s in flowergal_proj_assets::LICENSE_TEXT.lines() { 109 | let _ = mgba.write_str(s); 110 | mgba.send(MGBADebugLevel::Info); 111 | } 112 | } 113 | } 114 | 115 | prev_keys = cur_keys; 116 | w.advance_frame(); 117 | h.draw_borders(text_showing); 118 | 119 | gba::bios::vblank_interrupt_wait(); 120 | 121 | driver.audio().mixer(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/logging.rs: -------------------------------------------------------------------------------- 1 | /** 2 | * gba crate has its own logging macros, but I wanted a little more info in it 3 | * & compiling out lower-level statements in release 4 | */ 5 | use core::fmt::{Arguments, Write}; 6 | use gba::mgba::{MGBADebug, MGBADebugLevel}; 7 | 8 | // FIXME: load-bearing logs in some places due to optimization errors, can't disable some of these... 9 | #[cfg(not(debug_assertions))] 10 | pub const STATIC_MAX_LEVEL: MGBADebugLevel = MGBADebugLevel::Info; 11 | #[cfg(debug_assertions)] 12 | pub const STATIC_MAX_LEVEL: MGBADebugLevel = MGBADebugLevel::Debug; 13 | 14 | pub const STATIC_LEVEL_NAMES: [&str; 5] = ["F", "E", "W", "I", "D"]; 15 | 16 | #[instruction_set(arm::t32)] 17 | pub fn internal_write_log(mgba: &mut MGBADebug, args: Arguments) { 18 | if let Some(s) = args.as_str() { 19 | let _ = mgba.write_str(s); 20 | } else { 21 | let _ = mgba.write_fmt(args); 22 | } 23 | } 24 | 25 | #[macro_export] 26 | macro_rules! log { 27 | (target: $target:expr, $lvl:expr, $message:expr) => ({ 28 | let lvl = $lvl; 29 | if lvl as u16 <= $crate::logging::STATIC_MAX_LEVEL as u16 { 30 | if let Some(mut mgba) = gba::mgba::MGBADebug::new() { 31 | $crate::logging::internal_write_log(&mut mgba, format_args!( 32 | "[{}] ({}) {}", 33 | $crate::logging::STATIC_LEVEL_NAMES[lvl as usize], 34 | $target, 35 | $message 36 | )); 37 | mgba.send(lvl); 38 | } 39 | } 40 | }); 41 | (target: $target:expr, $lvl:expr, $($arg:tt)+) => ({ 42 | let lvl = $lvl; 43 | if lvl as u16 <= $crate::logging::STATIC_MAX_LEVEL as u16 { 44 | if let Some(mut mgba) = gba::mgba::MGBADebug::new() { 45 | $crate::logging::internal_write_log(&mut mgba, format_args!( 46 | "[{}] ({}) ", 47 | $crate::logging::STATIC_LEVEL_NAMES[lvl as usize], 48 | $target 49 | )); 50 | $crate::logging::internal_write_log(&mut mgba, format_args!($($arg)+)); 51 | mgba.send(lvl); 52 | } 53 | } 54 | }); 55 | ($lvl:expr, $($arg:tt)+) => ($crate::log!(target: module_path!(), $lvl, $($arg)+)) 56 | } 57 | 58 | #[macro_export] 59 | macro_rules! debug { 60 | (target: $target:expr, $($arg:tt)+) => ( 61 | $crate::log!(target: $target, gba::mgba::MGBADebugLevel::Debug, $($arg)+); 62 | ); 63 | ($($arg:tt)+) => ( 64 | $crate::log!(gba::mgba::MGBADebugLevel::Debug, $($arg)+); 65 | ) 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! info { 70 | (target: $target:expr, $($arg:tt)+) => ( 71 | $crate::log!(target: $target, gba::mgba::MGBADebugLevel::Info, $($arg)+); 72 | ); 73 | ($($arg:tt)+) => ( 74 | $crate::log!(gba::mgba::MGBADebugLevel::Info, $($arg)+); 75 | ) 76 | } 77 | 78 | #[macro_export] 79 | macro_rules! warn { 80 | (target: $target:expr, $($arg:tt)+) => ( 81 | $crate::log!(target: $target, gba::mgba::MGBADebugLevel::Warning, $($arg)+); 82 | ); 83 | ($($arg:tt)+) => ( 84 | $crate::log!(gba::mgba::MGBADebugLevel::Warning, $($arg)+); 85 | ) 86 | } 87 | 88 | #[macro_export] 89 | macro_rules! error { 90 | (target: $target:expr, $($arg:tt)+) => ( 91 | $crate::log!(target: $target, gba::mgba::MGBADebugLevel::Error, $($arg)+); 92 | ); 93 | ($($arg:tt)+) => ( 94 | $crate::log!(gba::mgba::MGBADebugLevel::Error, $($arg)+); 95 | ) 96 | } 97 | 98 | #[macro_export] 99 | macro_rules! fatal { 100 | (target: $target:expr, $($arg:tt)+) => ( 101 | $crate::log!(target: $target, gba::mgba::MGBADebugLevel::Fatal, $($arg)+); 102 | panic!(); 103 | ); 104 | ($($arg:tt)+) => ( 105 | $crate::log!(gba::mgba::MGBADebugLevel::Fatal, $($arg)+); 106 | panic!(); 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GBA FLAC Demo 2 | ---- 3 | 4 | [![Screenshot](https://github.com/lifning/gba-flac-demo/raw/readme-assets/suzanne_ve.gba_preview.jpg)
Video preview](https://github.com/lifning/gba-flac-demo/raw/readme-assets/suzanne_ve.gba_preview.mp4) 5 | 6 | ## Usage notes 7 | 8 | If you run this in something other than a hardware GBA, DS, or 3DS+open_gba_firm, be sure to enable the **interframe blending** feature in the settings, to simulate the LCD ghosting that occurs on the portables' hardware, which this demo requires for its visual effects. 9 | - In the official GameBoy Player Start-up Disc, that's `Z Button : Options`|`Sharpness`|`Soft` 10 | - In GameBoyInterface, that's `--filter=blend` in the .dol+cli arguments. 11 | - In mGBA, that's `Audio/Video`|:heavy_check_mark:`Interframe blending`. You may also need to **disable** `Tools`|`Settings...`|`BIOS`|:white_large_square:`Use BIOS file if found` to avoid a crash whose cause I have yet to determine. Press the Select button after enabling Info logging (`Tools`|`View logs...`|:ballot_box_with_check:`Info`) to view licenses of third-party runtime dependency crates which require a copyright message to be reproduced in binary distributions. 12 | - In VisualBoyAdvance-M, that's `Options`|`Video`|`Change interframe blending`; select that option until the status bar (`Options`|`Video`|`Status bar`) says "Using interframe blending #2". Note that despite having a workaround baked into the demo to prevent flickering, there will still be some visible inaccuracy in the textbox rendering. 13 | - No$GBA and NanoboyAdvance do not seem to have an interframe blending feature, but otherwise render correctly. 14 | - While normally an accurate emulator, I currently can't recommend higan for this demo in particular, as it struggles with rendering the scanline effects properly (without flickering), and I don't yet have a way of detecting when the demo is running in higan to enable workarounds. But for completeness / in case the problem gets fixed after this writing, it's `Settings`|`Video...`|:ballot_box_with_check:`Interframe Blending` 15 | 16 | ## Caveat for developers 17 | 18 | The quality of a lot of the code here isn't what I'd call production-grade, or even idiomatic Rust; this was primarily a demo thrown together to demonstrate to myself that Rust was viable at all for targetting GBA hardware with a nontrivial workload (that is, more than just [drawing three pixels to a framebuffer](https://www.coranac.com/tonc/text/first.htm)). There were already growing pains in the codebase by the time I finished this (particularly the mutable static global used for interfacing with the hardware in ways that completely neglect a lot of what Rust brings to the table in terms of Fearless Concurrency:tm:) 19 | 20 | ## Development setup 21 | 22 | Either clone this repo with `git clone --recurse-submodules` or use `git submodule update --init --recursive` to get all the dependencies. 23 | 24 | Install `youtube-dl`, `clang++`, `auto{conf,make}`, `libtool`, `pkg-config`, `gettext`, `ffmpeg`, `mgba-qt`, `SDL2-devel`, `SDL2_image-devel`, and `arm-none-eabi-{as,gcc,ld,objcopy}` wherever Unix packages are sold. (If a cross-compile GCC toolchain for `arm-none-eabi` isn't packaged for your distribution, you may choose to simply use the one included in devkitARM from devkitPro, but devkitPro is not *required*) 25 | 26 | You'll need at least the nightly-2021-01-15 (or so) Rust toolchain. 27 | ```sh 28 | rustup toolchain install nightly 29 | rustup component add --toolchain nightly rust-src 30 | ``` 31 | 32 | If you're on Windows, you'll probably want to try doing all this in WSL or LxSS or whatever they're calling it these days. [It might work!](https://ld-linux.so/) 33 | 34 | ## Build and run 35 | ```sh 36 | cargo run --release 37 | ``` 38 | 39 | ## Make ROM for hardware 40 | 41 | (Or just for non-development-oriented emulators) 42 | 43 | You'll need `gbafix` from either cargo (`cargo install gbafix`) or devkitPro. 44 | 45 | ```sh 46 | make 47 | ``` 48 | 49 | You'll find the built ROM at `target/flac-demo-release.gba`. 50 | 51 | ## Acknowledgements 52 | Thanks to Lokathor & other contributors to rust-console/gba. 53 | 54 | Thanks to Nayuki for the accessible example of FLAC decoding. 55 | 56 | Thanks to Leonarth for the VBA-detection trick. 57 | 58 | Thanks to endrift for mGBA, without which development would be a huge pain. 59 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/render/palette.rs: -------------------------------------------------------------------------------- 1 | /// # GBA display timing periods (courtesy TONC) 2 | /// ```text 3 | /// |<------- 240px ------->|<-68->| 4 | /// _ ______________________________ 5 | /// ^ |>=== scanline == 1,232 cyc ==>| 6 | /// | | | | 7 | /// | |>====== hdraw ========>|hblank| 8 | /// 160px | 960 cycles (~4/px) |272cyc| 9 | /// | | | | 10 | /// | | * vdraw | | 11 | /// | | 197,120 cycles (= 1232 * 160)| 12 | /// v_|_______________________| __ __| 13 | /// ^ | * vblank | 14 | /// 68px| | 83,776 cycles (= 1232 * 68) | 15 | /// v_|______________________________| 16 | /// ``` 17 | /// 18 | /// # Math time! 19 | /// ```text 20 | /// |<40>|<-- 160px -->|<40>|<-68->| 21 | /// _ ______________________________ 22 | /// ^ |hud |gameplay area|hud |hblank| 23 | /// | |<------200px----->|<----148px-@ {200*4 = 800cyc after VCount hits to start copy} 24 | /// | @--->| | | | {148*4 = 596cyc to copy blends to PalRAM, 25 | /// 160px | | |<----------@ (40*2)*4 = 320cyc of which have an addn'l waitstate} 26 | /// | @---------------456px----------@ 27 | /// | @--->| textbox | | | {456*4 = 1,824cyc to copy textbox to PalRAM, 28 | /// | | |=============| | | (240+40*2)*4 = 1,280cyc of which have addn'l waitstate} 29 | /// v_|____|_____________|____| __ __| 30 | /// ^ | | {copy y=0 blend into PalRAM and second palette to IWRAM 31 | /// 68px| | 83,776cyc of vblank for game | before end of vblank} 32 | /// v_|______________________________| 33 | /// ``` 34 | /// - on VCount interrupt (at x=0), set a timer IRQ to overflow in 800-n cycles, where n is the 35 | /// number of cycles it takes to handle the VCount interrupt and set the timer registers 36 | /// - on Timer interrupt (at x=200ish), start copying blend colors from ROM to PalRAM. 37 | /// - 320c of 5c copies (amortized 2c/word read from ROM + 2c/word write to PalRAM + 1 waitstate) 38 | /// - 320/5 = 64 words = 128 colors (minus overhead < 8 palette lines) 39 | /// - 276c of 4c copies (amortized 2c/word read from ROM + 2c/word write to PalRAM) 40 | /// - 276/4 = 69 words = 138 colors (minus overhead < 8.5 palette lines) 41 | 42 | use flowergal_proj_config::resources::{PaletteData, BLEND_ENTRIES, TEXTBOX_Y_START, TEXTBOX_Y_END, BLEND_RESOLUTION}; 43 | use gba::io::display::VBLANK_SCANLINE; 44 | 45 | const ZERO: u32 = 0; 46 | 47 | pub(crate) const NO_EFFECT: &[PaletteData] = &[]; 48 | pub(crate) const NO_COLORS: &[gba::Color] = &[]; 49 | 50 | /// must be in sorted order 51 | pub const TEXTBOX_VCOUNTS: [u16; 2] = [TEXTBOX_Y_START - 1, TEXTBOX_Y_END]; 52 | 53 | /// last vcount interrupt that fires on hardware 54 | const VCOUNT_LAST: u16 = VBLANK_SCANLINE + 68 - 1; // 227 55 | 56 | /// two passes of the screen in cycles, one with vcounts offset by half the blend resolution 57 | /// for flickering between palette swap lines to smooth horizontal bands. 58 | /// (playing on emulator? turn on "interframe blending") 59 | pub(crate) const VCOUNT_SEQUENCE_LEN: usize = 2 * (BLEND_ENTRIES + TEXTBOX_VCOUNTS.len() + 1); 60 | pub(crate) const VCOUNT_SEQUENCE: [u16; VCOUNT_SEQUENCE_LEN] = compute_vcount_sequence(); 61 | /// used in the above array to signify that we should rewind 62 | const VCOUNT_INVALID: u16 = 0xFF; 63 | 64 | #[allow(clippy::comparison_chain)] 65 | const fn compute_vcount_sequence() -> [u16; VCOUNT_SEQUENCE_LEN] { 66 | let mut vcounts = [VCOUNT_INVALID; VCOUNT_SEQUENCE_LEN]; 67 | let offsets: [u16; 2] = [BLEND_RESOLUTION as u16, BLEND_RESOLUTION as u16 / 2]; 68 | let mut oi = 0; 69 | let mut vi = 0; 70 | 71 | while oi < offsets.len() { 72 | let mut line = offsets[oi]; 73 | let mut ti = 0; 74 | while line < VBLANK_SCANLINE { 75 | if ti < TEXTBOX_VCOUNTS.len() { 76 | let y = TEXTBOX_VCOUNTS[ti]; 77 | if line > y { 78 | vcounts[vi] = y; 79 | vi += 1; 80 | ti += 1; 81 | continue; 82 | } else if line == y { 83 | ti += 1; 84 | } 85 | } 86 | vcounts[vi] = line; 87 | vi += 1; 88 | line += BLEND_RESOLUTION as u16; 89 | } 90 | vcounts[vi] = VCOUNT_LAST - 5; 91 | vi += 1; 92 | oi += 1; 93 | } 94 | 95 | vcounts 96 | } 97 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/tile_pixels.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use super::{PAL_LEN, TILE_H, TILE_W}; 4 | 5 | use std::collections::BTreeSet; 6 | 7 | // compare tileimg redundancy on rgbpixels, 8 | // sort palette colors & *then* map colors to palette indices 9 | // map -[sliding window]-> TilePixels -[calculate most-redundant palettes]-> (Pattern, Palette) 10 | 11 | #[derive(Eq, PartialEq, Clone)] 12 | pub struct TilePixels { 13 | pub pixel_colors: Vec, 14 | pub pal: BTreeSet, 15 | } 16 | 17 | impl From<&[gba::Color]> for TilePixels { 18 | fn from(slice: &[gba::Color]) -> Self { 19 | TilePixels { 20 | pixel_colors: slice.to_vec(), 21 | pal: slice.iter().cloned().collect(), 22 | } 23 | } 24 | } 25 | 26 | pub struct TilePixelsBank { 27 | pub tiles: Vec, 28 | } 29 | impl TilePixelsBank { 30 | pub fn new() -> Self { 31 | TilePixelsBank { 32 | tiles: vec![TilePixels::from(&[gba::Color(0); TILE_W * TILE_H][..])], 33 | } 34 | } 35 | 36 | pub fn try_onboard_tile(&mut self, pixel_colors: &[gba::Color]) -> usize { 37 | let new_pattern = TilePixels::from(pixel_colors); 38 | if let Some(tile_num) = self.tiles.iter().position(|img| *img == new_pattern) { 39 | tile_num 40 | } else { 41 | let tile_num = self.tiles.len(); 42 | self.tiles.push(new_pattern); 43 | tile_num 44 | } 45 | } 46 | 47 | /// given a collection of 8x8 pixel ARGB1555 bitmap tiles, each with no more than 15 colors, 48 | /// find a clustering of tile groups, each sharing a 15-color palette, 49 | /// resulting in the fewest possible distinct palettes. 50 | /// 51 | /// Note: this problem is very similar to the NP-hard budgeted maximum coverage problem. 52 | /// See https://en.wikipedia.org/wiki/Maximum_coverage_problem for context. 53 | pub fn approximate_optimal_grouping(&self) -> Vec<(BTreeSet, Vec)> { 54 | let mut groupings: Vec<(BTreeSet, Vec)> = Vec::new(); 55 | let mut remaining_tiles = self.tiles.clone(); 56 | 57 | while !remaining_tiles.is_empty() { 58 | // find the tile whose colors superset those of the greatest number of other tiles 59 | let mut colors = remaining_tiles 60 | .iter() 61 | .max_by_key(|t| { 62 | remaining_tiles 63 | .iter() 64 | .filter(|t2| t.pal.is_superset(&t2.pal)) 65 | .count() 66 | }) 67 | .unwrap() 68 | .pal 69 | .clone(); 70 | 71 | // it's free real estate! 72 | let mut tiles: Vec<_> = remaining_tiles 73 | .drain_filter(|t| t.pal.is_subset(&colors)) 74 | .collect(); 75 | 76 | // fill out the rest of colors until we hit PAL_LEN 77 | while colors.len() < PAL_LEN { 78 | // find tile with most colors common with this palette & won't break the max len, 79 | // with a tie breaker of how many other tiles it would let us superset (as above) 80 | let merge_colors_opt = remaining_tiles 81 | .iter() 82 | .filter(|t| t.pal.union(&colors).count() <= PAL_LEN) 83 | .max_by_key(|t| { 84 | ( 85 | t.pal.intersection(&colors).count(), 86 | remaining_tiles 87 | .iter() 88 | .filter(|t2| { 89 | colors 90 | .union(&t.pal) 91 | .cloned() 92 | .collect::>() 93 | .is_superset(&t2.pal) 94 | }) 95 | .count(), 96 | ) 97 | }) 98 | .map(|t| &t.pal); 99 | if let Some(merge_colors) = merge_colors_opt { 100 | colors.extend(merge_colors.iter()); 101 | tiles.extend(remaining_tiles.drain_filter(|t| t.pal.is_subset(&colors))); 102 | } else { 103 | break; 104 | } 105 | } 106 | groupings.push((colors, tiles)) 107 | } 108 | 109 | groupings 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/pattern.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use super::{SbEntry, TILE_H, TILE_W}; 4 | use gba::vram::{Tile4bpp, Tile8bpp}; 5 | use crate::tile_generation::PalBankId; 6 | 7 | // TODO: compare tileimg redundancy on rgbpixels, not pal index images, 8 | // sort palette colors & *then* map colors 9 | 10 | #[derive(Eq, PartialEq)] 11 | pub struct Pattern(pub Vec); 12 | 13 | impl Pattern { 14 | pub fn hflip(&self) -> Self { 15 | let mut result = Vec::with_capacity(self.0.len()); 16 | for y in 0..TILE_H { 17 | for x in (0..TILE_W).rev() { 18 | result.push(self.0[(y * TILE_W) + x]); 19 | } 20 | } 21 | Pattern(result) 22 | } 23 | pub fn vflip(&self) -> Self { 24 | let mut result = Vec::with_capacity(self.0.len()); 25 | for y in (0..TILE_H).rev() { 26 | for x in 0..TILE_W { 27 | result.push(self.0[(y * TILE_W) + x]); 28 | } 29 | } 30 | Pattern(result) 31 | } 32 | // from GBATEK: 33 | // the lower 4 bits define the color for the left (!) dot, 34 | // the upper 4 bits the color for the right dot. 35 | pub fn to_4bpp(&self) -> [u8; 8 * 8 / 2] { 36 | let mut data: [u8; 8 * 8 / 2] = unsafe { std::mem::transmute(Tile4bpp::default()) }; 37 | for (i, x) in self.0.chunks(2).enumerate() { 38 | let left = x[0]; 39 | let right = x[1]; 40 | data[i] = ((left & 0xF) | ((right & 0xF) << 4)) as u8; 41 | } 42 | data 43 | } 44 | pub fn to_8bpp(&self) -> [u8; 8 * 8] { 45 | let mut data: [u8; 8 * 8] = unsafe { std::mem::transmute(Tile8bpp::default()) }; 46 | for (i, x) in self.0.iter().enumerate() { 47 | data[i] = *x as u8; 48 | } 49 | data 50 | } 51 | } 52 | 53 | impl From<&Pattern> for Tile8bpp { 54 | fn from(img: &Pattern) -> Self { 55 | Tile8bpp(unsafe { std::mem::transmute(img.to_8bpp()) }) 56 | } 57 | } 58 | 59 | impl From<&Pattern> for Tile4bpp { 60 | fn from(img: &Pattern) -> Self { 61 | Tile4bpp(unsafe { std::mem::transmute(img.to_4bpp()) }) 62 | } 63 | } 64 | 65 | pub struct PatternBank { 66 | pub patterns: Vec, 67 | pub max_patterns: usize, 68 | pub is_4bpp: bool, 69 | } 70 | impl PatternBank { 71 | pub fn new(is_4bpp: bool) -> Self { 72 | let max_patterns = if is_4bpp { 512 } else { 256 }; 73 | PatternBank { 74 | patterns: vec![Pattern(vec![0; TILE_W * TILE_H])], 75 | max_patterns, 76 | is_4bpp, 77 | } 78 | } 79 | 80 | pub fn find_existing(&self, new_pattern: &Pattern, palbank: PalBankId) -> Option { 81 | if let Some(tile_num) = self.patterns.iter().position(|img| *img == *new_pattern) { 82 | return Some(SbEntry { 83 | tile_num, 84 | hflip: false, 85 | vflip: false, 86 | palbank, 87 | }); 88 | } 89 | if self.is_4bpp { 90 | let h_flipped = new_pattern.hflip(); 91 | if let Some(tile_num) = self.patterns.iter().position(|img| *img == h_flipped) { 92 | return Some(SbEntry { 93 | tile_num, 94 | hflip: true, 95 | vflip: false, 96 | palbank, 97 | }); 98 | } 99 | let v_flipped = new_pattern.vflip(); 100 | if let Some(tile_num) = self.patterns.iter().position(|img| *img == v_flipped) { 101 | return Some(SbEntry { 102 | tile_num, 103 | hflip: false, 104 | vflip: true, 105 | palbank, 106 | }); 107 | } 108 | let hv_flipped = h_flipped.vflip(); 109 | if let Some(tile_num) = self.patterns.iter().position(|img| *img == hv_flipped) { 110 | return Some(SbEntry { 111 | tile_num, 112 | hflip: true, 113 | vflip: true, 114 | palbank, 115 | }); 116 | } 117 | } 118 | None 119 | } 120 | 121 | pub fn try_onboard_pattern(&mut self, new_img: Pattern, palbank: PalBankId) -> Option { 122 | if let Some(sbe) = self.find_existing(&new_img, palbank) { 123 | Some(sbe) 124 | } else { 125 | self.try_onboard_without_reduction(new_img, palbank) 126 | } 127 | } 128 | 129 | pub fn try_onboard_without_reduction( 130 | &mut self, 131 | new_img: Pattern, 132 | palbank: PalBankId, 133 | ) -> Option { 134 | if self.patterns.len() < self.max_patterns { 135 | let tile_num = self.patterns.len(); 136 | self.patterns.push(new_img); 137 | Some(SbEntry { 138 | tile_num, 139 | hflip: false, 140 | vflip: false, 141 | palbank, 142 | }) 143 | } else { 144 | None 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/palette.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use super::Pattern; 4 | 5 | pub const PAL_LEN: usize = 16; 6 | 7 | pub struct Palette { 8 | pub colors: Vec, 9 | pub max_colors: usize, 10 | } 11 | 12 | impl Palette { 13 | pub(crate) fn new(max_colors: usize) -> Self { 14 | Palette { 15 | colors: vec![gba::Color::from_rgb(0, 0, 0)], // backdrop color if first palette 16 | max_colors, 17 | } 18 | } 19 | 20 | pub fn find_existing(&self, pixel_colors: &[gba::Color]) -> Option { 21 | let mut tile_pixels = Vec::new(); 22 | for color in pixel_colors { 23 | if (color.0 & (1 << 15)) == 0 { 24 | tile_pixels.push(0) // transparent is always index 0 25 | } else if let Some(index) = self.colors.iter().position(|c| *c == *color) { 26 | tile_pixels.push(index) 27 | } else { 28 | return None; 29 | } 30 | } 31 | Some(Pattern(tile_pixels)) 32 | } 33 | 34 | pub fn try_onboard_tile(&mut self, pixel_colors: &[gba::Color]) -> Option { 35 | let start_len = self.colors.len(); // revert to this if we abandon 36 | let mut tile_pixels = Vec::new(); // will convert to 4bpp later 37 | for color in pixel_colors { 38 | if let Some(index) = self.try_onboard_color(*color) { 39 | tile_pixels.push(index); 40 | } else { 41 | // drop any colors we tentatively added, since we can't represent this tile 42 | self.colors.truncate(start_len); 43 | return None; 44 | } 45 | } 46 | Some(Pattern(tile_pixels)) 47 | } 48 | 49 | pub fn try_onboard_color(&mut self, color: gba::Color) -> Option { 50 | // if non-transparent 51 | if (color.0 & (1 << 15)) != 0 { 52 | if let Some(index) = self.colors.iter().position(|c| *c == color) { 53 | Some(index) 54 | } else if self.colors.len() < self.max_colors { 55 | let last = self.colors.len(); 56 | self.colors.push(color); 57 | Some(last) 58 | } else { 59 | None 60 | } 61 | } else { 62 | Some(0) // transparent is entry 0 in all palettes 63 | } 64 | } 65 | } 66 | 67 | #[derive(Copy, Clone, Eq, PartialEq, Debug)] 68 | pub enum PalBankId { 69 | Plain(usize), 70 | Blended(usize), 71 | } 72 | 73 | impl PalBankId { 74 | pub fn new(palbank: usize, blend: bool) -> Self { 75 | if blend { 76 | PalBankId::Blended(palbank) 77 | } else { 78 | PalBankId::Plain(palbank) 79 | } 80 | } 81 | 82 | pub fn bake(&self, pal_bank_ofs: usize) -> u16 { 83 | match self { 84 | PalBankId::Plain(x) => (pal_bank_ofs + x) as u16, 85 | PalBankId::Blended(x) => *x as u16, 86 | } 87 | } 88 | } 89 | 90 | pub struct PaletteBank { 91 | pub palettes_blend: Vec, 92 | pub palettes_plain: Vec, 93 | pub max_palettes: usize, 94 | pub palette_max_colors: usize, 95 | } 96 | 97 | impl PaletteBank { 98 | pub fn new(max_colors: usize) -> Self { 99 | PaletteBank { 100 | palettes_blend: Vec::new(), 101 | palettes_plain: Vec::new(), 102 | max_palettes: max_colors / PAL_LEN, 103 | palette_max_colors: PAL_LEN, 104 | } 105 | } 106 | 107 | pub fn try_onboard_colors(&mut self, pixel_colors: &[gba::Color], blend: bool) -> Option<(Pattern, PalBankId)> { 108 | /* 109 | if let Some(x) = self.find_existing_colors(pixel_colors, blend) { 110 | return Some(x) 111 | } 112 | */ 113 | let total_palettes = self.palettes_plain.len() + self.palettes_blend.len(); 114 | let palette_group = if blend { &mut self.palettes_blend } else { &mut self.palettes_plain }; 115 | for (palbank, pal) in palette_group.iter_mut().enumerate() { 116 | if let Some(img) = pal.try_onboard_tile(pixel_colors) { 117 | return Some((img, PalBankId::new(palbank, blend))); 118 | } 119 | } 120 | if total_palettes < self.max_palettes { 121 | let mut pal = Palette::new(self.palette_max_colors); 122 | if let Some(img) = pal.try_onboard_tile(pixel_colors) { 123 | let palbank = palette_group.len(); 124 | palette_group.push(pal); 125 | return Some((img, PalBankId::new(palbank, blend))); 126 | } 127 | } 128 | None 129 | } 130 | 131 | /* 132 | fn find_existing_colors(&self, pixel_colors: &[gba::Color], blend: bool) -> Option<(Pattern, PalBankId)> { 133 | let pal_set = if blend { &self.palettes_blend } else { &self.palettes_plain }; 134 | for (palbank, pal) in pal_set.iter().enumerate() { 135 | if let Some(img) = pal.find_existing(pixel_colors) { 136 | return Some((img, PalBankId::new(palbank, blend))); 137 | } 138 | } 139 | None 140 | } 141 | */ 142 | } 143 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/music/pcm_conv.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use flowergal_proj_config::sound_info; 4 | use build_const::ConstWriter; 5 | use itertools::Itertools; 6 | use rayon::prelude::*; 7 | use std::error::Error; 8 | use std::path::{Path, PathBuf}; 9 | use std::process::{Command, Output}; 10 | 11 | const MP3_DIR: &str = "../../assets/mp3"; 12 | const LOSSY_WAV: &str = "../../external/lossywav/lossyWAV"; 13 | const FLAC_MOD: &str = "../../external/flac/src/flac/flac"; 14 | 15 | trait CommandSuccess { 16 | fn actually_run(&mut self) -> Result>; 17 | } 18 | 19 | impl CommandSuccess for Command { 20 | fn actually_run(&mut self) -> Result> { 21 | let output = self.output()?; 22 | if !output.status.success() { 23 | let dir = self.get_current_dir() 24 | .map(|p| p.canonicalize().unwrap_or_else(|_| p.to_owned())) 25 | .map(|pb| pb.to_string_lossy().to_string()) 26 | .unwrap_or_default(); 27 | let mut msg = format!( 28 | "Command failed:\n[{}]$ {:?} {:?}", 29 | dir, 30 | self.get_program(), 31 | self.get_args().into_iter().format(" "), 32 | ); 33 | let stdout = String::from_utf8_lossy(&output.stdout).trim_end().to_owned(); 34 | if !stdout.is_empty() { 35 | msg = format!("{}\n-- cmd stdout --\n{}", msg, stdout); 36 | } 37 | let stderr = String::from_utf8_lossy(&output.stderr).trim_end().to_owned(); 38 | if !stderr.is_empty() { 39 | msg = format!("{}\n-- cmd stderr --\n{}", msg, stderr); 40 | } 41 | Err(msg.into()) 42 | } else { 43 | Ok(output) 44 | } 45 | } 46 | } 47 | 48 | pub fn convert_songs_and_sfx() -> Result<(), Box> { 49 | let mut bc_out = ConstWriter::for_build("sound_data_bc")?.finish_dependencies(); 50 | 51 | Command::new("make") 52 | .arg("-j4") 53 | .current_dir(Path::new(LOSSY_WAV).parent().unwrap()) 54 | .actually_run()?; 55 | 56 | if !Path::new(MP3_DIR).exists() { 57 | std::fs::create_dir_all(MP3_DIR)?; 58 | } 59 | 60 | Command::new("youtube-dl") 61 | .args(&["--ignore-config", "--download-archive", "downloaded-ids.txt", "32ZTjFW2RYo"]) 62 | .current_dir(Path::new(MP3_DIR)) 63 | .actually_run()?; 64 | 65 | if !Path::new(FLAC_MOD).exists() { 66 | let flac_dir = Path::new(FLAC_MOD).parent().unwrap().parent().unwrap().parent().unwrap(); 67 | if !flac_dir.join("Makefile").exists() { 68 | Command::new("sh") 69 | .arg("autogen.sh") 70 | .current_dir(flac_dir) 71 | .actually_run()?; 72 | Command::new("sh") 73 | .args(&["configure", "-C"]) 74 | .current_dir(flac_dir) 75 | .actually_run()?; 76 | } 77 | Command::new("make") 78 | .arg("-j4") 79 | .current_dir(flac_dir) 80 | .actually_run()?; 81 | } 82 | 83 | let vec: Vec = sound_info::SONG_FILES 84 | .par_iter() 85 | .map(|mp3_name| { 86 | let mp3_path = Path::new(MP3_DIR).join(mp3_name); 87 | let flac_path = convert_mp3_to_flac(mp3_path).unwrap(); 88 | format!( 89 | "Sound::Flac(include_bytes_align_as!(u32, \"{}\"))", 90 | flac_path.to_string_lossy() 91 | ) 92 | }) 93 | .collect(); 94 | let vec_ref: Vec<&str> = vec.iter().map(String::as_str).collect(); 95 | bc_out.add_array_raw("MUSIC_DATA", "Sound", &vec_ref); 96 | 97 | Ok(()) 98 | } 99 | 100 | fn convert_mp3_to_flac(mp3_path: impl AsRef) -> Result> { 101 | let mp3_path = mp3_path.as_ref(); 102 | 103 | let temp_dir = tempfile::tempdir()?; 104 | let wav_path = temp_dir.path().join("temp_flac_conv").with_extension("wav"); 105 | let lossywav_path = wav_path.with_extension("lossy.wav"); 106 | 107 | let flac_path = 108 | Path::new(&std::env::var("OUT_DIR")?) 109 | .join(mp3_path.file_name().ok_or("uh oh")?) 110 | .with_extension("flac"); 111 | if flac_path.is_file() { 112 | return Ok(flac_path); 113 | } 114 | 115 | println!("cargo:rerun-if-changed={}", mp3_path.to_string_lossy()); 116 | Command::new("ffmpeg") 117 | .args(&["-loglevel", "quiet", "-y", "-i"]) 118 | .arg(mp3_path) 119 | .args(&["-ac", "1", "-ar"]) 120 | .arg(format!("{}", sound_info::SAMPLE_RATE)) 121 | .arg(&wav_path) 122 | .actually_run()?; 123 | 124 | if let Err(e) = Command::new(LOSSY_WAV) 125 | .arg(&wav_path) 126 | .args(&["--force", "--silent", "--shaping", "fixed", "--outdir"]) 127 | .arg(lossywav_path.parent().unwrap()) 128 | .actually_run() { 129 | std::mem::forget(temp_dir); 130 | return Err(e); 131 | } 132 | std::fs::remove_file(wav_path)?; 133 | 134 | if let Err(e) = Command::new(FLAC_MOD) 135 | .args(&["--silent", "-f", "-8", "--escape-coding", "--rice-partition-order=8", "--max-lpc-order=4",]) 136 | .arg(format!("--blocksize={}", sound_info::PLAYBUF_SIZE)) 137 | .arg(format!("--output-name={}", flac_path.to_string_lossy())) 138 | .arg(&lossywav_path) 139 | .actually_run() { 140 | std::mem::forget(temp_dir); 141 | return Err(e); 142 | } 143 | std::fs::remove_file(lossywav_path)?; 144 | 145 | Ok(flac_path) 146 | } 147 | -------------------------------------------------------------------------------- /src/hud.rs: -------------------------------------------------------------------------------- 1 | use gba::io::background::BackgroundControlSetting; 2 | use gba::io::background::{BGSize, BG0CNT}; 3 | use gba::vram::text::TextScreenblockEntry; 4 | use gba::vram::SCREEN_BASE_BLOCKS; 5 | 6 | use voladdress::VolAddress; 7 | 8 | use flowergal_proj_assets::{UI_IMG, UI_PAL}; 9 | use flowergal_runtime::Driver; 10 | use flowergal_proj_config::resources::{TEXT_TOP_ROW, TEXT_BOTTOM_ROW}; 11 | 12 | type TSE = TextScreenblockEntry; 13 | 14 | const HUD_SCREENBLOCK_ID: u16 = 30; 15 | const HUD_SCREENBLOCK: VolAddress = unsafe { 16 | SCREEN_BASE_BLOCKS 17 | .index_unchecked(HUD_SCREENBLOCK_ID as usize) 18 | .cast() 19 | }; 20 | pub const HUD_CHARBLOCK_ID: u16 = 2; 21 | pub const HUD_PALETTE: u16 = 15; 22 | 23 | pub const HUD_LEFT_COL: isize = 4; 24 | pub const HUD_RIGHT_COL: isize = HUD_LEFT_COL + 21; 25 | 26 | pub enum Button { 27 | A, 28 | B, 29 | } 30 | 31 | pub struct Hud {} 32 | 33 | impl Hud { 34 | pub fn new() -> Self { 35 | let bg_settings = BackgroundControlSetting::new() 36 | .with_screen_base_block(HUD_SCREENBLOCK_ID) 37 | .with_char_base_block(HUD_CHARBLOCK_ID) 38 | .with_mosaic(true) 39 | .with_bg_priority(0) 40 | .with_size(BGSize::Zero); 41 | 42 | BG0CNT.write(bg_settings); 43 | 44 | let renderer = unsafe { Driver::instance_mut() }.video(); 45 | renderer.set_normal_colors_bg(240, &UI_PAL); 46 | renderer.load_bg_tiles(HUD_CHARBLOCK_ID, &UI_IMG); 47 | 48 | Hud {} 49 | } 50 | 51 | fn odd_frame(&self) -> bool { 52 | unsafe { Driver::instance_mut().video() }.frame_counter & 1 != 0 53 | } 54 | 55 | fn write_entry(&self, row: isize, col: isize, entry: TSE) { 56 | unsafe { HUD_SCREENBLOCK.offset(row * 32 + col) } 57 | .write(TextScreenblockEntry(entry.with_palbank(HUD_PALETTE).0 + 1)) 58 | } 59 | 60 | fn write_entry_alternating(&self, row: isize, col: isize, entry: TSE) { 61 | // hack 62 | let alternation = if col > 15 { 63 | !self.odd_frame() 64 | } else { 65 | self.odd_frame() 66 | } as u16; 67 | self.write_entry(row, col, TextScreenblockEntry(entry.0 + alternation)) 68 | } 69 | 70 | pub fn draw_background(&self) { 71 | for row in 0..20 { 72 | for col in 0..30 { 73 | let entry = match (col, row) { 74 | (0..=4, _) | (25..=29, _) => tiles::BG, 75 | _ => tiles::NONE, 76 | }; 77 | self.write_entry(row, col, entry); 78 | } 79 | } 80 | } 81 | 82 | pub fn clear_text_area(&self) { 83 | let left = HUD_LEFT_COL + 1; 84 | let right = left + 20; 85 | for row in TEXT_TOP_ROW..=TEXT_BOTTOM_ROW { 86 | for col in left..right { 87 | self.write_entry(row, col, tiles::NONE); 88 | } 89 | } 90 | } 91 | 92 | pub fn draw_borders(&mut self, with_textbox: bool) { 93 | for row in 0..20 { 94 | self.write_entry_alternating(row, HUD_LEFT_COL, tiles::BG_LEFT_EDGE_1); 95 | self.write_entry_alternating(row, HUD_RIGHT_COL, tiles::BG_RIGHT_EDGE_1); 96 | } 97 | if with_textbox { 98 | self.write_entry_alternating(TEXT_TOP_ROW, HUD_LEFT_COL, tiles::TEXTBOX_TL_1); 99 | for i in (HUD_LEFT_COL+1)..=(HUD_RIGHT_COL-1) { 100 | self.write_entry(TEXT_TOP_ROW, i, tiles::TEXTBOX_T); 101 | } 102 | self.write_entry_alternating(TEXT_TOP_ROW, HUD_RIGHT_COL, tiles::TEXTBOX_TR_1); 103 | for i in (TEXT_TOP_ROW+1)..=(TEXT_BOTTOM_ROW-1) { 104 | self.write_entry_alternating(i, HUD_LEFT_COL, tiles::TEXTBOX_L_1); 105 | self.write_entry_alternating(i, HUD_RIGHT_COL, tiles::TEXTBOX_R_1); 106 | } 107 | self.write_entry_alternating(TEXT_BOTTOM_ROW, HUD_LEFT_COL, tiles::TEXTBOX_BL_1); 108 | for i in (HUD_LEFT_COL+1)..=(HUD_RIGHT_COL-1) { 109 | self.write_entry(TEXT_BOTTOM_ROW, i, tiles::TEXTBOX_B); 110 | } 111 | self.write_entry_alternating(TEXT_BOTTOM_ROW, HUD_RIGHT_COL, tiles::TEXTBOX_BR_1); 112 | } 113 | } 114 | 115 | pub fn draw_text(&self, string: &str) { 116 | let left = HUD_LEFT_COL + 1; 117 | let right = left + 20; 118 | let mut row = TEXT_TOP_ROW + 1; 119 | let mut col = left; 120 | for c in string.chars() { 121 | if c == '\n' { 122 | row += 1; 123 | col = left; 124 | } else { 125 | if col >= right { 126 | row += 1; 127 | col = left; 128 | } 129 | self.write_entry(row, col, TSE::from_tile_id(c as u16)); 130 | col += 1; 131 | } 132 | } 133 | } 134 | } 135 | 136 | mod tiles { 137 | use super::*; 138 | pub const NONE: TSE = TSE::from_tile_id(0); 139 | pub const BG: TSE = TSE::from_tile_id(1); 140 | pub const BG_LEFT_EDGE_1: TSE = TSE::from_tile_id(2); 141 | // pub const BG_LEFT_EDGE_2: TSE = TSE::from_tile_id(3); 142 | pub const BG_RIGHT_EDGE_1: TSE = TSE::from_tile_id(2).with_hflip(true); 143 | // pub const BG_RIGHT_EDGE_2: TSE = TSE::from_tile_id(3).with_hflip(true); 144 | 145 | pub const TEXTBOX_TL_1: TSE = TSE::from_tile_id(4); 146 | // pub const TEXTBOX_TL_2: TSE = TSE::from_tile_id(5); 147 | pub const TEXTBOX_TR_1: TSE = TSE::from_tile_id(4).with_hflip(true); 148 | // pub const TEXTBOX_TR_2: TSE = TSE::from_tile_id(5).with_hflip(true); 149 | pub const TEXTBOX_L_1: TSE = TSE::from_tile_id(6); 150 | // pub const TEXTBOX_L_2: TSE = TSE::from_tile_id(7); 151 | pub const TEXTBOX_R_1: TSE = TSE::from_tile_id(6).with_hflip(true); 152 | // pub const TEXTBOX_R_2: TSE = TSE::from_tile_id(7).with_hflip(true); 153 | pub const TEXTBOX_BL_1: TSE = TSE::from_tile_id(8); 154 | // pub const TEXTBOX_BL_2: TSE = TSE::from_tile_id(9); 155 | pub const TEXTBOX_BR_1: TSE = TSE::from_tile_id(8).with_hflip(true); 156 | // pub const TEXTBOX_BR_2: TSE = TSE::from_tile_id(9).with_hflip(true); 157 | pub const TEXTBOX_T: TSE = TSE::from_tile_id(10); 158 | pub const TEXTBOX_B: TSE = TSE::from_tile_id(10).with_vflip(true); 159 | } 160 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/map.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use gba::vram::affine::AffineScreenblockEntry; 4 | use gba::vram::text::TextScreenblockEntry; 5 | 6 | use flowergal_proj_config::resources::{ 7 | Metamap, RoomData, RoomEntries4bpp, RoomEntries8bpp, ROOM_SIZE, 8 | }; 9 | 10 | use super::{TILE_H, TILE_W}; 11 | use crate::tile_generation::PalBankId; 12 | 13 | #[derive(Eq, PartialEq, Clone)] 14 | pub struct SbEntry { 15 | pub tile_num: usize, 16 | pub hflip: bool, 17 | pub vflip: bool, 18 | pub palbank: PalBankId, 19 | } 20 | 21 | impl Default for SbEntry { 22 | fn default() -> Self { 23 | SbEntry { 24 | tile_num: 42, 25 | hflip: true, 26 | vflip: true, 27 | palbank: PalBankId::Blended(42), 28 | } 29 | } 30 | } 31 | 32 | impl SbEntry { 33 | pub fn to_text_sbe(&self, pal_bank_ofs: usize) -> TextScreenblockEntry { 34 | let palbank = self.palbank.bake(pal_bank_ofs); 35 | assert!(self.tile_num < 512); 36 | assert!(palbank < 16); 37 | TextScreenblockEntry::from_tile_id(self.tile_num as u16) 38 | .with_hflip(self.hflip) 39 | .with_vflip(self.vflip) 40 | .with_palbank(palbank) 41 | } 42 | pub fn to_affine_sbe(&self) -> AffineScreenblockEntry { 43 | assert!(self.tile_num < 256); 44 | assert!(!self.hflip); 45 | assert!(!self.vflip); 46 | assert_eq!(self.palbank, PalBankId::Blended(0)); 47 | AffineScreenblockEntry(self.tile_num as u8) 48 | } 49 | } 50 | 51 | #[derive(Clone)] 52 | pub struct Grid { 53 | pub sb_entries: Vec>, 54 | pub grid_width: usize, 55 | pub grid_height: usize, 56 | pub is_4bpp: bool, 57 | } 58 | 59 | impl Grid { 60 | pub fn new(grid_size: (usize, usize), is_4bpp: bool) -> Self { 61 | let (grid_width, grid_height) = grid_size; 62 | Grid { 63 | sb_entries: vec![vec![SbEntry::default(); grid_width]; grid_height], 64 | grid_width, 65 | grid_height, 66 | is_4bpp, 67 | } 68 | } 69 | 70 | pub fn set_sbe(&mut self, tx: usize, ty: usize, sbe: SbEntry) { 71 | self.sb_entries[ty][tx] = sbe; 72 | } 73 | 74 | pub fn gba_raw_sbe_4bpp(&self, pal_bank_ofs: usize) -> Vec { 75 | assert!(self.is_4bpp); 76 | let mut entries = Vec::new(); 77 | for data in self.sb_entries.iter() { 78 | for ent in data.iter() { 79 | entries.push(ent.to_text_sbe(pal_bank_ofs)); 80 | } 81 | } 82 | entries 83 | } 84 | 85 | pub fn gba_raw_sbe_8bpp(&self) -> Vec { 86 | assert!(!self.is_4bpp); 87 | let mut entries = Vec::new(); 88 | for data in self.sb_entries.iter() { 89 | for ent in data.iter() { 90 | entries.push(ent.to_affine_sbe()); 91 | } 92 | } 93 | entries 94 | } 95 | 96 | pub fn gba_room_entries_4bpp(&self, pal_bank_ofs: usize) -> RoomEntries4bpp { 97 | assert!(self.is_4bpp); 98 | assert_eq!(self.grid_width, ROOM_SIZE.0); 99 | assert_eq!(self.grid_height, ROOM_SIZE.1); 100 | let mut array = [[TextScreenblockEntry::default(); ROOM_SIZE.0]; ROOM_SIZE.1]; 101 | for (row, data) in self.sb_entries.iter().enumerate() { 102 | for (col, ent) in data.iter().enumerate() { 103 | array[row][col] = ent.to_text_sbe(pal_bank_ofs); 104 | } 105 | } 106 | RoomEntries4bpp(array) 107 | } 108 | 109 | pub fn gba_room_entries_8bpp(&self) -> RoomEntries8bpp { 110 | assert!(!self.is_4bpp); 111 | assert_eq!(self.grid_width, ROOM_SIZE.0/2); 112 | assert_eq!(self.grid_height, ROOM_SIZE.1/2); 113 | let mut array = [[AffineScreenblockEntry::default(); ROOM_SIZE.0/2]; ROOM_SIZE.1/2]; 114 | for (row, data) in self.sb_entries.iter().enumerate() { 115 | for (col, ent) in data.iter().enumerate() { 116 | array[row][col] = ent.to_affine_sbe(); 117 | } 118 | } 119 | RoomEntries8bpp(array) 120 | } 121 | } 122 | 123 | pub struct RoomBank { 124 | pub rooms: Vec>, 125 | pub room_width: usize, 126 | pub room_height: usize, 127 | pub map_width: usize, 128 | pub map_height: usize, 129 | pub is_4bpp: bool, 130 | } 131 | 132 | impl RoomBank { 133 | pub fn new(map_size_pixels: (u32, u32), room_size: (usize, usize), is_4bpp: bool) -> Self { 134 | let (room_width, room_height) = room_size; 135 | let map_width = map_size_pixels.0 as usize / TILE_W; 136 | let map_height = map_size_pixels.1 as usize / TILE_H; 137 | let rooms = vec![ 138 | vec![Grid::new(room_size, is_4bpp); map_width / room_width]; 139 | map_height / room_height 140 | ]; 141 | RoomBank { 142 | rooms, 143 | room_width, 144 | room_height, 145 | map_width, 146 | map_height, 147 | is_4bpp, 148 | } 149 | } 150 | 151 | pub fn set_sbe(&mut self, tx: usize, ty: usize, sbe: SbEntry) { 152 | self.rooms[ty / self.room_height][tx / self.room_width].set_sbe( 153 | tx % self.room_width, 154 | ty % self.room_height, 155 | sbe, 156 | ) 157 | } 158 | 159 | pub fn gba_metamap_and_roomdata(&self, pal_bank_ofs: usize) -> (Metamap, RoomData) { 160 | // could do with a refactor... 161 | let mut room_data_4bpp = Vec::with_capacity(self.map_width * self.map_height); 162 | let mut room_data_8bpp = Vec::with_capacity(self.map_width * self.map_height); 163 | let mut meta_outer: Vec<&[u8]> = Vec::with_capacity(self.rooms.len()); 164 | for row in &self.rooms { 165 | let mut meta_inner = Vec::with_capacity(row.len()); 166 | for room in row { 167 | if self.is_4bpp { 168 | meta_inner.push(room_data_4bpp.len() as u8); 169 | room_data_4bpp.push(room.gba_room_entries_4bpp(pal_bank_ofs)); 170 | } else { 171 | meta_inner.push(room_data_8bpp.len() as u8); 172 | room_data_8bpp.push(room.gba_room_entries_8bpp()); 173 | } 174 | } 175 | meta_outer.push(Box::leak(meta_inner.into_boxed_slice())); 176 | } 177 | let mm = Metamap(Box::leak(meta_outer.into_boxed_slice())); 178 | let rd = if self.is_4bpp { 179 | RoomData::Text(Box::leak(room_data_4bpp.into_boxed_slice())) 180 | } else { 181 | RoomData::Affine(Box::leak(room_data_8bpp.into_boxed_slice())) 182 | }; 183 | (mm, rd) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /internal/flowergal-proj-config/src/resources/mod.rs: -------------------------------------------------------------------------------- 1 | pub use gba::vram::affine::AffineScreenblockEntry; 2 | pub use gba::vram::text::TextScreenblockEntry; 3 | pub use gba::vram::{Tile4bpp, Tile8bpp}; 4 | pub use gba::Color; 5 | 6 | use crate::sound_info::TrackList; 7 | use crate::WorldId; 8 | 9 | pub mod blend; 10 | 11 | pub const ROOM_SIZE: (usize, usize) = (32, 32); 12 | 13 | pub const TEXT_TOP_ROW: isize = 13; 14 | pub const TEXT_BOTTOM_ROW: isize = TEXT_TOP_ROW + 4; 15 | 16 | pub const TEXTBOX_Y_START: u16 = TEXT_TOP_ROW as u16 * 8 + 1; 17 | pub const TEXTBOX_Y_END: u16 = TEXT_BOTTOM_ROW as u16 * 8 + 5; 18 | pub const TEXTBOX_Y_MID_EFFECT_INDEX: usize = (TEXTBOX_Y_START + 20) as usize / BLEND_RESOLUTION; 19 | 20 | // TODO: source from image file? 21 | pub const TEXTBOX_R: u16 = 42; 22 | pub const TEXTBOX_G: u16 = 42; 23 | pub const TEXTBOX_B: u16 = 69; 24 | pub const TEXTBOX_A: u16 = 141; 25 | 26 | /// resolution of palette effects (not every scanline for memory and cpu's sake) 27 | /// must be a power of 2 for ARMv4's sake (CPU long division during hblank is a bad plan) 28 | pub const BLEND_RESOLUTION: usize = if cfg!(debug_assertions) { 16 } else { 4 }; 29 | /// note: this many KiB get used by the blend cache in EWRAM, adjust RESOLUTION accordingly... 30 | pub const BLEND_ENTRIES: usize = 160 / BLEND_RESOLUTION; 31 | 32 | #[repr(align(4))] 33 | pub struct AlignWrapper(pub T); 34 | 35 | #[repr(transparent)] 36 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 37 | pub struct RoomEntries4bpp(pub [[TextScreenblockEntry; ROOM_SIZE.0]; ROOM_SIZE.1]); 38 | 39 | #[repr(transparent)] 40 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 41 | pub struct RoomEntries8bpp(pub [[AffineScreenblockEntry; ROOM_SIZE.0/2]; ROOM_SIZE.1/2]); 42 | 43 | #[repr(transparent)] 44 | pub struct Metamap(pub &'static [&'static [u8]]); 45 | 46 | pub enum TilePatterns { 47 | Text(&'static [Tile4bpp]), 48 | Affine(&'static [Tile8bpp]), 49 | TextLz77(&'static [u32]), 50 | AffineLz77(&'static [u32]), 51 | } 52 | 53 | pub enum RoomData { 54 | Text(&'static [RoomEntries4bpp]), 55 | Affine(&'static [RoomEntries8bpp]), 56 | TextLz77(&'static [&'static [u32]]), 57 | } 58 | 59 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 60 | pub struct Layer { 61 | pub map: RoomData, 62 | pub meta: Metamap, 63 | } 64 | 65 | // alignment woes... 66 | #[derive(Clone)] 67 | pub struct PaletteData(pub &'static [u32]); 68 | 69 | impl PaletteData { 70 | pub const fn new(c: &'static [Color]) -> Self { 71 | let ptr = c.as_ptr(); 72 | let len = (c.len() + 1) / 2; 73 | unsafe { PaletteData(&*core::ptr::slice_from_raw_parts(ptr as *const u32, len)) } 74 | } 75 | pub const fn data(&self) -> &'static [Color] { 76 | let ptr = self.0.as_ptr(); 77 | let len = self.0.len() * 2; 78 | unsafe { &*core::ptr::slice_from_raw_parts(ptr as *const Color, len) } 79 | } 80 | } 81 | 82 | #[derive(Clone)] 83 | pub struct WorldPalettes { 84 | pub normal_palette: PaletteData, 85 | pub blended_palettes: &'static [PaletteData], 86 | pub textbox_blend_palette: PaletteData, 87 | } 88 | 89 | pub enum ColorEffectType { 90 | None, 91 | Overlay, 92 | HardLight, 93 | Multiply, 94 | } 95 | 96 | #[repr(align(4))] 97 | pub enum Sound { 98 | RawPcm8(&'static [u8]), 99 | Flac(&'static [u8]), 100 | } 101 | 102 | impl Sound { 103 | pub fn data_ptr(&self) -> *const u8 { 104 | match self { 105 | Sound::RawPcm8(x) | Sound::Flac(x) => x.as_ptr(), 106 | } 107 | } 108 | } 109 | 110 | #[cfg_attr(not(target_arch = "arm"), derive(Debug))] 111 | pub struct WorldData { 112 | pub id: WorldId, 113 | pub name: &'static str, 114 | pub pal: WorldPalettes, 115 | pub img: TilePatterns, 116 | pub img_special: TilePatterns, 117 | pub bg_layer: Option, 118 | pub fg_layer: Option, 119 | pub skybox_layer: Option, 120 | // TODO: pub anim_tiles: { Grid } 121 | // TODO: this'll have to be referential rather than a copy of the data. 122 | // (some songs like Mitra's theme are used in multiple places, plus DEBUG has a soundtest) 123 | pub music: TrackList, 124 | } 125 | 126 | /// The impl Debug from the derive macro doesn't put amps in front of references, 127 | /// or exist for statically sized arrays larger than 32. 128 | #[cfg(not(target_arch = "arm"))] 129 | mod debug_impls_for_build_const { 130 | use super::*; 131 | use std::fmt::Formatter; 132 | 133 | impl core::fmt::Debug for PaletteData { 134 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 135 | write!(f, "PaletteData(&{:?})", self.0) 136 | } 137 | } 138 | 139 | impl core::fmt::Debug for Metamap { 140 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 141 | writeln!(f, "Metamap(&[")?; 142 | for row in self.0 { 143 | writeln!(f, " &{:?},", row)?; 144 | } 145 | write!(f, "])") 146 | } 147 | } 148 | 149 | impl core::fmt::Debug for TilePatterns { 150 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 151 | match self { 152 | TilePatterns::Text(data) => write!(f, "TilePatterns::Text(&{:?})", data), 153 | TilePatterns::Affine(data) => write!(f, "TilePatterns::Affine(&{:?})", data), 154 | TilePatterns::TextLz77(data) => write!(f, "TilePatterns::TextLz77(&{:?})", data), 155 | TilePatterns::AffineLz77(data) => { 156 | write!(f, "TilePatterns::AffineLz77(&{:?})", data) 157 | } 158 | } 159 | } 160 | } 161 | 162 | impl core::fmt::Debug for RoomData { 163 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 164 | match self { 165 | RoomData::Text(data) => write!(f, "RoomData::Text(&{:?})", data), 166 | RoomData::Affine(data) => write!(f, "RoomData::Affine(&{:?})", data), 167 | RoomData::TextLz77(data) => { 168 | writeln!(f, "RoomData::TextLz77(&[")?; 169 | for d in *data { 170 | writeln!(f, " &{:?},", *d)?; 171 | } 172 | write!(f, "])") 173 | } 174 | } 175 | } 176 | } 177 | 178 | impl core::fmt::Debug for WorldPalettes { 179 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 180 | writeln!(f, "WorldPalettes {{")?; 181 | writeln!(f, " normal_palette: {:?},", self.normal_palette)?; 182 | writeln!(f, " blended_palettes: &[")?; 183 | for row in self.blended_palettes.iter() { 184 | writeln!(f, " {:?},", row)?; 185 | } 186 | writeln!(f, " ],")?; 187 | writeln!( 188 | f, 189 | " textbox_blend_palette: {:?},", 190 | self.textbox_blend_palette 191 | )?; 192 | write!(f, "}}") 193 | } 194 | } 195 | 196 | impl core::fmt::Debug for Sound { 197 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 198 | match self { 199 | Sound::RawPcm8(data) => write!(f, "Sound::RawPcm8(&{:?})", data), 200 | Sound::Flac(data) => write!(f, "Sound::Flac(&{:?})", data), 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/license_text.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::collections::BTreeMap; 4 | use std::error::Error; 5 | use std::fmt::Write; 6 | use std::fs::File; 7 | use std::io::Read; 8 | use std::path::PathBuf; 9 | use std::sync::Arc; 10 | 11 | use cargo_about::licenses::{Gatherer, LicenseStore, LicenseInfo}; 12 | use cargo_about::licenses::config::{Config, KrateConfig, Ignore}; 13 | use spdx::{Licensee, LicenseId}; 14 | use spdx::expression::{ExprNode, Operator}; 15 | use hyphenation::Load; 16 | use regex::Regex; 17 | 18 | use build_const::ConstWriter; 19 | 20 | //use crate::compression::{CompressibleAsset, do_lz77_compression}; 21 | 22 | const LICENSE_PREFERENCES: &[&str] = &["0BSD", "Unlicense", "MIT", "BSD-3-Clause", "Apache-2.0"]; 23 | const NO_INCLUSION_OBLIGATIONS: &[&str] = &["0BSD", "Unlicense"]; 24 | 25 | const WRAP_WIDTH: usize = 255; // length of mGBA debugString minus \0 terminator 26 | 27 | /* 28 | enum TextResource { 29 | Raw(String), 30 | Lz77(&'static [u32]), 31 | } 32 | impl CompressibleAsset for TextResource { 33 | fn compress(self) -> Result> { 34 | match self { 35 | TextResource::Raw(s) => { 36 | Ok(TextResource::Lz77(do_lz77_compression(s.as_bytes(), true)?)) 37 | } 38 | x => Ok(x), 39 | } 40 | } 41 | } 42 | */ 43 | 44 | pub fn generate_text() -> Result<(), Box> { 45 | let mut bc_out = ConstWriter::for_build("license_text_bc")?.finish_dependencies(); 46 | 47 | let text = get_text()?; 48 | bc_out.add_value("LICENSE_TEXT", "&str", text); 49 | 50 | bc_out.finish(); 51 | 52 | // TextResource::Raw(_text).compress()?; 53 | 54 | Ok(()) 55 | } 56 | 57 | fn read_from_files(included_crate_licenses: BTreeMap>) -> Result, String>, Box> { 58 | // keyed on license text! 59 | let mut reverse_map: BTreeMap> = BTreeMap::new(); 60 | for (crate_name, licenses) in included_crate_licenses { 61 | let mut buf = String::new(); 62 | for (_license_name, path) in licenses { 63 | File::open(path)?.read_to_string(&mut buf)?; 64 | } 65 | buf = buf.replace("–", "-"); 66 | buf = buf.replace("©", "(c)"); 67 | // FIXME: put some unicode chars in font, special-case the tile IDs here when we do 68 | buf = buf.replace("ń", "n"); 69 | buf = buf.replace("ł", "l"); 70 | let dirty_newline = Regex::new(r"(\r\n|\n\t|\n )")?; 71 | while dirty_newline.is_match(&buf) { 72 | buf = dirty_newline.replace_all(&buf, "\n").to_string(); 73 | } 74 | let single_newline = Regex::new(r"([^\n])\n([^\n])")?; 75 | while single_newline.is_match(&buf) { 76 | buf = single_newline.replace_all(&buf, "$1 $2").to_string(); 77 | } 78 | reverse_map.entry(buf).or_default().push(crate_name); 79 | } 80 | let forward_map = reverse_map.into_iter() 81 | .map(|(k, v)| (v, k)) 82 | .collect(); 83 | 84 | Ok(forward_map) 85 | } 86 | 87 | fn get_text() -> Result> { 88 | let included_crate_licenses = get_relevant_crate_licenses()?; 89 | 90 | let grouped_licenses = read_from_files(included_crate_licenses)?; 91 | 92 | let mut text = "\ 93 | This demo uses the following crates \ 94 | under their respective open-source licenses.\ 95 | \n================\n\n".to_string(); 96 | 97 | const WRAP_WIDTH_MINUS_4: usize = WRAP_WIDTH - 4; 98 | const WRAP_WIDTH_MINUS_3: usize = WRAP_WIDTH - 3; 99 | const WRAP_WIDTH_MINUS_2: usize = WRAP_WIDTH - 2; 100 | for (crate_names, license_text) in grouped_licenses { 101 | for crate_name in crate_names { 102 | match crate_name.len() { 103 | 0 => return Err("crate had no name!".into()), 104 | 1..=WRAP_WIDTH_MINUS_4 => writeln!(text, "= {} =", crate_name)?, 105 | WRAP_WIDTH_MINUS_3 | WRAP_WIDTH_MINUS_2 => writeln!(text, "={}=", crate_name)?, 106 | _ => writeln!(text, "{}\n===", crate_name)?, 107 | } 108 | } 109 | writeln!(text, "\n{}\n---\n", license_text)?; 110 | } 111 | 112 | let splitter = hyphenation::Standard::from_embedded(hyphenation::Language::EnglishUS)?; 113 | let options = textwrap::Options::new(WRAP_WIDTH).splitter(splitter); 114 | let wrapped = textwrap::fill(&text, options); 115 | 116 | Ok(wrapped) 117 | } 118 | 119 | fn get_relevant_crate_licenses() -> Result>, Box> { 120 | // workaround for build tools and crates already accounted for in the cargo metadata (gba) 121 | // causing the root crate to have a confusing license detected 122 | // FIXME: we could just.. reorganize the workspace to not have the top-level be a crate? 123 | let mut crates: BTreeMap = BTreeMap::new(); 124 | crates.insert("flac-demo".to_string(), KrateConfig { 125 | additional: vec![], 126 | ignore: [ 127 | ("BSD-3-Clause", "external/flac/COPYING.Xiph"), 128 | ("GFDL-1.2", "external/flac/COPYING.FDL"), 129 | ("GPL-2.0", "external/flac/COPYING.GPL"), 130 | ("LGPL-2.1", "external/flac/COPYING.LGPL"), 131 | ("Apache-2.0", "external/gba/LICENSE-APACHE2.txt"), 132 | ("MIT", "external/gba-compression/LICENSE"), 133 | ("GPL-3.0", "external/lossywav/gpl.txt"), 134 | ("BSD-3-Clause", "internal/flowergal-runtime/COPYING"), 135 | ("MIT", "internal/flowergal-runtime/COPYING-simpleflac"), 136 | ("AGPL-3.0", "internal/flowergal-buildtools/COPYING"), 137 | ].iter().map(|(lic, path)| Ignore { 138 | license: spdx::license_id(lic).unwrap(), 139 | license_file: path.into(), 140 | license_start: None, 141 | license_end: None 142 | }).collect() 143 | }); 144 | 145 | let cfg = Config { 146 | targets: std::env::var("CARGO_BUILD_TARGET").into_iter().collect(), 147 | ignore_build_dependencies: true, 148 | ignore_dev_dependencies: true, 149 | accepted: LICENSE_PREFERENCES 150 | .iter() 151 | .map(|x| Licensee::parse(x).unwrap()) 152 | .collect(), 153 | crates 154 | }; 155 | let cargo_toml = std::env::current_dir()?.parent().unwrap().parent().unwrap().join("Cargo.toml"); 156 | let krates = cargo_about::get_all_crates( 157 | cargo_toml, 158 | true, 159 | false, 160 | Vec::new(), // FIXME: can we determine what --features were passed to cargo build? 161 | &cfg 162 | )?; 163 | let gatherer = Gatherer::with_store(Arc::new(LicenseStore::from_cache()?)); 164 | // threshold chosen to avoid autodetecting Makefiles in external/flac, etc., 165 | // actual license files are generally > 98.5% confidence 166 | let summary = gatherer.with_confidence_threshold(0.93).gather(&krates, &cfg); 167 | 168 | let mut included_crate_licenses = BTreeMap::new(); 169 | for nfo in summary.nfos { 170 | let info = nfo.lic_info; 171 | let stack = reduce_license_expression_by_preference(&info); 172 | for file in nfo.license_files.iter() 173 | .filter(|lf| stack.contains(&lf.id)) { 174 | included_crate_licenses.entry(nfo.krate.name.clone()) 175 | .or_insert_with(BTreeMap::new) 176 | .insert(file.id.full_name, file.path.clone()); 177 | } 178 | } 179 | 180 | Ok(included_crate_licenses) 181 | } 182 | 183 | fn reduce_license_expression_by_preference(info: &LicenseInfo) -> Vec { 184 | let mut stack = Vec::new(); 185 | match &info { 186 | LicenseInfo::Expr(expr) => { 187 | let nodes: Vec<_> = expr.iter().collect(); 188 | // FIXME: this logic extremely doesn't work for nodes.len() > 3 189 | assert!(nodes.len() <= 3); 190 | for node in nodes { 191 | match node { 192 | ExprNode::Op(Operator::Or) => { 193 | stack.sort_by_cached_key(|li| LICENSE_PREFERENCES 194 | .iter() 195 | .enumerate() 196 | .find_map(|(index, name)| { 197 | if *li == spdx::license_id(name).unwrap() { 198 | Some(index) 199 | } else { 200 | None 201 | } 202 | }) 203 | ); 204 | stack.pop(); 205 | } 206 | ExprNode::Op(Operator::And) => {} 207 | ExprNode::Req(req) => { 208 | stack.push(req.req.license.id().unwrap()); 209 | } 210 | } 211 | } 212 | } 213 | LicenseInfo::Unknown => {} 214 | } 215 | stack.drain_filter(|li| NO_INCLUSION_OBLIGATIONS.iter() 216 | .map(|name| spdx::license_id(name).unwrap()) 217 | .find(|x| x == li) 218 | .is_some()); 219 | stack 220 | } 221 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/world_data_gen.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::error::Error; 4 | use std::path::Path; 5 | 6 | use build_const::ConstWriter; 7 | 8 | use rayon::prelude::*; 9 | 10 | use sdl2::image::LoadSurface; 11 | use sdl2::pixels::PixelFormatEnum; 12 | use sdl2::surface::Surface; 13 | 14 | use crate::compression::CompressibleAsset; 15 | use crate::tile_generation::{sdl_support, ImageBank, Grid}; 16 | use flowergal_proj_config::resources::blend::float; 17 | use flowergal_proj_config::resources::{ColorEffectType, Layer, PaletteData, WorldData, WorldPalettes, BLEND_ENTRIES, TEXTBOX_A, TEXTBOX_B, TEXTBOX_G, TEXTBOX_R, TEXTBOX_Y_MID_EFFECT_INDEX, ROOM_SIZE}; 18 | use flowergal_proj_config::{WorldId, WorldResourceInfo, WORLD_RESOURCE_INFO}; 19 | 20 | const ASSET_DIR: &str = "../../assets/gfx"; 21 | const TILEMAPS_DIR: &str = "../../assets/tilemaps"; 22 | const ANIMTILES_DIR: &str = "../../assets/animtiles"; 23 | const SKYBOX_DIR: &str = "../../assets/skybox"; 24 | const EFFECT_DIR: &str = "../../assets/effect"; 25 | 26 | fn convert_single_world_map(world: &WorldResourceInfo) -> String { 27 | let mod_name = format!("{}_gfx_bc", world.name); 28 | let mut out = ConstWriter::for_build(&mod_name) 29 | .unwrap() 30 | .finish_dependencies(); 31 | 32 | let data = generate_world_data(world) 33 | .map_err(|e| format!("{}: {}", world.name, e)) 34 | .unwrap(); 35 | 36 | out.add_value(&format!("{}_DATA", world.name), "WorldData", data); 37 | out.finish(); 38 | mod_name 39 | } 40 | 41 | pub fn convert_world_maps() -> Result<(), Box> { 42 | let _sdl_context = sdl2::init().expect("Couldn't init SDL2"); 43 | let _image_context = 44 | sdl2::image::init(sdl2::image::InitFlag::PNG).expect("Couldn't init SDL2_image"); 45 | 46 | let mut bc_out = ConstWriter::for_build("world_gfx_bc")?.finish_dependencies(); 47 | let mod_name_results: Vec = WORLD_RESOURCE_INFO 48 | .par_iter() 49 | .map(convert_single_world_map) 50 | .collect(); 51 | for mod_name_res in mod_name_results { 52 | bc_out.add_raw(&format!(r#"build_const!("{}");"#, mod_name_res)); 53 | } 54 | 55 | bc_out.finish(); 56 | 57 | Ok(()) 58 | } 59 | 60 | pub fn render_world_to_16bit_surfaces( 61 | world: &WorldResourceInfo, 62 | ) -> Result, Box> { 63 | let orig = load_surface_resource(TILEMAPS_DIR, world.tilemap_path)?; 64 | let format = PixelFormatEnum::ARGB1555; 65 | let mut dst = Surface::new(orig.width(), orig.height(), format)?; 66 | let dst_rect = dst.rect(); 67 | orig.blit(orig.rect(), &mut dst, dst_rect)?; 68 | Ok(vec![dst]) 69 | } 70 | 71 | fn load_surface_resource<'a>( 72 | base_dir: &str, 73 | filename: &str, 74 | ) -> Result, Box> { 75 | let surf_path = [ASSET_DIR, base_dir] 76 | .iter() 77 | .map(|s| Path::new(s).join(filename)) 78 | .find(|p| p.is_file()) 79 | .ok_or(format!( 80 | "{} not found in {} or override dir", 81 | filename, base_dir 82 | ))?; 83 | println!("cargo:rerun-if-changed={}", surf_path.to_string_lossy()); 84 | Ok(Surface::from_file(&surf_path)?) 85 | } 86 | 87 | fn generate_blend_palettes( 88 | gba_pal_normal: &[gba::Color], 89 | blend_colors: &Vec, 90 | blend_function: fn(gba::Color, (u8, u8, u8)) -> gba::Color, 91 | ) -> Vec<&'static [gba::Color]> { 92 | let num_palettes = BLEND_ENTRIES; 93 | let mut palettes: Vec<&'static [gba::Color]> = Vec::with_capacity(num_palettes); 94 | for i in 0..num_palettes { 95 | let sdl_c = blend_colors 96 | [(blend_colors.len() - 1).min(i * blend_colors.len() / (num_palettes - 1))] 97 | .rgb(); 98 | let p: Vec = gba_pal_normal 99 | .iter() 100 | .map(|gba_c| blend_function(*gba_c, sdl_c)) 101 | .collect(); 102 | palettes.push(Box::leak(p.into_boxed_slice())); 103 | } 104 | palettes 105 | } 106 | 107 | fn generate_animtiles( 108 | world: &WorldResourceInfo, 109 | bank: &mut ImageBank, 110 | ) -> Result> { 111 | if let Some(anim_path) = world.anim_path { 112 | let surf = load_surface_resource(ANIMTILES_DIR, anim_path)?; 113 | let blend = match world.id { 114 | _ => true, 115 | }; 116 | let grid = bank.process_image_region(&surf, None, true, false, blend)?; 117 | Ok(grid) 118 | } else { 119 | Ok(Grid::new((0, 0), true)) 120 | } 121 | } 122 | 123 | fn generate_skybox( 124 | world: &WorldResourceInfo, 125 | bank: &mut ImageBank, 126 | ) -> Result, Box> { 127 | if let Some(skybox_path) = world.skybox_path { 128 | let surf = load_surface_resource(SKYBOX_DIR, skybox_path)?; 129 | let tile8_is_blended = match world.id { 130 | _ => |_, _| true, 131 | }; 132 | let rb = bank.process_world_map(&surf, true, &tile8_is_blended, (16, 16))?; 133 | 134 | // FIXME/HACK: at this point we happen to have all the colors 135 | let pal_bank_ofs = bank.palette_bank.palettes_blend.len(); 136 | let (meta, map) = rb.gba_metamap_and_roomdata(pal_bank_ofs); 137 | Ok(Some(Layer { 138 | map: map.compress()?, 139 | meta, 140 | })) 141 | } else { 142 | Ok(None) 143 | } 144 | } 145 | 146 | fn compute_blend_effects( 147 | world: &WorldResourceInfo, 148 | bank: &mut ImageBank, 149 | ) -> Result> { 150 | let normal_palette = bank.gba_palette_full(); 151 | let blend_len = bank.blend_palette_size(); 152 | 153 | let blended_palettes = 154 | Box::leak(compute_overlay_palettes(world, &normal_palette.data()[..blend_len])? 155 | .into_boxed_slice()); 156 | 157 | let mut base_pal = Vec::from(normal_palette.data()); 158 | for (base_col, blend_col) in base_pal.iter_mut() 159 | .zip(blended_palettes.get(TEXTBOX_Y_MID_EFFECT_INDEX).map(|x| x.data().iter()).unwrap_or([].iter())) 160 | { 161 | *base_col = *blend_col; 162 | } 163 | let base_pal = PaletteData::new(base_pal.leak()); 164 | let textbox_blend_palette = compute_textbox_palette(&base_pal); 165 | 166 | Ok(WorldPalettes { 167 | normal_palette, 168 | blended_palettes, 169 | textbox_blend_palette, 170 | // gradient_colors: PaletteData(Box::leak(sdl_support::sdl_to_gba_colors(blend_colors).into_boxed_slice())), 171 | }) 172 | } 173 | 174 | fn compute_textbox_palette(base_pal: &PaletteData) -> PaletteData { 175 | let textbox_rgb = (TEXTBOX_R as u8, TEXTBOX_G as u8, TEXTBOX_B as u8); 176 | let alpha = TEXTBOX_A as f64 / 255.0; 177 | let text_pal: Vec = base_pal 178 | .data() 179 | .iter() 180 | .map(|a| float::blend_alpha(*a, textbox_rgb, alpha)) 181 | .collect(); 182 | PaletteData::new(Box::leak(text_pal.into_boxed_slice())) 183 | } 184 | 185 | fn compute_overlay_palettes( 186 | world: &WorldResourceInfo, 187 | gba_pal_normal: &[gba::Color], 188 | ) -> Result, Box> { 189 | if let Some(effect_path) = world.effect_path { 190 | let surf = load_surface_resource(EFFECT_DIR, effect_path)?; 191 | let mut rect = surf.rect(); 192 | rect.set_x(rect.width() as i32 / 3); 193 | rect.set_width(1); 194 | // note: backdrop of gray seems to be a no-op color for overlay blend 195 | // (instead of implementing alpha compositing again) 196 | let blend_colors = 197 | sdl_support::pixels_of_rect_32bit(&surf, rect, Some(sdl2::pixels::Color::GRAY))?; 198 | 199 | let blend_function = match world.effect_type { 200 | ColorEffectType::Overlay => float::blend_overlay, 201 | ColorEffectType::HardLight => float::blend_hardlight, 202 | ColorEffectType::Multiply => float::blend_multiply, 203 | ColorEffectType::None => { 204 | return Err("Specified an effect_path, but no effect_type".into()); 205 | } 206 | }; 207 | let palettes = generate_blend_palettes(&gba_pal_normal, &blend_colors, blend_function) 208 | .into_iter() 209 | .map(|x| PaletteData::new(x)) 210 | .collect(); 211 | 212 | Ok(palettes) 213 | } else if let ColorEffectType::None = world.effect_type { 214 | Ok(Vec::new()) 215 | } else { 216 | Err("Specified an effect_type, but no effect_path".into()) 217 | } 218 | } 219 | 220 | fn generate_world_data(world: &WorldResourceInfo) -> Result> { 221 | let special_is_4bpp = world.id != WorldId::TomsDiner; 222 | let max_colors = 240; 223 | 224 | let mut bank = ImageBank::new(max_colors, special_is_4bpp); 225 | let mut rooms = Vec::new(); 226 | 227 | // render actual level maps 228 | let map_surfs = render_world_to_16bit_surfaces(world)?; 229 | for surf in map_surfs.into_iter() { 230 | let tile8_is_blended = |_, _| true; 231 | rooms.push(bank.process_world_map(&surf, false, tile8_is_blended, ROOM_SIZE)?); 232 | } 233 | // animated tiles... might get crowded 234 | // TODO: save grid's.. grid 235 | let _anim = generate_animtiles(world, &mut bank)?; 236 | 237 | // FIXME: subtle: starts converting SbEntries on its own, needs refactor 238 | let skybox_layer = generate_skybox(world, &mut bank)?; 239 | 240 | let pal_bank_ofs = bank.palette_bank.palettes_blend.len(); 241 | 242 | let mut bg_fg_layers: Vec = rooms 243 | .iter() 244 | .map(|b| b.gba_metamap_and_roomdata(pal_bank_ofs)) 245 | .map(|(meta, map)| Layer { 246 | map: map.compress().unwrap(), 247 | meta, 248 | }) 249 | .collect(); 250 | let bg_layer = bg_fg_layers.remove(0).into(); 251 | let fg_layer = bg_fg_layers.pop(); 252 | 253 | let pal = compute_blend_effects(world, &mut bank)?; 254 | 255 | let img = bank.gba_patterns(false).compress()?; 256 | let img_special = bank.gba_patterns(true).compress()?; 257 | 258 | let world_data = WorldData { 259 | id: world.id, 260 | name: world.name, 261 | pal, 262 | img, 263 | img_special, 264 | bg_layer, 265 | fg_layer, 266 | skybox_layer, 267 | // TODO anim: Box::leak(anim.into_boxed_slice()), anim_grid 268 | music: world.songs.clone(), 269 | }; 270 | 271 | Ok(world_data) 272 | } 273 | -------------------------------------------------------------------------------- /internal/flowergal-buildtools/src/tile_generation/image_bank.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2021 lifning, licensed under the GNU Affero General Public License version 3. 2 | 3 | use std::error::Error; 4 | 5 | use sdl2::rect::Rect; 6 | use sdl2::surface::Surface; 7 | 8 | use flowergal_proj_config::resources::{PaletteData, TilePatterns}; 9 | 10 | use crate::tile_generation::tile_pixels::TilePixelsBank; 11 | use crate::tile_generation::{sdl_support, traverse_16_grid_as_8, Grid, PaletteBank, Pattern, PatternBank, RoomBank, SbEntry, PAL_LEN, TILE_H, TILE_W, PalBankId}; 12 | 13 | pub struct ImageBank { 14 | // charblock 0 15 | pub pattern_bank: PatternBank, 16 | // charblock 1 17 | pub pattern_bank_special: PatternBank, 18 | pub palette_bank: PaletteBank, 19 | pub tile_pixels_bank: TilePixelsBank, 20 | pub tile_pixels_bank_special: TilePixelsBank, 21 | pub special_is_4bpp: bool, 22 | } 23 | 24 | impl ImageBank { 25 | pub fn new(max_colors: usize, special_is_4bpp: bool) -> Self { 26 | assert!(max_colors <= 256); 27 | ImageBank { 28 | pattern_bank: PatternBank::new(true), 29 | pattern_bank_special: PatternBank::new(special_is_4bpp), 30 | palette_bank: PaletteBank::new(max_colors), 31 | tile_pixels_bank: TilePixelsBank::new(), 32 | tile_pixels_bank_special: TilePixelsBank::new(), 33 | special_is_4bpp, 34 | } 35 | } 36 | 37 | pub fn process_world_map( 38 | &mut self, 39 | surf: &Surface, 40 | special: bool, 41 | tile8_is_blended: impl Fn(usize, usize) -> bool, 42 | (room_width, room_height): (usize, usize) 43 | ) -> Result> { 44 | let is_4bpp = !special || self.special_is_4bpp; 45 | 46 | // optimize palette color packing (feeding them to pattern/palette banks in the right order) 47 | if is_4bpp { 48 | let mut tile_pixels_bank_plain = TilePixelsBank::new(); 49 | let mut tile_pixels_bank_blend = TilePixelsBank::new(); 50 | let grid_width = surf.width() as usize / TILE_W; 51 | let grid_height = surf.height() as usize / TILE_H; 52 | for ty in 0..grid_height { 53 | for tx in 0..grid_width { 54 | let pixel_colors = sdl_support::pixels_of_tile(&surf, tx, ty)?; 55 | if tile8_is_blended(tx, ty) { 56 | tile_pixels_bank_blend.try_onboard_tile(&pixel_colors); 57 | } else { 58 | tile_pixels_bank_plain.try_onboard_tile(&pixel_colors); 59 | } 60 | } 61 | } 62 | for (_colors, tiles) in tile_pixels_bank_blend.approximate_optimal_grouping() { 63 | for tile in tiles.iter() { 64 | self.try_onboard_tile_colors(&tile.pixel_colors, true, special, true) 65 | .ok_or_else(|| self.error(0, 0xffff).unwrap_err())?; 66 | } 67 | } 68 | for (_colors, tiles) in tile_pixels_bank_plain.approximate_optimal_grouping() { 69 | /* TODO: so the palette is in a sorted order? currently breaks. 70 | self.palette_bank 71 | .try_onboard_colors(&colors.into_iter().collect::>()) 72 | .ok_or("Couldn't onboard palette colors during palette optimization pass")?;*/ 73 | for tile in tiles.iter() { 74 | self.try_onboard_tile_colors(&tile.pixel_colors, true, special, false) 75 | .ok_or_else(|| self.error(0xffff, 0).unwrap_err())?; 76 | } 77 | } 78 | } 79 | 80 | let mut metamap = RoomBank::new(surf.size(), (room_width, room_height), is_4bpp); 81 | for ry in 0..(metamap.map_height / room_height) { 82 | for rx in 0..(metamap.map_width / room_width) { 83 | for (x, y) in traverse_16_grid_as_8(room_width, room_height) { 84 | let tx = (rx * room_width) + x; 85 | let ty = (ry * room_height) + y; 86 | let pixel_colors = sdl_support::pixels_of_tile(&surf, tx, ty)?; 87 | 88 | let blend = tile8_is_blended(tx, ty); 89 | if let Some(mapsel) = self.try_onboard_tile_colors(&pixel_colors, true, special, blend) 90 | { 91 | metamap.set_sbe(tx, ty, mapsel); 92 | } else { 93 | self.error(tx, ty)?; 94 | } 95 | } 96 | } 97 | } 98 | 99 | Ok(metamap) 100 | } 101 | 102 | pub fn process_image_region( 103 | &mut self, 104 | surf: &Surface, 105 | rect: Option, 106 | deduplicate: bool, 107 | special: bool, 108 | blend: bool, 109 | ) -> Result> { 110 | let rect = rect.unwrap_or_else(|| surf.rect()); 111 | let grid_size = ( 112 | rect.width() as usize / TILE_W, 113 | rect.height() as usize / TILE_H, 114 | ); 115 | let ofsx = rect.x() as usize / TILE_W; 116 | let ofsy = rect.y() as usize / TILE_H; 117 | let is_4bpp = !special || self.special_is_4bpp; 118 | let mut grid = Grid::new(grid_size, is_4bpp); 119 | 120 | for ty in 0..grid.grid_height { 121 | for tx in 0..grid.grid_width { 122 | let pixel_colors = sdl_support::pixels_of_tile(&surf, tx + ofsx, ty + ofsy)?; 123 | if let Some(mapsel) = 124 | self.try_onboard_tile_colors(&pixel_colors, deduplicate, special, blend) 125 | { 126 | grid.set_sbe(tx, ty, mapsel); 127 | } else { 128 | self.error(tx, ty)?; 129 | } 130 | } 131 | } 132 | 133 | Ok(grid) 134 | } 135 | 136 | pub fn try_onboard_tile_colors( 137 | &mut self, 138 | pixel_colors: &[gba::Color], 139 | deduplicate: bool, 140 | special: bool, 141 | blend: bool, 142 | ) -> Option { 143 | if let Some((mut pattern, mut palbank)) = self.palette_bank.try_onboard_colors(pixel_colors, blend) 144 | { 145 | let pattern_bank; 146 | if special { 147 | if !self.special_is_4bpp { 148 | // FIXME: how to *actually* handle this weird case with PalBankId? 149 | // for now assuming that it's blended; i think the only 8bpp specials are. 150 | let palbank_index = if let PalBankId::Blended(x) = palbank { 151 | x 152 | } else { 153 | unimplemented!("non-blended 8bpp tiles??") 154 | }; 155 | for x in pattern.0.iter_mut() { 156 | *x += palbank_index * PAL_LEN; 157 | } 158 | palbank = PalBankId::Blended(0); 159 | } 160 | pattern_bank = &mut self.pattern_bank_special; 161 | } else { 162 | pattern_bank = &mut self.pattern_bank; 163 | }; 164 | 165 | let option_sb_entry = if deduplicate { 166 | pattern_bank.try_onboard_pattern(pattern, palbank) 167 | } else { 168 | pattern_bank.try_onboard_without_reduction(pattern, palbank) 169 | }; 170 | if let Some(sb_entry) = option_sb_entry { 171 | return Some(sb_entry); 172 | } 173 | } 174 | None 175 | } 176 | 177 | fn collect_patterns<'a, T: From<&'a Pattern>>(pattern_bank: &'a PatternBank) -> &'static [T] { 178 | let mut vec = Vec::with_capacity(pattern_bank.patterns.len()); 179 | for pattern in &pattern_bank.patterns { 180 | vec.push(pattern.into()) 181 | } 182 | Box::leak(vec.into_boxed_slice()) 183 | } 184 | 185 | pub fn gba_patterns(&self, special: bool) -> TilePatterns { 186 | if special { 187 | if self.special_is_4bpp { 188 | TilePatterns::Text(Self::collect_patterns(&self.pattern_bank_special)) 189 | } else { 190 | TilePatterns::Affine(Self::collect_patterns(&self.pattern_bank_special)) 191 | } 192 | } else { 193 | TilePatterns::Text(Self::collect_patterns(&self.pattern_bank)) 194 | } 195 | } 196 | 197 | pub fn gba_palette_full(&self) -> PaletteData { 198 | let mut vec = Vec::new(); 199 | 200 | for pal in self.palette_bank.palettes_blend.iter() 201 | .chain(self.palette_bank.palettes_plain.iter()) 202 | { 203 | for c in &pal.colors { 204 | vec.push(*c); 205 | } 206 | // each line must be the same length in the output, or else subsequent lines will be wrong 207 | // TODO: consider 2D array? 208 | for _ in pal.colors.len()..16 { 209 | vec.push(gba::Color(0)); 210 | } 211 | } 212 | 213 | while *vec.last().unwrap_or(&gba::Color(1)) == gba::Color(0) { 214 | vec.pop(); 215 | } 216 | 217 | PaletteData::new(Box::leak(vec.into_boxed_slice())) 218 | } 219 | 220 | pub fn blend_palette_size(&self) -> usize { 221 | if self.palette_bank.palettes_blend.is_empty() { 222 | 0 223 | } else { 224 | let padded = self.palette_bank.palettes_blend.len() * 16; 225 | let last_len = self.palette_bank.palettes_blend.last().unwrap().colors.len(); 226 | padded - (16 - last_len) 227 | } 228 | } 229 | 230 | fn error(&self, tx: usize, ty: usize) -> Result<(), Box> { 231 | for (i, pal) in self.palette_bank.palettes_plain.iter().enumerate() { 232 | eprintln!( 233 | "palette {} length: {}/{}", 234 | i, 235 | pal.colors.len(), 236 | pal.max_colors 237 | ); 238 | } 239 | Err(format!( 240 | "Could not add tile at ({}, {}); palettes {}+{}/{}, images {}/{}, special {}/{}", 241 | tx * TILE_W, 242 | ty * TILE_H, 243 | self.palette_bank.palettes_plain.len(), 244 | self.palette_bank.palettes_blend.len(), 245 | self.palette_bank.max_palettes, 246 | self.pattern_bank.patterns.len(), 247 | self.pattern_bank.max_patterns, 248 | self.pattern_bank_special.patterns.len(), 249 | self.pattern_bank_special.max_patterns, 250 | ).into()) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/memory.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::size_of_in_element_count)] 2 | 3 | /// # GBA memory map & timing (courtesy GBATEK) 4 | /// 5 | /// |Region |Bus |Read len |Write len|Cycles |Offset range | 6 | /// |------------|-----|---------|---------|---------|---------------------| 7 | /// | BIOS ROM | 32 | 8/16/32 | - | 1/1/1 | `000_0000-000_3fff` | 8 | /// | IWRAM 32K | 32 | 8/16/32 | 8/16/32 | 1/1/1 | `300_0000-300_7fff` | 9 | /// | I/O Regs | 32 | 8/16/32 | 8/16/32 | 1/1/1 | `400_0000-400_03fe` | 10 | /// | OAM | 32 | 8/16/32 |- /16/32 | 1/1/1`*`| `700_0000-700_03ff` | 11 | /// | EWRAM 256K | 16 | 8/16/32 | 8/16/32 | 3/3/6 | `200_0000-203_ffff` | 12 | /// | Pal. RAM | 16 | 8/16/32 |- /16/32 | 1/1/2`*`| `500_0000-500_03ff` | 13 | /// | VRAM | 16 | 8/16/32 |- /16/32 | 1/1/2`*`| `600_0000-601_7fff` | 14 | /// | Cart (WS0) | 16 | 8/16/32 |- /16/32 | 5/5/8`~`| `800_0000-9ff_ffff` | 15 | /// | Cart (WS1) | " | " | " | " | `a00_0000-bff_ffff` | 16 | /// | Cart (WS2) | " | " | " | " | `c00_0000-dff_ffff` | 17 | /// | Cart SRAM | 8 | 8/ - / -| 8/ - / -| 5/ -/ - | `e00_0000-e00_ffff` | 18 | /// 19 | /// `*`: add one cycle if the CPU tries to access VRAM while the GBA is drawing 20 | /// `~`: sequential accesses use a different waitstate, depending on value of WAITCNT. 21 | /// for our value, i believe ROM is more like 4/4/6, where 6 = 1(overhead) + 4(ws0a) + 1(ws0b). 22 | /// generally, reading n halfwords would cost 4+n cycles (plus write depending on destination). 23 | 24 | use core::mem::size_of; 25 | use gba::io::dma::{DMAControlSetting, DMASrcAddressControl, DMAStartTiming, DMA3}; 26 | 27 | // TODO: detect addresses in SRAM range and force those to byte-at-a-time copy 28 | 29 | #[no_mangle] 30 | #[no_builtins] 31 | #[link_section = ".iwram"] 32 | pub unsafe extern "aapcs" fn __aeabi_memcpy(mut dest: *mut u8, mut src: *const u8, mut n: usize) { 33 | // No matter what, we at least want half-word-alignment for our optimization paths. 34 | if dest as usize & 0b1 != 0 && n != 0 { 35 | *dest = *src; 36 | dest = dest.add(1); 37 | src = src.add(1); 38 | n -= 1; 39 | } 40 | match (dest as usize ^ src as usize) & 0b11 { 41 | // Same alignment, might just need the first half-word and then hand off to _memcpy4 42 | 0b00 => { 43 | if dest as usize & 0b10 != 0 { 44 | asm!( 45 | "subs {tmp}, {n}, #2", 46 | "movge {n}, {tmp}", 47 | "ldrhge {tmp}, [{src}], #2", 48 | "strhge {tmp}, [{dest}], #2", 49 | dest = inout(reg) dest, 50 | src = inout(reg) src, 51 | n = inout(reg) n, 52 | tmp = out(reg) _, 53 | ); 54 | } 55 | __aeabi_memcpy4(dest, src, n) 56 | }, 57 | // Both have 2-alignment, so 16-bit register transfer is okay. 58 | 0b10 => asm!( 59 | "1:", 60 | "subs {tmp}, {n}, #2", 61 | "movge {n}, {tmp}", 62 | "ldrhge {tmp}, [{src}], #2", 63 | "strhge {tmp}, [{dest}], #2", 64 | "bge 1b", 65 | "cmp {n}, #1", 66 | "ldrbeq {tmp}, [{src}], #1", 67 | "strbeq {tmp}, [{dest}], #1", 68 | dest = inout(reg) dest => _, 69 | src = inout(reg) src => _, 70 | n = inout(reg) n => _, 71 | tmp = out(reg) _, 72 | options(nostack) 73 | ), 74 | // Misaligned at the byte level, one byte at a time copy 75 | 0b01 | 0b11 => for i in 0..n { 76 | *dest.add(i) = *src.add(i); 77 | } 78 | _ => unreachable!(), 79 | } 80 | } 81 | 82 | #[no_mangle] 83 | #[no_builtins] 84 | #[link_section = ".iwram"] 85 | pub unsafe extern "aapcs" fn __aeabi_memcpy4(dest: *mut u8, src: *const u8, n: usize) { 86 | // We are guaranteed 4-alignment, so 32-bit register transfer is okay. 87 | asm!( 88 | "1:", 89 | "subs r8, {n}, #32", 90 | "movge {n}, r8", 91 | "ldmiage {src}!, {{r0-r5, r7-r8}}", 92 | "stmiage {dest}!, {{r0-r5, r7-r8}}", 93 | "bge 1b", 94 | "subs r8, {n}, #16", 95 | "movge {n}, r8", 96 | "ldmiage {src}!, {{r0-r3}}", 97 | "stmiage {dest}!, {{r0-r3}}", 98 | "subs r8, {n}, #8", 99 | "movge {n}, r8", 100 | "ldmiage {src}!, {{r0-r1}}", 101 | "stmiage {dest}!, {{r0-r1}}", 102 | "subs r8, {n}, #4", 103 | "movge {n}, r8", 104 | "ldrge r0, [{src}], #4", 105 | "strge r0, [{dest}], #4", 106 | "subs r8, {n}, #2", 107 | "movge {n}, r8", 108 | "ldrhge r0, [{src}], #2", 109 | "strhge r0, [{dest}], #2", 110 | "cmp {n}, #1", 111 | "ldrbeq r0, [{src}], #1", 112 | "strbeq r0, [{dest}], #1", 113 | dest = inout(reg) dest => _, 114 | src = inout(reg) src => _, 115 | n = inout(reg) n => _, 116 | out("r0") _, 117 | out("r1") _, 118 | out("r2") _, 119 | out("r3") _, 120 | out("r4") _, 121 | out("r5") _, 122 | out("r7") _, 123 | out("r8") _, 124 | options(nostack) 125 | ); 126 | } 127 | 128 | #[no_mangle] 129 | #[no_builtins] 130 | #[link_section = ".iwram"] 131 | pub unsafe extern "aapcs" fn __aeabi_memset(mut dest: *mut u8, mut n: usize, c: i32) { 132 | let byte = (c as u32) & 0xff; 133 | // No matter what, we at least want half-word-alignment for our optimization paths. 134 | if dest as usize & 0b1 != 0 && n != 0 { 135 | *dest = byte as u8; 136 | dest = dest.add(1); 137 | n -= 1; 138 | } 139 | let c = (byte << 24) | (byte << 16) | (byte << 8) | byte; 140 | if dest as usize & 0b10 != 0 { 141 | asm!( 142 | "subs {tmp}, {n}, #2", 143 | "movge {n}, {tmp}", 144 | "strhge {c}, [{dest}], #2", 145 | dest = inout(reg) dest, 146 | c = in(reg) c, 147 | n = inout(reg) n, 148 | tmp = out(reg) _, 149 | ); 150 | } 151 | memset_word(dest as *mut u32, n, c) 152 | } 153 | 154 | #[no_mangle] 155 | #[no_builtins] 156 | #[link_section = ".iwram"] 157 | pub unsafe extern "aapcs" fn __aeabi_memset4(dest: *mut u8, n: usize, c: i32) { 158 | let byte = (c as u32) & 0xff; 159 | let c = (byte << 24) | (byte << 16) | (byte << 8) | byte; 160 | memset_word(dest as *mut u32, n, c) 161 | } 162 | 163 | #[link_section = ".iwram"] 164 | pub unsafe fn memset_word(dest: *mut u32, n_bytes: usize, c: u32) { 165 | asm!( 166 | "mov r1, r0", 167 | "mov r2, r0", 168 | "mov r3, r0", 169 | "mov r4, r0", 170 | "mov r5, r0", 171 | "mov r7, r0", 172 | "mov r8, r0", 173 | "1:", 174 | "subs {tmp}, {n}, #32", 175 | "movge {n}, {tmp}", 176 | "stmiage {dest}!, {{r0-r5, r7, r8}}", 177 | "bge 1b", 178 | "subs {tmp}, {n}, #16", 179 | "movge {n}, {tmp}", 180 | "stmiage {dest}!, {{r0-r3}}", 181 | "subs {tmp}, {n}, #8", 182 | "movge {n}, {tmp}", 183 | "stmiage {dest}!, {{r0-r1}}", 184 | "subs {tmp}, {n}, #4", 185 | "movge {n}, {tmp}", 186 | "strge r0, [{dest}], #4", 187 | "subs {tmp}, {n}, #2", 188 | "movge {n}, {tmp}", 189 | "strhge r0, [{dest}], #2", 190 | "subs {tmp}, {n}, #1", 191 | "moveq {n}, {tmp}", 192 | "strbeq r0, [{dest}], #1", 193 | dest = inout(reg) dest => _, 194 | n = inout(reg) n_bytes => _, 195 | tmp = out(reg) _, 196 | in("r0") c, 197 | out("r1") _, 198 | out("r2") _, 199 | out("r3") _, 200 | out("r4") _, 201 | out("r5") _, 202 | out("r7") _, 203 | out("r8") _, 204 | options(nostack) 205 | ); 206 | } 207 | 208 | pub trait MemoryOps { 209 | unsafe fn copy_slice_to_address(src: &[T], dest: usize); 210 | unsafe fn fill_slice_32(dest: &mut [u32], value: u32); 211 | unsafe fn fill_slice_16(_dest: &mut [u16], _value: u16) { unimplemented!(); } 212 | unsafe fn zero_block(dest: usize, count: usize) { 213 | let dest = core::slice::from_raw_parts_mut(dest as *mut u32, count / size_of::()); 214 | Self::fill_slice_32(dest, 0); 215 | } 216 | } 217 | 218 | pub struct CoreLib; 219 | 220 | impl MemoryOps for CoreLib { 221 | unsafe fn copy_slice_to_address(src: &[T], dest: usize) { 222 | core::ptr::copy_nonoverlapping(src.as_ptr(), dest as *mut T, src.len()) 223 | // core::slice::from_raw_parts_mut(dest as *mut T, src.len()).copy_from_slice(src); 224 | } 225 | 226 | unsafe fn fill_slice_32(dest: &mut [u32], value: u32) { 227 | dest.fill(value); 228 | // core::ptr::write_bytes(dest.as_mut_ptr(), value, dest.len()); 229 | } 230 | } 231 | 232 | pub struct BiosCalls; 233 | 234 | impl MemoryOps for BiosCalls { 235 | unsafe fn copy_slice_to_address(src: &[T], dest: usize) { 236 | let src_ptr = src.as_ptr() as *const u32; 237 | let dest_ptr = dest as *mut u32; 238 | let byte_count = src.len() * size_of::(); 239 | 240 | // enforce alignment 241 | if !cfg!(release) { 242 | assert_eq!(src_ptr as usize & 3, 0); 243 | assert_eq!(dest_ptr as usize & 3, 0); 244 | assert_eq!(byte_count & 3, 0); 245 | } 246 | 247 | let block_byte_count = byte_count & !31; 248 | let block_word_count = block_byte_count / size_of::(); 249 | 250 | if block_word_count != 0 { 251 | gba::bios::cpu_fast_set(src_ptr, dest_ptr, block_word_count as u32, false); 252 | } 253 | 254 | let remainder_byte_count = byte_count & 31; 255 | let remainder_word_count = remainder_byte_count / size_of::(); 256 | 257 | if remainder_word_count != 0 { 258 | gba::bios::cpu_set32( 259 | src_ptr.add(block_word_count), 260 | dest_ptr.add(block_word_count), 261 | remainder_word_count as u32, 262 | false, 263 | ); 264 | } 265 | } 266 | 267 | unsafe fn fill_slice_32(dest: &mut [u32], value: u32) { 268 | let count = dest.len() as u32; 269 | assert_eq!(count & 31, 0); // 32-byte blocks only 270 | gba::bios::cpu_fast_set(&value, dest.as_mut_ptr() as *mut u32, count, true); 271 | } 272 | 273 | unsafe fn fill_slice_16(dest: &mut [u16], value: u16) { 274 | let count = (dest.len() * size_of::() / size_of::()) as u32; 275 | assert_eq!(count & 31, 0); // 32-byte blocks only 276 | let src = value as u32 | ((value as u32) << 16); 277 | gba::bios::cpu_fast_set(&src, dest.as_mut_ptr() as *mut u32, count, true); 278 | } 279 | } 280 | 281 | impl MemoryOps for DMA3 { 282 | unsafe fn copy_slice_to_address(src: &[T], dest: usize) { 283 | let src_ptr = src.as_ptr() as *const u32; 284 | let dest_ptr = dest as *mut u32; 285 | let byte_count = src.len() * size_of::(); 286 | 287 | if !cfg!(release) { 288 | // enforce alignment 289 | assert_eq!(src_ptr as usize & 3, 0); 290 | assert_eq!(dest_ptr as usize & 3, 0); 291 | assert_eq!(byte_count & 3, 0); 292 | } 293 | 294 | let word_count = (byte_count / size_of::()) as u16; 295 | Self::set_source(src.as_ptr() as *const u32); 296 | Self::set_dest(dest as *mut u32); 297 | Self::set_count(word_count); 298 | Self::set_control( 299 | DMAControlSetting::new() 300 | .with_source_address_control(DMASrcAddressControl::Increment) 301 | .with_use_32bit(true) 302 | .with_start_time(DMAStartTiming::Immediate) 303 | .with_enabled(true), 304 | ); 305 | asm!("NOP; NOP", options(nostack)); 306 | } 307 | 308 | unsafe fn fill_slice_32(dest: &mut [u32], value: u32) { 309 | let count = dest.len() as u16; 310 | Self::fill32(&value, dest.as_mut_ptr() as *mut u32, count); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/render/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_upper_case_globals)] 4 | 5 | /// # VRAM division by charblock: 6 | /// 0. level charblock 7 | /// 1. extra charblock (windmill) 8 | /// 2. hud charblock 9 | /// 3. screenblocks (add 24 to all of these): 10 | /// 0. and 1: level geometry (256x512 or 512x256 for scrolling) 11 | /// 2. and 3: foreground (as above) 12 | /// 4. and 5: skybox, or affine 512x512 for windmill 13 | /// 6. hud 14 | /// 7. ??? 15 | /// 4. and 5. obj (subdivisions TBD) 16 | /// 17 | 18 | pub mod palette; 19 | 20 | use core::mem::{size_of, transmute}; 21 | 22 | use gba::vram::{Tile4bpp, Tile8bpp}; 23 | use gba::vram::{CHAR_BASE_BLOCKS, SCREEN_BASE_BLOCKS}; 24 | use gba::Color; 25 | 26 | use gba::io::display::{ 27 | DisplayControlSetting, DisplayStatusSetting, MosaicSetting, DISPCNT, DISPSTAT, MOSAIC, 28 | VBLANK_SCANLINE, VCOUNT, 29 | }; 30 | use gba::io::window::{InsideWindowSetting, WININ, OutsideWindowSetting}; 31 | use gba::palram::{PALRAM_BG, PALRAM_OBJ}; 32 | use gba::oam::{ObjectAttributes, OBJAttr0, OBJAttr1, OBJAttr2, ObjectRender, ObjectMode, ObjectShape, ObjectSize}; 33 | 34 | use flowergal_proj_config::resources::*; 35 | 36 | use crate::timers::GbaTimer; 37 | use crate::render::palette::{NO_EFFECT, VCOUNT_SEQUENCE, VCOUNT_SEQUENCE_LEN, NO_COLORS, TEXTBOX_VCOUNTS}; 38 | use crate::memory::MemoryOps; 39 | use core::ops::Range; 40 | use gba::io::dma::DMA3; 41 | 42 | #[derive(PartialEq)] 43 | pub enum Platform { 44 | Hardware, 45 | MGBA, 46 | VBA, 47 | NoCash, 48 | } 49 | 50 | pub struct GbaRenderer { 51 | palette_normal_rom: &'static [Color], 52 | /// used for applying overlay/hardlight gradients every so many scanlines 53 | // TODO: compute obj blends 54 | palette_effect_rom: &'static [PaletteData], 55 | /// applied during hblank at vcount=113 and reverted at vcount=153. 56 | /// should be computed based on palette_normal or `palette_effect[133 / PAL_FX_SCANLINES]` 57 | palette_textbox_rom: &'static [Color], 58 | /// chosen during vcount irq, while waiting for timer1 irq. 59 | next_copy_pal: &'static [Color], 60 | /// workaround for next_copy_pal being insufficient alone for textbox_end when blend enabled 61 | next_copy_extra_norm_range: Range, 62 | dispstat: DisplayStatusSetting, 63 | showing_textbox: bool, 64 | showing_effect: bool, 65 | pub frame_counter: u32, 66 | vcount_index: usize, 67 | perf_log: [u32; VCOUNT_SEQUENCE_LEN], 68 | pub platform: Platform, 69 | // shadow_oam: ShadowOam, 70 | } 71 | 72 | const fn palram_bg_slice() -> &'static mut [Color] { 73 | unsafe { 74 | &mut *core::ptr::slice_from_raw_parts_mut( 75 | PALRAM_BG.index_unchecked(0).to_usize() as *mut Color, 76 | PALRAM_BG.len() 77 | ) 78 | } 79 | } 80 | 81 | const fn palram_obj_slice() -> &'static mut [Color] { 82 | unsafe { 83 | &mut *core::ptr::slice_from_raw_parts_mut( 84 | PALRAM_OBJ.index_unchecked(0).to_usize() as *mut Color, 85 | PALRAM_OBJ.len() 86 | ) 87 | } 88 | } 89 | 90 | impl GbaRenderer { 91 | pub const fn new() -> Self { 92 | GbaRenderer { 93 | palette_normal_rom: NO_COLORS, 94 | palette_effect_rom: NO_EFFECT, 95 | palette_textbox_rom: NO_COLORS, 96 | next_copy_pal: NO_COLORS, 97 | next_copy_extra_norm_range: 0..0, 98 | dispstat: DisplayStatusSetting::new() 99 | .with_vblank_irq_enable(true) 100 | .with_vcounter_irq_enable(true), 101 | showing_textbox: false, 102 | showing_effect: false, 103 | frame_counter: 0, 104 | vcount_index: 0, 105 | perf_log: [0; VCOUNT_SEQUENCE_LEN], 106 | platform: Platform::Hardware, 107 | } 108 | } 109 | 110 | fn detect_platform() -> Platform { 111 | if let Some(_) = gba::mgba::MGBADebug::new() { 112 | return Platform::MGBA; 113 | } 114 | 115 | unsafe { 116 | let rom_src: &[u32] = &[0x900dc0de]; 117 | let bios_src: &[u32] = core::slice::from_raw_parts(core::ptr::null(), 1); 118 | let mut dest: [u32; 2] = [!0, !0]; 119 | DMA3::copy_slice_to_address(rom_src, dest.as_mut_ptr() as usize); 120 | DMA3::copy_slice_to_address(bios_src, dest.as_mut_ptr().add(1) as usize); 121 | if dest[1] == 0 { 122 | return Platform::VBA; 123 | } 124 | } 125 | 126 | // FIXME: detect No$GBA debugger? imperfect, can be disabled, detection doesn't work yet 127 | unsafe { 128 | let nocash_id = core::slice::from_raw_parts(0x4fffa00 as *const u8, 16); 129 | if nocash_id[2] == '$' as u8 { 130 | (0x4fffa10 as *mut *const u8).write_volatile("hello".as_ptr()); 131 | return Platform::NoCash; 132 | } 133 | } 134 | 135 | Platform::Hardware 136 | } 137 | 138 | pub fn initialize(&mut self) { 139 | self.platform = Self::detect_platform(); 140 | 141 | gba::io::window::WINOUT.write(OutsideWindowSetting::new() 142 | .with_outside_bg0(true) 143 | .with_outside_bg1(true) 144 | .with_outside_bg2(true) 145 | .with_outside_bg3(true) 146 | .with_outside_color_special(true) 147 | .with_obj_win_bg0(true) 148 | .with_obj_win_bg1(true) 149 | .with_obj_win_bg2(false) 150 | .with_obj_win_bg3(true) 151 | .with_obj_win_color_special(true) 152 | ); 153 | 154 | let sprite_chars = unsafe { 155 | let ptr = CHAR_BASE_BLOCKS.get(4).unwrap().to_usize() as *mut Tile4bpp; 156 | core::slice::from_raw_parts_mut(ptr, 0x4000 / core::mem::size_of::()) 157 | }; 158 | for char in sprite_chars { 159 | char.0 = [ 160 | 0x11111111, 161 | 0x00000000, 162 | 0x11111111, 163 | 0x00000000, 164 | 0x11111111, 165 | 0x00000000, 166 | 0x11111111, 167 | 0x00000000, 168 | ]; 169 | } 170 | PALRAM_OBJ.get(1).unwrap().write(gba::Color(0xffff)); 171 | 172 | self.update_sprite_attributes(); 173 | } 174 | 175 | fn update_sprite_attributes(&mut self) { 176 | for x in 0..=2 { 177 | for y in 0..=2 { 178 | let shape = match y { 179 | 2 => ObjectShape::Horizontal, 180 | _ => ObjectShape::Square, 181 | }; 182 | let slot = (y * 3 + x) as usize; 183 | gba::oam::write_obj_attributes(slot, ObjectAttributes { 184 | attr0: OBJAttr0::new() 185 | .with_row_coordinate(64 * y) 186 | .with_obj_rendering(ObjectRender::Normal) 187 | .with_obj_mode(ObjectMode::OBJWindow) 188 | .with_obj_shape(shape), 189 | attr1: OBJAttr1::new() 190 | .with_col_coordinate(64 * x + 24) 191 | .with_vflip(self.even_odd_frame()) 192 | .with_obj_size(ObjectSize::Three), 193 | attr2: OBJAttr2::new() 194 | .with_tile_id(0) 195 | .with_priority(0) 196 | .with_palbank(0), 197 | }); 198 | } 199 | } 200 | } 201 | 202 | pub fn vblank(&mut self) { 203 | self.frame_counter += 1; 204 | self.update_sprite_attributes(); 205 | } 206 | 207 | pub fn even_odd_frame(&self) -> bool { 208 | self.frame_counter & 1 != 0 209 | } 210 | 211 | pub fn frame_counter(&self) -> u32 { 212 | self.frame_counter 213 | } 214 | 215 | #[link_section = ".iwram"] 216 | pub fn vcounter(&mut self) { 217 | // TODO: hit every other vcount and only set up timer1 if we're supposed to do a thing? 218 | // fudging the numbers a bit on this 750, but i'm assuming there'll be ~50 cycles overhead 219 | let cycles = match self.platform { 220 | // HACK: workaround for https://github.com/mgba-emu/mgba/issues/1996 221 | // start copying much sooner so it gets done *before* the next hdraw starts. 222 | Platform::MGBA | Platform::VBA => 50, 223 | _ => 750, 224 | }; 225 | GbaTimer::setup_timer1_irq(cycles); 226 | 227 | let mut vcount = VCOUNT.read(); 228 | if vcount >= VBLANK_SCANLINE /*- BLEND_RESOLUTION*/ as u16 { 229 | vcount = 0; 230 | } 231 | 232 | self.next_copy_pal = if self.showing_effect && !self.palette_effect_rom.is_empty() 233 | && !(self.showing_textbox && (TEXTBOX_VCOUNTS[0]..=TEXTBOX_VCOUNTS[1]).contains(&vcount)) { 234 | self.palette_effect_rom[vcount as usize / BLEND_RESOLUTION].data() 235 | } else if self.showing_textbox && vcount == TEXTBOX_VCOUNTS[0] { 236 | self.palette_textbox_rom 237 | } else if self.showing_textbox && vcount == TEXTBOX_VCOUNTS[1] { 238 | if self.showing_effect && !self.palette_effect_rom.is_empty() { 239 | let pal = self.palette_effect_rom[TEXTBOX_VCOUNTS[1] as usize / BLEND_RESOLUTION].data(); 240 | let next_line_index = (pal.len() & !15) + 16; 241 | if next_line_index < self.palette_normal_rom.len() { 242 | self.next_copy_extra_norm_range = next_line_index..self.palette_normal_rom.len(); 243 | } 244 | pal 245 | } else { 246 | self.palette_normal_rom 247 | } 248 | } else { 249 | NO_COLORS 250 | }; 251 | } 252 | 253 | #[link_section = ".iwram"] 254 | pub fn timer1(&mut self) { 255 | let start = GbaTimer::get_ticks(); 256 | 257 | let pal = self.next_copy_pal; 258 | palram_bg_slice()[0..pal.len()].copy_from_slice(pal); 259 | if !self.next_copy_extra_norm_range.is_empty() { 260 | let mut range = 0..0; 261 | core::mem::swap(&mut self.next_copy_extra_norm_range, &mut range); 262 | palram_bg_slice()[range.clone()].copy_from_slice(&self.palette_normal_rom[range]); 263 | } 264 | 265 | self.perf_log[self.vcount_index] = GbaTimer::get_ticks() - start; 266 | self.vcount_index += 1; 267 | let mut next_vcount = VCOUNT_SEQUENCE[self.vcount_index]; 268 | if next_vcount == 0xFF { 269 | #[cfg(feature = "bench_video")] 270 | warn!("pal copy: {:?}", &self.perf_log[..self.vcount_index]); 271 | self.vcount_index = 0; 272 | next_vcount = VCOUNT_SEQUENCE[self.vcount_index]; 273 | } 274 | DISPSTAT.write(self.dispstat.with_vcount_setting(next_vcount)); 275 | } 276 | 277 | pub fn set_color_effect_shown(&mut self, showing_effect: bool) { 278 | self.showing_effect = showing_effect; 279 | if !showing_effect { 280 | let pal = self.palette_normal_rom; 281 | let effect_len = self.palette_effect_rom.first() 282 | .map(|x| x.0.len() * 2) 283 | .unwrap_or(0); 284 | unsafe { 285 | palram_bg_slice()[..effect_len].copy_from_slice(&pal[..effect_len]) 286 | } 287 | } 288 | } 289 | 290 | pub fn set_textbox_shown(&mut self, show: bool) { 291 | self.showing_textbox = show; 292 | if !show { 293 | let pal = self.palette_normal_rom; 294 | let effect_len = self.palette_effect_rom.first() 295 | .map(|x| x.0.len() * 2) 296 | .unwrap_or(0); 297 | unsafe { 298 | palram_bg_slice()[effect_len..pal.len()] 299 | .copy_from_slice(&pal[effect_len..pal.len()]) 300 | } 301 | } 302 | } 303 | 304 | pub fn set_normal_colors_bg(&mut self, index: usize, colors: &[gba::Color]) { 305 | palram_bg_slice()[index..(index + colors.len())].copy_from_slice(colors); 306 | } 307 | 308 | pub fn load_world_palettes(&mut self, world_pal: &WorldPalettes) { 309 | self.palette_normal_rom = world_pal.normal_palette.data(); 310 | self.palette_effect_rom = world_pal.blended_palettes; 311 | self.palette_textbox_rom = world_pal.textbox_blend_palette.data(); 312 | self.showing_effect = !world_pal.blended_palettes.is_empty(); 313 | self.set_normal_colors_bg(0, self.palette_normal_rom); 314 | palram_bg_slice()[self.palette_normal_rom.len()..240].fill(gba::Color(0)); 315 | } 316 | 317 | pub fn load_bg_tiles(&self, charblock: u16, tiles: &[T]) { 318 | assert!(charblock < 4); 319 | assert!(tiles.len() * size_of::() <= 256 * 8 * 8); 320 | info!("load_bg_tiles {}", charblock); 321 | let dest_addr = CHAR_BASE_BLOCKS.index(charblock as usize).to_usize(); 322 | unsafe { 323 | core::slice::from_raw_parts_mut(dest_addr as *mut T, tiles.len()) 324 | .copy_from_slice(tiles); 325 | } 326 | } 327 | 328 | pub fn load_bg_tiles_lz77(&self, charblock: u16, data: &[u32]) { 329 | assert!(charblock < 4); 330 | assert!(data[0] >> 8 <= 256 * 8 * 8); 331 | let dest_addr = CHAR_BASE_BLOCKS.index(charblock as usize).to_usize(); 332 | unsafe { 333 | gba::bios::lz77_uncomp_16bit(data.as_ptr(), dest_addr as *mut u16); 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/world.rs: -------------------------------------------------------------------------------- 1 | use core::mem::size_of; 2 | 3 | use gba::bios::BgAffineSetParams; 4 | use gba::io::background::{ 5 | BGSize, BackgroundControlSetting, BG1CNT, BG1HOFS, BG1VOFS, BG2CNT, BG2HOFS, BG2VOFS, BG3CNT, 6 | BG3HOFS, BG3VOFS, 7 | }; 8 | use gba::io::display::{DisplayControlSetting, DisplayMode, DISPCNT}; 9 | use gba::vram::SCREEN_BASE_BLOCKS; 10 | 11 | use voladdress::VolAddress; 12 | 13 | use flowergal_proj_config::resources::{Layer, RoomData, TextScreenblockEntry, TilePatterns, WorldData}; 14 | use flowergal_proj_config::WorldId; 15 | 16 | use flowergal_runtime::{Driver, MemoryOps, CoreLib}; 17 | 18 | use flowergal_proj_assets::MUSIC_DATA; 19 | use gba::io::color_blend::{ 20 | AlphaBlendingSetting, ColorEffectSetting, ColorSpecialEffect, BLDALPHA, BLDCNT, 21 | }; 22 | use gba::io::window::OutsideWindowSetting; 23 | 24 | const WORLD_CHARBLOCK_ID: u16 = 0; 25 | const WORLD_CHARBLOCK_SPECIAL_ID: u16 = 1; 26 | 27 | const WORLD_SCREENBLOCK_BG_ID: u16 = 24; 28 | const WORLD_SCREENBLOCK_FG_ID: u16 = 26; 29 | const WORLD_SCREENBLOCK_SKYBOX_ID: u16 = 28; 30 | 31 | const WORLD_SCREENBLOCK_BG: VolAddress = unsafe { 32 | SCREEN_BASE_BLOCKS 33 | .index_unchecked(WORLD_SCREENBLOCK_BG_ID as usize) 34 | .cast() 35 | }; 36 | const WORLD_SCREENBLOCK_FG: VolAddress = unsafe { 37 | SCREEN_BASE_BLOCKS 38 | .index_unchecked(WORLD_SCREENBLOCK_FG_ID as usize) 39 | .cast() 40 | }; 41 | const WORLD_SCREENBLOCK_SKYBOX: VolAddress = unsafe { 42 | SCREEN_BASE_BLOCKS 43 | .index_unchecked(WORLD_SCREENBLOCK_SKYBOX_ID as usize) 44 | .cast() 45 | }; 46 | 47 | const TEXT_SCREENBLOCK_TILES: usize = 32; 48 | const ROOM_TILES: usize = 32; 49 | 50 | const BG_HOFS_BASE: u16 = 0; 51 | const BG_VOFS_BASE: u16 = 0; 52 | 53 | #[repr(usize)] 54 | #[derive(Clone, Copy)] 55 | enum ScreenblockAddress { 56 | Background = WORLD_SCREENBLOCK_BG.to_usize(), 57 | Foreground = WORLD_SCREENBLOCK_FG.to_usize(), 58 | Skybox = WORLD_SCREENBLOCK_SKYBOX.to_usize(), 59 | } 60 | 61 | impl ScreenblockAddress { 62 | fn blocks_as_mut_slice(self, n: usize) -> &'static mut [T] { 63 | unsafe { 64 | core::slice::from_raw_parts_mut(self as usize as *mut T, (0x800 * n) / size_of::()) 65 | } 66 | } 67 | } 68 | 69 | enum ScreenblockCorner { 70 | TopLeft, 71 | TopRight, 72 | BottomLeft, 73 | BottomRight, 74 | } 75 | 76 | enum SkyboxBehavior { 77 | None, 78 | Windmill, 79 | } 80 | 81 | impl ScreenblockCorner { 82 | pub fn text_offset(&self) -> usize { 83 | let tile_offset_within = TEXT_SCREENBLOCK_TILES - ROOM_TILES; 84 | let second_block = 0x800; 85 | match self { 86 | ScreenblockCorner::TopLeft => { 87 | (TEXT_SCREENBLOCK_TILES + 1) 88 | * tile_offset_within 89 | * size_of::() 90 | } 91 | ScreenblockCorner::TopRight => { 92 | second_block 93 | + TEXT_SCREENBLOCK_TILES 94 | * tile_offset_within 95 | * size_of::() 96 | } 97 | ScreenblockCorner::BottomLeft => { 98 | second_block + tile_offset_within * size_of::() 99 | } 100 | ScreenblockCorner::BottomRight => { 101 | panic!("text screenblocks in this game don't have a bottom-right!") 102 | } 103 | } 104 | } 105 | } 106 | impl From<(usize, usize)> for ScreenblockCorner { 107 | fn from(pair: (usize, usize)) -> Self { 108 | match pair { 109 | (0, 0) => ScreenblockCorner::TopLeft, 110 | (1, 0) => ScreenblockCorner::TopRight, 111 | (0, 1) => ScreenblockCorner::BottomLeft, 112 | (1, 1) => ScreenblockCorner::BottomRight, 113 | (x, y) => panic!("Not a screenblock position: ({}, {})", x, y), 114 | } 115 | } 116 | } 117 | 118 | pub struct World { 119 | world_data: Option<&'static WorldData>, 120 | frame_count: i32, 121 | current_room_x: usize, 122 | current_room_y: usize, 123 | skybox_anim: SkyboxBehavior, 124 | } 125 | 126 | impl World { 127 | pub fn new() -> Self { 128 | let bg1_settings = BackgroundControlSetting::new() 129 | .with_screen_base_block(WORLD_SCREENBLOCK_BG_ID) 130 | .with_char_base_block(WORLD_CHARBLOCK_ID) 131 | .with_mosaic(true) 132 | .with_bg_priority(2) 133 | .with_size(BGSize::One); 134 | 135 | BG1CNT.write(bg1_settings); 136 | 137 | let bg2_settings = BackgroundControlSetting::new() 138 | .with_screen_base_block(WORLD_SCREENBLOCK_SKYBOX_ID) 139 | .with_char_base_block(WORLD_CHARBLOCK_SPECIAL_ID) 140 | .with_mosaic(true) 141 | .with_bg_priority(3) 142 | .with_size(BGSize::Zero); 143 | 144 | BG2CNT.write(bg2_settings); 145 | 146 | let bg3_settings = BackgroundControlSetting::new() 147 | .with_screen_base_block(WORLD_SCREENBLOCK_FG_ID) 148 | .with_char_base_block(WORLD_CHARBLOCK_ID) 149 | .with_mosaic(true) 150 | .with_bg_priority(1) 151 | .with_size(BGSize::One); 152 | 153 | BG3CNT.write(bg3_settings); 154 | 155 | World { 156 | world_data: None, 157 | frame_count: 0, 158 | current_room_x: 0, 159 | current_room_y: 0, 160 | skybox_anim: SkyboxBehavior::None, 161 | } 162 | } 163 | 164 | pub fn load_world(&mut self, data: &'static WorldData) { 165 | self.world_data = Some(data); 166 | 167 | let driver = unsafe { Driver::instance_mut() }; 168 | 169 | if let Some(song_id) = data.music.0.first() { 170 | driver.audio().set_bgm(&MUSIC_DATA[*song_id as usize]); 171 | } 172 | 173 | let renderer = driver.video(); 174 | renderer.load_world_palettes(&data.pal); 175 | 176 | info!( 177 | "bg {} fg {} skybox {}", 178 | data.bg_layer.is_some(), 179 | data.fg_layer.is_some(), 180 | data.skybox_layer 181 | .as_ref() 182 | .map(|sb| { 183 | if let RoomData::Text(_) = sb.map { 184 | "text" 185 | } else { 186 | "affine" 187 | } 188 | }) 189 | .unwrap_or("none"), 190 | ); 191 | 192 | let dispcnt_tmp = DisplayControlSetting::new() 193 | .with_bg0(true) // HUD 194 | .with_bg1(data.bg_layer.is_some()) 195 | .with_bg2(data.skybox_layer.is_some()) 196 | .with_bg3(data.fg_layer.is_some()) 197 | .with_obj(true) 198 | .with_win0(false) 199 | .with_win1(false) 200 | .with_obj_window(true); 201 | 202 | // making up for alignment in screenblock copying. 203 | BG1HOFS.write(BG_HOFS_BASE); 204 | BG1VOFS.write(BG_VOFS_BASE); 205 | BG2HOFS.write(BG_HOFS_BASE); 206 | BG2VOFS.write(BG_VOFS_BASE); 207 | BG3HOFS.write(BG_HOFS_BASE); 208 | BG3VOFS.write(BG_VOFS_BASE); 209 | 210 | match data.img { 211 | TilePatterns::Text(imgs) => { 212 | renderer.load_bg_tiles(WORLD_CHARBLOCK_ID, &imgs[..512.min(imgs.len())]); 213 | } 214 | TilePatterns::TextLz77(data) => { 215 | renderer.load_bg_tiles_lz77(WORLD_CHARBLOCK_ID, data); 216 | } 217 | TilePatterns::AffineLz77(_) | TilePatterns::Affine(_) => { 218 | panic!("Tried to load world with 8bpp main images, not supported"); 219 | } 220 | } 221 | 222 | match data.img_special { 223 | TilePatterns::Text(imgs) => { 224 | DISPCNT.write(dispcnt_tmp.with_mode(DisplayMode::Mode0)); 225 | renderer.load_bg_tiles(WORLD_CHARBLOCK_SPECIAL_ID, &imgs[..512.min(imgs.len())]); 226 | } 227 | TilePatterns::Affine(imgs) => { 228 | DISPCNT.write(dispcnt_tmp.with_mode(DisplayMode::Mode1)); 229 | renderer.load_bg_tiles(WORLD_CHARBLOCK_SPECIAL_ID, &imgs[..256.min(imgs.len())]); 230 | } 231 | TilePatterns::TextLz77(data) => { 232 | DISPCNT.write(dispcnt_tmp.with_mode(DisplayMode::Mode0)); 233 | renderer.load_bg_tiles_lz77(WORLD_CHARBLOCK_SPECIAL_ID, data); 234 | } 235 | TilePatterns::AffineLz77(data) => { 236 | DISPCNT.write(dispcnt_tmp.with_mode(DisplayMode::Mode1)); 237 | renderer.load_bg_tiles_lz77(WORLD_CHARBLOCK_SPECIAL_ID, data); 238 | } 239 | } 240 | 241 | let bg1_settings = BG1CNT.read(); 242 | let bg2_settings = BG2CNT.read(); 243 | if data.id == WorldId::TomsDiner { 244 | BG1CNT.write(bg1_settings); 245 | BG2CNT.write(bg2_settings.with_size(BGSize::Zero)); 246 | BLDCNT.write( 247 | ColorEffectSetting::new() 248 | .with_bg1_1st_target_pixel(true) 249 | .with_bg2_2nd_target_pixel(true) 250 | .with_color_special_effect(ColorSpecialEffect::AlphaBlending), 251 | ); 252 | BLDALPHA.write( 253 | AlphaBlendingSetting::new() 254 | .with_eva_coefficient(4) 255 | .with_evb_coefficient(12), 256 | ); 257 | 258 | 259 | self.skybox_anim = SkyboxBehavior::Windmill; 260 | } 261 | 262 | self.draw_skybox(); 263 | } 264 | 265 | pub fn draw_skybox(&mut self) { 266 | let sb_addr = ScreenblockAddress::Skybox; 267 | sb_addr.blocks_as_mut_slice(2).fill(0u16); 268 | 269 | if let Some(data) = self.world_data { 270 | if let Some(layer) = data.skybox_layer.as_ref() { 271 | let rows = layer.meta.0.len(); 272 | if rows != 0 { 273 | let cols = layer.meta.0[0].len(); 274 | debug!("{} skybox: ({}, {})", data.name, cols, rows); 275 | for row in 0..rows { 276 | for col in 0..cols { 277 | let sb_corner = ScreenblockCorner::from((col, row)); 278 | self.draw_room_layer(sb_addr, sb_corner, layer, row, col); 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | fn write_row(&self, sb_addr: usize, row: usize, entries: &[T]) { 287 | unsafe { 288 | CoreLib::copy_slice_to_address(entries, sb_addr + (row * 64)); 289 | /* 290 | if size_of::() == 2 { 291 | // 4bpp. u16 292 | } else { 293 | // 8bpp, u8 294 | } 295 | */ 296 | } 297 | } 298 | 299 | fn draw_room_layer( 300 | &self, 301 | sb_addr: ScreenblockAddress, 302 | sb_corner: ScreenblockCorner, 303 | layer: &Layer, 304 | room_row: usize, 305 | room_col: usize, 306 | ) { 307 | let Layer { meta, map } = layer; 308 | 309 | let meta_width = meta.0.get(0).map(|x| x.len()).unwrap_or_default(); 310 | if room_row < meta.0.len() && room_col < meta_width { 311 | let room_id = meta.0[room_row][room_col]; 312 | match map { 313 | RoomData::Text(map) => { 314 | let src = &map[room_id as usize].0; 315 | let sb_addr = sb_addr as usize + sb_corner.text_offset(); 316 | for (row, entries) in src.iter().enumerate().take(ROOM_TILES) { 317 | self.write_row(sb_addr, row, entries); 318 | } 319 | } 320 | RoomData::Affine(map) => { 321 | let src = &map[room_id as usize].0; 322 | //let sb_addr = sb_addr as usize + sb_corner.affine_offset(); 323 | let sb_slice = sb_addr.blocks_as_mut_slice(1); 324 | for (row, entries) in src.iter().enumerate().take(ROOM_TILES/2) { 325 | let start = row * entries.len(); 326 | let end = start + entries.len(); 327 | sb_slice[start..end].copy_from_slice(entries); 328 | } 329 | } 330 | RoomData::TextLz77(map) => { 331 | let src = map[room_id as usize]; 332 | let sb_addr = sb_addr as usize + sb_corner.text_offset(); 333 | gba::bios::lz77_uncomp_16bit(src.as_ptr(), sb_addr as *mut u16); 334 | } 335 | } 336 | } 337 | } 338 | 339 | pub fn draw_room(&mut self, room_row: usize, room_col: usize) { 340 | debug!("drawing room: row {}, col {}", room_row, room_col); 341 | // TODO: refactor (draw_current_room separate from a mut set_current_room) 342 | self.current_room_x = room_col; 343 | self.current_room_y = room_row; 344 | if let Some(world_data) = self.world_data { 345 | if let Some(layer) = &world_data.bg_layer { 346 | self.draw_room_layer( 347 | ScreenblockAddress::Background, 348 | ScreenblockCorner::TopLeft, 349 | layer, 350 | room_row, 351 | room_col, 352 | ); 353 | self.draw_room_layer( 354 | ScreenblockAddress::Background, 355 | ScreenblockCorner::TopRight, 356 | layer, 357 | room_row, 358 | room_col + 1, 359 | ); 360 | } 361 | if let Some(layer) = &world_data.fg_layer { 362 | self.draw_room_layer( 363 | ScreenblockAddress::Foreground, 364 | ScreenblockCorner::TopLeft, 365 | layer, 366 | room_row, 367 | room_col, 368 | ); 369 | } 370 | } 371 | } 372 | 373 | pub fn dimensions(&self) -> (usize, usize) { 374 | let meta = &self 375 | .world_data 376 | .as_ref() 377 | .expect("gfx") 378 | .bg_layer 379 | .as_ref() 380 | .expect("bg_layer") 381 | .meta; 382 | let rows = meta.0.len(); 383 | let cols = meta.0.get(0).map(|x| x.len()).unwrap_or_default(); 384 | (cols, rows) 385 | } 386 | 387 | pub fn advance_frame(&mut self) { 388 | self.frame_count += 1; 389 | match self.skybox_anim { 390 | SkyboxBehavior::None => { 391 | BG2HOFS.write(BG_HOFS_BASE); 392 | BG2VOFS.write(BG_VOFS_BASE); 393 | } 394 | SkyboxBehavior::Windmill => { 395 | let angle = (self.frame_count << 7) as u16; 396 | let params = BgAffineSetParams { 397 | data_center_x: 64 << 8, 398 | data_center_y: 64 << 8, 399 | display_center_x: 120, 400 | display_center_y: 80, 401 | scale_x: 0b10010000, 402 | scale_y: 0b10010000, 403 | angle, 404 | }; 405 | gba::bios::bg_affine_set(¶ms, 0x400_0020usize, 1); 406 | 407 | let x = (512 - (self.frame_count & 1023)).abs() as u16; 408 | BG1HOFS.write(x); 409 | 410 | let alpha = (32 - ((self.frame_count >> 3) & 63)).abs() as u16; 411 | let renderer = unsafe { Driver::instance_mut().video() }; 412 | // HACK: don't alternate meshes on frames where we're swapping dominant layers 413 | if self.frame_count & 511 == 256+127 { 414 | renderer.frame_counter -= 1; 415 | } 416 | if self.frame_count & 511 == 135 { 417 | renderer.frame_counter += 1; 418 | } 419 | // when bg2 (affine) is not fully opaque 420 | if alpha != 32 { 421 | if alpha < 16 { 422 | gba::io::window::WINOUT.write(OutsideWindowSetting::new() 423 | .with_outside_bg0(true) 424 | .with_outside_bg1(true) 425 | .with_outside_bg2(true) 426 | .with_outside_bg3(true) 427 | .with_outside_color_special(true) 428 | .with_obj_win_bg0(true) 429 | .with_obj_win_bg1(true) 430 | .with_obj_win_bg2(false) 431 | .with_obj_win_bg3(true) 432 | .with_obj_win_color_special(true) 433 | ); 434 | } else { 435 | gba::io::window::WINOUT.write(OutsideWindowSetting::new() 436 | .with_outside_bg0(true) 437 | .with_outside_bg1(true) 438 | .with_outside_bg2(true) 439 | .with_outside_bg3(true) 440 | .with_outside_color_special(true) 441 | .with_obj_win_bg0(true) 442 | .with_obj_win_bg1(false) 443 | .with_obj_win_bg2(true) 444 | .with_obj_win_bg3(true) 445 | .with_obj_win_color_special(true) 446 | ); 447 | } 448 | BLDALPHA.write( 449 | AlphaBlendingSetting::new() 450 | .with_eva_coefficient(16 - (alpha & 15)) 451 | .with_evb_coefficient(alpha & 15), 452 | ); 453 | } 454 | } 455 | } 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /internal/flowergal-runtime/src/audio/mod.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Deref, DerefMut}; 2 | 3 | use gba::io::dma::{ 4 | DMAControlSetting, DMADestAddressControl, DMASrcAddressControl, DMAStartTiming, DMA1, DMA2, 5 | }; 6 | use gba::io::sound::{ 7 | NumberSoundVolume, SoundMasterSetting, WaveVolumeEnableSetting, FIFO_A_L, FIFO_B_L, SOUNDBIAS, 8 | SOUNDCNT_H, SOUNDCNT_X, 9 | }; 10 | use gba::io::timers::{TimerControlSetting, TimerTickRate, TM0CNT_H, TM0CNT_L}; 11 | #[cfg(not(feature = "supercard"))] 12 | use gba::rom::{WaitstateControl, WaitstateFirstAccess, WAITCNT}; 13 | 14 | use heapless::Vec; 15 | 16 | use crate::audio::raw_pcm::RawPcm8; 17 | use flowergal_proj_config::resources::Sound; 18 | use flowergal_proj_config::sound_info::{PLAYBUF_SIZE, TIMER_VALUE}; 19 | use crate::audio::simple_flac::SimpleFlac; 20 | 21 | pub mod raw_pcm; 22 | pub mod simple_flac; 23 | 24 | type NumChannels = heapless::consts::U4; 25 | 26 | #[repr(align(4))] 27 | struct PlayBuffer(pub [i8; PLAYBUF_SIZE]); 28 | 29 | pub trait PlayableSound { 30 | fn mix_into(&mut self, mixbuf: &mut [i32]); 31 | fn remaining_samples(&self) -> usize; 32 | fn looping(&self) -> bool; 33 | fn data_ptr(&self) -> *const u8; 34 | fn reset(&mut self); 35 | 36 | fn finished(&self) -> bool { 37 | self.remaining_samples() == 0 && !self.looping() 38 | } 39 | } 40 | 41 | pub enum RuntimeSoundData { 42 | RawPcm8(RawPcm8), 43 | Flac(SimpleFlac), 44 | } 45 | 46 | impl From<&Sound> for RuntimeSoundData { 47 | fn from(s: &Sound) -> Self { 48 | // FIXME: assumes FLAC is looping and raw PCM is not 49 | assert_eq!(s.data_ptr() as usize & 3, 0); 50 | match s { 51 | Sound::RawPcm8(data) => RuntimeSoundData::RawPcm8(RawPcm8::new(data, false)), 52 | Sound::Flac(data) => RuntimeSoundData::Flac(SimpleFlac::new(data, true)), 53 | } 54 | } 55 | } 56 | 57 | impl Deref for RuntimeSoundData { 58 | type Target = dyn PlayableSound; 59 | 60 | fn deref(&self) -> &Self::Target { 61 | match self { 62 | RuntimeSoundData::RawPcm8(x) => x, 63 | RuntimeSoundData::Flac(x) => x, 64 | } 65 | } 66 | } 67 | 68 | impl DerefMut for RuntimeSoundData { 69 | fn deref_mut(&mut self) -> &mut Self::Target { 70 | match self { 71 | RuntimeSoundData::RawPcm8(x) => x, 72 | RuntimeSoundData::Flac(x) => x, 73 | } 74 | } 75 | } 76 | 77 | pub struct AudioDriver { 78 | playbuf_a: [PlayBuffer; 2], 79 | playbuf_b: [PlayBuffer; 2], 80 | cur_playbuf: usize, 81 | cur_bgm: Option, 82 | #[cfg(feature = "detect_silence")] 83 | is_silenced: bool, 84 | #[cfg(feature = "detect_silence")] 85 | should_silence: bool, 86 | sounds: Vec, 87 | pub ticks_decode: u32, 88 | pub ticks_unmix: u32, 89 | } 90 | 91 | const fn buf_a_to_b_distance() -> usize { 92 | //const driver: AudioDriver = AudioDriver::new(); 93 | const DISTANCE: usize = core::mem::size_of::() * 2; 94 | /* 95 | const distance = unsafe { 96 | driver.playbuf_b.as_ptr() as usize - driver.playbuf_a.as_ptr() as usize 97 | }; 98 | */ 99 | /* 100 | unsafe { 101 | const _after_offset: *const i8 = driver.playbuf_a[0].0.as_ptr().offset(distance as isize); 102 | const _b_ptr: *const i8 = driver.playbuf_b[0].0.as_ptr(); 103 | const_assert_eq!(_after_offset, _b_ptr); 104 | } 105 | */ 106 | //core::mem::forget(driver); 107 | DISTANCE 108 | } 109 | 110 | #[cfg(feature = "detect_silence")] 111 | const SILENCE_DETECT_THRESHOLD_MASK: u32 = 0xfefefefe; 112 | 113 | impl AudioDriver { 114 | pub const fn new() -> Self { 115 | AudioDriver { 116 | playbuf_a: [PlayBuffer([0; PLAYBUF_SIZE]), PlayBuffer([0; PLAYBUF_SIZE])], 117 | playbuf_b: [PlayBuffer([0; PLAYBUF_SIZE]), PlayBuffer([0; PLAYBUF_SIZE])], 118 | 119 | cur_playbuf: 0, 120 | cur_bgm: None, 121 | 122 | #[cfg(feature = "detect_silence")] 123 | is_silenced: false, 124 | #[cfg(feature = "detect_silence")] 125 | should_silence: false, 126 | 127 | sounds: Vec(heapless::i::Vec::new()), 128 | ticks_decode: 0, 129 | ticks_unmix: 0 130 | } 131 | } 132 | 133 | pub fn initialize(&self) { 134 | // proper ROM access times important to timely audio stream decoding! 135 | #[cfg(not(feature = "supercard"))] 136 | WAITCNT.write( 137 | WaitstateControl::new() 138 | .with_sram(WaitstateFirstAccess::Cycles8) 139 | .with_ws0_first_access(WaitstateFirstAccess::Cycles3) 140 | .with_ws0_second_access(true) 141 | .with_ws2_first_access(WaitstateFirstAccess::Cycles8) 142 | .with_game_pak_prefetch_buffer(true), 143 | ); 144 | 145 | TM0CNT_H.write(TimerControlSetting::new()); 146 | 147 | unsafe { 148 | DMA1::set_dest(FIFO_A_L.to_usize() as *mut u32); 149 | DMA1::set_count(1); 150 | } 151 | 152 | unsafe { 153 | DMA2::set_dest(FIFO_B_L.to_usize() as *mut u32); 154 | DMA2::set_count(1); 155 | } 156 | 157 | // turn on sound circuit 158 | SOUNDCNT_X.write(SoundMasterSetting::new().with_psg_fifo_master_enabled(true)); 159 | 160 | // full volume, enable both directsound channels to left and right 161 | SOUNDCNT_H.write( 162 | WaveVolumeEnableSetting::new() 163 | .with_sound_number_volume(NumberSoundVolume::Full) 164 | .with_dma_sound_a_full_volume(true) 165 | .with_dma_sound_a_enable_right(true) 166 | .with_dma_sound_a_enable_left(true) 167 | .with_dma_sound_a_reset_fifo(true) 168 | .with_dma_sound_a_timer_select(false) // 0 169 | .with_dma_sound_b_full_volume(true) 170 | .with_dma_sound_b_enable_right(true) 171 | .with_dma_sound_b_enable_left(true) 172 | .with_dma_sound_b_reset_fifo(true) 173 | .with_dma_sound_b_timer_select(false), // 0 174 | ); 175 | TM0CNT_L.write(TIMER_VALUE); 176 | TM0CNT_H.write( 177 | TimerControlSetting::new() 178 | .with_tick_rate(TimerTickRate::CPU1) 179 | .with_enabled(true), 180 | ); 181 | 182 | gba::bios::sound_bias(0x200); 183 | SOUNDBIAS.write(SOUNDBIAS.read().with_amplitude_resolution(0)); 184 | } 185 | 186 | // for crash handler 187 | pub fn disable(&self) { 188 | gba::bios::sound_bias(0x0); 189 | gba::bios::sound_channel_clear(); 190 | 191 | // turn on sound circuit 192 | SOUNDCNT_X.write(SoundMasterSetting::new().with_psg_fifo_master_enabled(false)); 193 | 194 | // full volume, enable both directsound channels to left and right 195 | SOUNDCNT_H.write( 196 | WaveVolumeEnableSetting::new() 197 | .with_sound_number_volume(NumberSoundVolume::Quarter) 198 | .with_dma_sound_a_full_volume(false) 199 | .with_dma_sound_a_enable_right(false) 200 | .with_dma_sound_a_enable_left(false) 201 | .with_dma_sound_a_reset_fifo(true) 202 | .with_dma_sound_a_timer_select(false) // 0 203 | .with_dma_sound_b_full_volume(false) 204 | .with_dma_sound_b_enable_right(false) 205 | .with_dma_sound_b_enable_left(false) 206 | .with_dma_sound_b_reset_fifo(true) 207 | .with_dma_sound_b_timer_select(false), // 0 208 | ); 209 | } 210 | 211 | fn remove_stale_sound(&mut self) { 212 | if let Some((index, _)) = self 213 | .sounds 214 | .iter() 215 | .enumerate() 216 | .filter(|(_, rt_sound)| !rt_sound.looping()) 217 | .min_by_key(|(_, rt_sound)| rt_sound.remaining_samples()) 218 | { 219 | self.remove_sound(index); 220 | } 221 | } 222 | 223 | fn remove_sound(&mut self, index: usize) { 224 | self.sounds.swap_remove(index); 225 | if let Some(x) = self.cur_bgm.as_mut() { 226 | if *x == self.sounds.len() { 227 | *x = index; 228 | } 229 | } 230 | } 231 | 232 | pub fn set_bgm(&mut self, sound: &Sound) { 233 | if let Some(index) = self.cur_bgm { 234 | if unsafe { self.sounds.get_unchecked(index) }.data_ptr() == sound.data_ptr() { 235 | return; 236 | } 237 | self.sounds.swap_remove(index); 238 | self.cur_bgm = None; 239 | } 240 | 241 | if self.sounds.len() == self.sounds.capacity() { 242 | self.remove_stale_sound(); 243 | } 244 | let rt_sound = RuntimeSoundData::from(sound); 245 | if let Err(..) = self.sounds.push(rt_sound) { 246 | error!("mixer has no room for bgm at {:?}", sound.data_ptr()); 247 | } 248 | self.cur_bgm = Some(self.sounds.len() - 1); 249 | } 250 | 251 | pub fn play_sfx(&mut self, sound: &Sound) { 252 | if self.sounds.len() == self.sounds.capacity() { 253 | self.remove_stale_sound(); 254 | } 255 | let rt_sound = RuntimeSoundData::from(sound); 256 | if let Err(..) = self.sounds.push(rt_sound) { 257 | error!("mixer has no room for sfx at {:?}", sound.data_ptr()); 258 | } 259 | } 260 | 261 | /// Timing-sensitive - call this immediately upon entering VBlank ISR! 262 | #[link_section = ".iwram"] 263 | #[instruction_set(arm::a32)] 264 | pub fn dsound_vblank(&mut self) { 265 | let (src_a, src_b) = self.cur_playbufs(); 266 | unsafe { 267 | DMA1::set_control(DMAControlSetting::new()); 268 | DMA2::set_control(DMAControlSetting::new()); 269 | 270 | // no-op to let DMA registers catch up 271 | asm!("NOP; NOP", options(nomem, nostack)); 272 | 273 | DMA1::set_source(src_a.as_ptr() as *const u32); 274 | DMA2::set_source(src_b.as_ptr() as *const u32); 275 | 276 | const DMA_CONTROL_FLAGS: DMAControlSetting = DMAControlSetting::new() 277 | .with_dest_address_control(DMADestAddressControl::Fixed) 278 | .with_source_address_control(DMASrcAddressControl::Increment) 279 | .with_dma_repeat(true) 280 | .with_use_32bit(true) 281 | .with_start_time(DMAStartTiming::Special) 282 | .with_enabled(true); 283 | 284 | DMA1::set_control(DMA_CONTROL_FLAGS); 285 | DMA2::set_control(DMA_CONTROL_FLAGS); 286 | } 287 | self.cur_playbuf = 1 - self.cur_playbuf; 288 | 289 | #[cfg(feature = "detect_silence")] 290 | if !self.is_silenced && self.should_silence { 291 | self.is_silenced = true; 292 | gba::bios::sound_bias(0x0); 293 | } 294 | } 295 | 296 | fn cur_playbufs(&mut self) -> (&mut [i8; PLAYBUF_SIZE], &mut [i8; PLAYBUF_SIZE]) { 297 | unsafe { 298 | ( 299 | &mut self.playbuf_a.get_unchecked_mut(self.cur_playbuf).0, 300 | &mut self.playbuf_b.get_unchecked_mut(self.cur_playbuf).0, 301 | ) 302 | } 303 | } 304 | 305 | pub fn prev_playbufs(&self) -> (&[i8; PLAYBUF_SIZE], &[i8; PLAYBUF_SIZE]) { 306 | unsafe { 307 | ( 308 | &self.playbuf_a.get_unchecked(1 - self.cur_playbuf).0, 309 | &self.playbuf_b.get_unchecked(1 - self.cur_playbuf).0, 310 | ) 311 | } 312 | } 313 | 314 | //noinspection RsBorrowChecker (can't tell that we've written to silence_detect_tmp) 315 | /// Call this once per frame, at some point after dsound_vblank(). 316 | #[link_section = ".iwram"] 317 | pub fn mixer(&mut self) { 318 | let start = super::timers::GbaTimer::get_ticks(); 319 | 320 | let mut mix_buffer = [0i32; PLAYBUF_SIZE]; 321 | for sound in self.sounds.iter_mut() { 322 | sound.mix_into(&mut mix_buffer); 323 | } 324 | 325 | let decoded = super::timers::GbaTimer::get_ticks(); 326 | 327 | // split into two channels. not for stereo reasons, but so we can get a cheeky 9th bit of 328 | // audio quality out of the gba's typically 8-bit sound registers, by rounding up in one of 329 | // the two channels for samples where it's relevant. the gba will wiggle its PWM at the 330 | // amplitude a + b. (proving that we get our 9th bit back as a result is an easy exercise) 331 | let (buf_a, _buf_b) = self.cur_playbufs(); 332 | #[cfg(feature = "detect_silence")] 333 | let mut silence_detect = 0u32; 334 | for i in 0..(mix_buffer.len() as isize / 8) { 335 | let mut _silence_detect_tmp: u32; 336 | unsafe { 337 | asm!( 338 | "ldmia r9, {{r0-r5, r7-r8}}", // load eight 32-bit samples from mixbuf 339 | // initializing the ninth-bit register with the 2nd sample first for shifty reasons 340 | "ands r9, r1, #0x0080", // grab ninth-bit of second sample 341 | "movne r9, r9, lsl #1", // reposition it if it's there 342 | "and r1, r1, #0xff00", // mask it & sign bits off 343 | // 1st sample 344 | "movs r0, r0, ror #8", // ninth-bit becomes sign-bit 345 | "orrmi r9, 0x01", // ninth-bit for first sample 346 | "and r0, r0, #0xff", // clear sign bits 347 | "orr r0, r0, r1", // merge in conveniently-positioned second sample 348 | // 3rd sample 349 | "movs r2, r2, ror #8", // ninth-bit becomes sign-bit 350 | "orrmi r9, 0x010000", // ninth-bit for 3rd sample (remember, little endian) 351 | "and r2, r2, #0xff", // clear sign bits 352 | "orr r0, r0, r2, lsl #16", 353 | // 4th sample 354 | "movs r3, r3, ror #8", // ninth-bit becomes sign-bit 355 | "orrmi r9, 0x01000000", // ninth-bit for 4th sample (remember, little endian) 356 | "and r3, r3, #0xff", // clear sign bits 357 | "orr r0, r0, r3, lsl #24", 358 | // playbuf b's copy with the ninth-bits added 359 | "add r1, r0, r9", 360 | 361 | // as above for the second set of four samples: 362 | // initializing the ninth-bit register with the 2nd sample first for shifty reasons 363 | "ands r9, r5, #0x0080", // grab ninth-bit of second sample 364 | "movne r9, r9, lsl #1", // reposition it if it's there 365 | "and r5, r5, #0xff00", // mask it & sign bits off 366 | // 1st sample 367 | "movs r4, r4, ror #8", // ninth-bit becomes sign-bit 368 | "orrmi r9, 0x01", // ninth-bit for first sample 369 | "and r4, r4, #0xff", // clear sign bits 370 | "orr r4, r4, r5", // merge in conveniently-positioned second sample 371 | // 3rd sample 372 | "movs r7, r7, ror #8", // ninth-bit becomes sign-bit 373 | "orrmi r9, 0x010000", // ninth-bit for 3rd sample (remember, little endian) 374 | "and r7, r7, #0xff", // clear sign bits 375 | "orr r4, r4, r7, lsl #16", 376 | // 4th sample 377 | "movs r8, r8, ror #8", // ninth-bit becomes sign-bit 378 | "orrmi r9, 0x01000000", // ninth-bit for 4th sample (remember, little endian) 379 | "and r8, r8, #0xff", // clear sign bits 380 | "orr r4, r4, r8, lsl #24", 381 | // playbuf b's copy with the ninth-bits added 382 | "add r5, r4, r9", 383 | 384 | // detect silence by seeing if any bits were set at all in the output buffer 385 | "orr r9, r0, r4", 386 | "stmia {buf_a}, {{r0, r4}}", // write eight 8-bit samples to buf_a 387 | "add {buf_a}, {buf_a}, {BUF_A_TO_B_DISTANCE}", 388 | "stmia {buf_a}, {{r1, r5}}", // write eight 8-bit samples to buf_b 389 | buf_a = in(reg) buf_a.as_ptr().offset(i * 8), 390 | // we save a register here by adding size_of::() * 2 to buf_a to get buf_b 391 | BUF_A_TO_B_DISTANCE = const buf_a_to_b_distance(), 392 | // NOTE: once we're done loading, we immediately start reusing the 'mixbuf' register 393 | // for 9th-bit scratch space, and after that we use it to detect silence. 394 | inout("r9") mix_buffer.as_ptr().offset(i * 8) => _silence_detect_tmp, 395 | out("r0") _, 396 | out("r1") _, 397 | out("r2") _, 398 | out("r3") _, 399 | out("r4") _, 400 | out("r5") _, 401 | out("r7") _, 402 | out("r8") _, 403 | options(nostack)); 404 | } 405 | 406 | #[cfg(feature = "detect_silence")] 407 | { 408 | silence_detect |= _silence_detect_tmp; 409 | } 410 | } 411 | 412 | #[cfg(feature = "detect_silence")] 413 | { 414 | self.should_silence = silence_detect & SILENCE_DETECT_THRESHOLD_MASK == 0; 415 | if !self.should_silence && self.is_silenced { 416 | gba::bios::sound_bias(0x200); 417 | self.is_silenced = false; 418 | } 419 | } 420 | 421 | #[cfg(feature = "verify_asm")] 422 | { 423 | let (mut ref_a, mut ref_b) = ([0i8; PLAYBUF_SIZE], [0i8; PLAYBUF_SIZE]); 424 | for ((a, b), mixed) in ref_a 425 | .iter_mut() 426 | .zip(ref_b.iter_mut()) 427 | .zip(mix_buffer.iter()) 428 | { 429 | let val = (mixed >> 8).clamp(-128, 127) as i8; 430 | *a = val; 431 | *b = val + if mixed & 0x0080 != 0 { 1 } else { 0 }; 432 | } 433 | 434 | let mut mismatches = 0; 435 | for (mix, ((a1, a2), (b1, b2))) in mix_buffer.iter().zip(buf_a.iter().zip(ref_a.iter()).zip(_buf_b.iter().zip(ref_b.iter()))) { 436 | if *a1 != *a2 { 437 | mismatches += 1; 438 | if mismatches > 8 { 439 | warn!("{:x} | {:x}={:x} | {:x}={:x}", mix, a1, a2, b1, b2); 440 | } 441 | } 442 | } 443 | if mismatches > 8 { 444 | panic!(); 445 | } 446 | } 447 | 448 | let split = super::timers::GbaTimer::get_ticks(); 449 | 450 | self.ticks_unmix = split - decoded; 451 | self.ticks_decode = decoded - start; 452 | 453 | let mut index = 0; 454 | // while loop because length changes during iteration 455 | while index < self.sounds.len() { 456 | // only increment in else because swap_remove swaps with end of vec, which we should also check 457 | if unsafe { self.sounds.get_unchecked(index) }.finished() { 458 | self.remove_sound(index); 459 | } else { 460 | index += 1; 461 | } 462 | } 463 | 464 | #[cfg(feature = "bench_audio")] let finish = super::timers::GbaTimer::get_ticks(); 465 | #[cfg(feature = "bench_audio")] info!("{} decode / {} split / {} finish / tick {}", decoded - start, split - decoded, finish - split, finish); 466 | 467 | // (be sure to multiply by the tick rate divisor used in timers.rs) 468 | // baseline 469 | // - 1 adpcm: 30,000 cycles decode / 20,000 cycles split / 156 cycles finish 470 | // - 1 adpcm + 3 pcm: 55,000 cycles decode 471 | // switched entire project from thumbv4t to armv4t: 472 | // - 1 adpcm: 21,000 cycles decode / 21,000 cycles split / 188 finish 473 | // - 1 adpcm + 3 pcm: 46,000 decode / 21,000 split / 674 finish 474 | // removed clamp from split: 12,000 cycle split 475 | // (potential for 8,000 cycle (or better) split if we forget about the 9-bit trickery) 476 | } 477 | } 478 | --------------------------------------------------------------------------------