├── android
├── .idea
│ ├── .name
│ ├── .gitignore
│ ├── codeStyles
│ │ ├── codeStyleConfig.xml
│ │ └── Project.xml
│ ├── compiler.xml
│ ├── kotlinc.xml
│ ├── vcs.xml
│ ├── migrations.xml
│ ├── misc.xml
│ ├── deploymentTargetSelector.xml
│ ├── gradle.xml
│ ├── runConfigurations.xml
│ ├── appInsightsSettings.xml
│ └── inspectionProfiles
│ │ └── Project_Default.xml
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── jniLibs
│ │ │ │ ├── x86_64
│ │ │ │ │ └── .keep
│ │ │ │ └── arm64-v8a
│ │ │ │ │ └── .keep
│ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ │ ├── themes.xml
│ │ │ │ │ └── colors.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ └── ic_launcher_round.webp
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ └── ic_launcher_round.webp
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ └── ic_launcher_round.webp
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ └── ic_launcher_round.webp
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.webp
│ │ │ │ │ └── ic_launcher_round.webp
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── drawable
│ │ │ │ │ ├── app_shortcut.xml
│ │ │ │ │ ├── nes_icon.xml
│ │ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ │ └── web_search.xml
│ │ │ │ └── xml
│ │ │ │ │ ├── backup_rules.xml
│ │ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── java
│ │ │ │ └── dev
│ │ │ │ │ └── luckasranarison
│ │ │ │ │ └── mes
│ │ │ │ │ ├── lib
│ │ │ │ │ ├── Rust.kt
│ │ │ │ │ ├── Controller.kt
│ │ │ │ │ ├── Audio.kt
│ │ │ │ │ └── Nes.kt
│ │ │ │ │ ├── data
│ │ │ │ │ ├── Store.kt
│ │ │ │ │ ├── Types.kt
│ │ │ │ │ └── SettingsRepository.kt
│ │ │ │ │ ├── ui
│ │ │ │ │ ├── theme
│ │ │ │ │ │ ├── Color.kt
│ │ │ │ │ │ ├── Type.kt
│ │ │ │ │ │ └── Theme.kt
│ │ │ │ │ ├── info
│ │ │ │ │ │ ├── Utils.kt
│ │ │ │ │ │ ├── AppIcon.kt
│ │ │ │ │ │ ├── InfoSection.kt
│ │ │ │ │ │ └── InfoScreen.kt
│ │ │ │ │ ├── settings
│ │ │ │ │ │ ├── Utils.kt
│ │ │ │ │ │ ├── FloatingSettings.kt
│ │ │ │ │ │ ├── SettingsSection.kt
│ │ │ │ │ │ └── SettingsScreen.kt
│ │ │ │ │ ├── shared
│ │ │ │ │ │ └── TopAppBar.kt
│ │ │ │ │ ├── home
│ │ │ │ │ │ ├── DirectoryChooser.kt
│ │ │ │ │ │ ├── FloatingButton.kt
│ │ │ │ │ │ ├── TopAppBar.kt
│ │ │ │ │ │ └── HomeScreen.kt
│ │ │ │ │ ├── rom
│ │ │ │ │ │ ├── InitialBox.kt
│ │ │ │ │ │ ├── sheet
│ │ │ │ │ │ │ ├── BottomSheet.kt
│ │ │ │ │ │ │ ├── TopRow.kt
│ │ │ │ │ │ │ └── Metadata.kt
│ │ │ │ │ │ ├── RomList.kt
│ │ │ │ │ │ └── RomContainer.kt
│ │ │ │ │ ├── license
│ │ │ │ │ │ ├── License.kt
│ │ │ │ │ │ ├── LibraryContainer.kt
│ │ │ │ │ │ └── BottomSheet.kt
│ │ │ │ │ ├── gamepad
│ │ │ │ │ │ ├── MenuButton.kt
│ │ │ │ │ │ ├── ActionButton.kt
│ │ │ │ │ │ ├── BaseButton.kt
│ │ │ │ │ │ ├── GamePadLayout.kt
│ │ │ │ │ │ └── DirectionButton.kt
│ │ │ │ │ └── emulator
│ │ │ │ │ │ ├── EmulatorView.kt
│ │ │ │ │ │ ├── EmulatorScreen.kt
│ │ │ │ │ │ ├── EmulatorBackHandler.kt
│ │ │ │ │ │ └── FullScreenContainer.kt
│ │ │ │ │ ├── extra
│ │ │ │ │ └── Shortcut.kt
│ │ │ │ │ ├── anim
│ │ │ │ │ └── Animation.kt
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ └── Application.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── dev
│ │ │ │ └── luckasranarison
│ │ │ │ └── mes
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── dev
│ │ │ └── luckasranarison
│ │ │ └── mes
│ │ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── gradle
│ ├── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ └── libs.versions.toml
├── .gitignore
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
└── gradlew.bat
├── .gitignore
├── web
├── src
│ ├── vite-env.d.ts
│ ├── assets
│ │ ├── fullscreen.svg
│ │ ├── upload.svg
│ │ ├── arrow-down.svg
│ │ ├── arrow-left.svg
│ │ ├── arrow-right.svg
│ │ ├── arrow-up.svg
│ │ ├── stop.svg
│ │ ├── error.svg
│ │ ├── keyboard.svg
│ │ ├── drag-drop.svg
│ │ ├── gamepad.svg
│ │ ├── sparkle.svg
│ │ └── github.svg
│ ├── index.css
│ ├── workers
│ │ └── audio.ts
│ ├── ringbuffer.ts
│ ├── controller.ts
│ ├── emulator.ts
│ └── main.ts
├── public
│ └── favicon.ico
├── postcss.config.js
├── vite.config.ts
├── tailwind.config.js
├── tsconfig.json
└── package.json
├── crates
├── mes-core
│ ├── src
│ │ ├── features
│ │ │ ├── mod.rs
│ │ │ └── json.rs
│ │ ├── ppu
│ │ │ ├── internals
│ │ │ │ ├── mod.rs
│ │ │ │ ├── oam.rs
│ │ │ │ ├── background.rs
│ │ │ │ └── sprite.rs
│ │ │ └── registers
│ │ │ │ ├── mod.rs
│ │ │ │ ├── mask.rs
│ │ │ │ ├── status.rs
│ │ │ │ ├── control.rs
│ │ │ │ └── address.rs
│ │ ├── apu
│ │ │ ├── channels
│ │ │ │ ├── mod.rs
│ │ │ │ ├── common
│ │ │ │ │ ├── sequencer.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── timer.rs
│ │ │ │ │ ├── envelope.rs
│ │ │ │ │ ├── length_counter.rs
│ │ │ │ │ └── sweep.rs
│ │ │ │ ├── noise.rs
│ │ │ │ ├── triangle.rs
│ │ │ │ └── pulse.rs
│ │ │ ├── filters.rs
│ │ │ └── frame_counter.rs
│ │ ├── cpu
│ │ │ ├── interrupt.rs
│ │ │ ├── address.rs
│ │ │ └── register.rs
│ │ ├── bus
│ │ │ ├── dma.rs
│ │ │ └── ppu.rs
│ │ ├── controller
│ │ │ └── mod.rs
│ │ ├── error
│ │ │ └── mod.rs
│ │ ├── mappers
│ │ │ ├── mapper_000.rs
│ │ │ ├── mapper_002.rs
│ │ │ ├── mapper_003.rs
│ │ │ ├── mod.rs
│ │ │ └── mapper_001.rs
│ │ ├── utils
│ │ │ ├── test.rs
│ │ │ └── mod.rs
│ │ └── lib.rs
│ └── Cargo.toml
├── mes-jni
│ ├── Cargo.toml
│ └── src
│ │ └── utils.rs
└── mes-wasm
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── rust-toolchain.toml
├── palette
└── nespalette.pal
├── Cargo.toml
├── .gitmodules
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── deploy.yml
├── LICENSE
└── README.md
/android/.idea/.name:
--------------------------------------------------------------------------------
1 | Mes
--------------------------------------------------------------------------------
/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/android/app/src/main/jniLibs/x86_64/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/android/app/src/main/jniLibs/arm64-v8a/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | node_modules
3 | dist
4 | pkg
5 |
--------------------------------------------------------------------------------
/web/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/android/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/crates/mes-core/src/features/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "json")]
2 | pub mod json;
3 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.88.0"
3 | components = ["clippy"]
4 |
--------------------------------------------------------------------------------
/palette/nespalette.pal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/palette/nespalette.pal
--------------------------------------------------------------------------------
/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/web/public/favicon.ico
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["crates/mes-core", "crates/mes-jni", "crates/mes-wasm"]
3 | resolver = "2"
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Mes
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "nes-test-roms"]
2 | path = nes-test-roms
3 | url = https://github.com/christopherpow/nes-test-roms
4 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/lib/Rust.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.lib
2 |
3 | object Rust {
4 | external fun setPanicHook()
5 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luckasRanarison/mes/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #E60012
4 |
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/internals/mod.rs:
--------------------------------------------------------------------------------
1 | mod background;
2 | mod oam;
3 | mod sprite;
4 |
5 | pub use background::BackgroundData;
6 | pub use oam::OamData;
7 | pub use sprite::SpriteData;
8 |
--------------------------------------------------------------------------------
/android/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/web/src/assets/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/registers/mod.rs:
--------------------------------------------------------------------------------
1 | mod address;
2 | mod control;
3 | mod mask;
4 | mod status;
5 |
6 | pub use address::AddressRegister;
7 | pub use control::ControlRegister;
8 | pub use mask::MaskRegister;
9 | pub use status::StatusRegister;
10 |
--------------------------------------------------------------------------------
/crates/mes-jni/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mes-jni"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | jni = "0.21.1"
8 | mes-core = { path = "../mes-core", features = ["json"] }
9 |
10 | [lib]
11 | crate-type = ["cdylib"]
12 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/mod.rs:
--------------------------------------------------------------------------------
1 | mod common;
2 | mod dmc;
3 | mod noise;
4 | mod pulse;
5 | mod triangle;
6 |
7 | pub use common::Channel;
8 | pub use dmc::Dmc;
9 | pub use noise::Noise;
10 | pub use pulse::Pulse;
11 | pub use triangle::Triangle;
12 |
--------------------------------------------------------------------------------
/web/src/assets/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/arrow-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import wasm from "vite-plugin-wasm";
3 | import topLevelAwait from "vite-plugin-top-level-await";
4 |
5 | export default defineConfig({
6 | plugins: [wasm(), topLevelAwait()],
7 | base: "/mes",
8 | });
9 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/data/Store.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.data
2 |
3 | import android.content.Context
4 | import androidx.datastore.preferences.preferencesDataStore
5 |
6 | val Context.dataStore by preferencesDataStore(name = "settings")
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Nov 18 10:28:17 MSK 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/crates/mes-core/src/features/json.rs:
--------------------------------------------------------------------------------
1 | use crate::{cartridge::Header, error::Error};
2 |
3 | pub fn serialize_rom_header(bytes: &[u8]) -> Result {
4 | let header = Header::try_from_bytes(bytes)?;
5 | let serialized = serde_json::to_string(&header).unwrap();
6 | Ok(serialized)
7 | }
8 |
--------------------------------------------------------------------------------
/android/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | *.so
17 | app/release
18 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["index.html", "./src/**/*.ts"],
4 | theme: {
5 | colors: {
6 | primary: "#E60012",
7 | secondary: "#484848",
8 | white: "#FFFFFF",
9 | smoke: "#00000020",
10 | },
11 | },
12 | plugins: [],
13 | };
14 |
--------------------------------------------------------------------------------
/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.compose) apply false
6 | alias(libs.plugins.aboutlibraries) apply false
7 | }
--------------------------------------------------------------------------------
/web/src/assets/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | data object ColorScheme {
6 | val Primary = Color(0xFFE60012)
7 | val Secondary = Color(0xFF484848)
8 | val Light = Color(0xFFFFFFFF)
9 | val Dark = Color(0xFF1E1E1E)
10 | val Smoke = Color(0x00000020)
11 | }
--------------------------------------------------------------------------------
/web/src/assets/keyboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/web"
5 | schedule:
6 | interval: "weekly"
7 |
8 | - package-ecosystem: "cargo"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
13 | - package-ecosystem: "maven"
14 | directories:
15 | - "/android"
16 | - "/android/app"
17 | schedule:
18 | interval: "weekly"
19 |
--------------------------------------------------------------------------------
/android/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/info/Utils.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.info
2 |
3 | import android.content.Context
4 |
5 | fun getAppVersion(ctx: Context) =
6 | ctx.packageManager?.getPackageInfo(ctx.packageName, 0)?.versionName
7 | ?: throw Exception("Failed to get package info")
8 |
9 | fun makeMailMessage(address: String) =
10 | "https://mail.google.com/mail/?view=cm&fs=1&to=$address&su=Subject&body=Message"
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/settings/Utils.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.settings
2 |
3 | import android.content.Context
4 | import androidx.core.net.toUri
5 | import androidx.documentfile.provider.DocumentFile
6 |
7 | fun String.toPathName(context: Context): String {
8 | val uri = this.toUri()
9 | val documentFile = DocumentFile.fromTreeUri(context, uri)
10 | return documentFile?.name ?: "Unknown Directory"
11 | }
--------------------------------------------------------------------------------
/web/src/assets/drag-drop.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/app/src/test/java/dev/luckasranarison/mes/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/app_shortcut.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/crates/mes-core/src/cpu/interrupt.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/CPU_interrupts
2 |
3 | pub const INTERRUPT_LATENCY: u8 = 7;
4 |
5 | #[derive(Debug, Clone, Copy, PartialEq)]
6 | pub enum Interrupt {
7 | Nmi,
8 | Reset,
9 | Irq,
10 | }
11 |
12 | impl Interrupt {
13 | pub fn vector(&self) -> u16 {
14 | match self {
15 | Interrupt::Nmi => 0xFFFA,
16 | Interrupt::Reset => 0xFFFC,
17 | Interrupt::Irq => 0xFFFE,
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/crates/mes-wasm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mes-wasm"
3 | version = "0.1.0"
4 | edition = "2021"
5 | description = "mes-core WASM bindings"
6 | license = "MIT"
7 | repository = "https//github.com/luckasRanarison/mes"
8 | authors = ["LIOKA Ranarison Fiderana "]
9 |
10 | [dependencies]
11 | mes-core = { path = "../mes-core" }
12 | wasm-bindgen = "0.2.90"
13 | web-sys = { version = "0.3.76", features = ["ImageData"] }
14 |
15 | [lib]
16 | crate-type = ["cdylib", "rlib"]
17 |
--------------------------------------------------------------------------------
/crates/mes-core/src/bus/dma.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug)]
2 | pub struct DmaState {
3 | pub high_byte: u8,
4 | pub current_page: u8,
5 | pub buffer: Option,
6 | }
7 |
8 | impl DmaState {
9 | pub fn new(offset: u8) -> Self {
10 | Self {
11 | high_byte: offset,
12 | current_page: 0x00,
13 | buffer: None,
14 | }
15 | }
16 |
17 | pub fn get_ram_address(&self) -> u16 {
18 | u16::from_le_bytes([self.current_page, self.high_byte])
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/crates/mes-core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mes-core"
3 | version = "0.1.0"
4 | edition = "2021"
5 | description = "Yet another NES emulator"
6 | license = "MIT"
7 | repository = "https//github.com/luckasRanarison/mes"
8 | authors = ["LIOKA Ranarison Fiderana "]
9 |
10 | [features]
11 | default = []
12 | json = ["serde", "serde_json"]
13 |
14 | [dependencies]
15 | serde = { version = "1.0.215", features = ["derive"], optional = true }
16 | serde_json = { version = "1.0.133", optional = true }
17 |
--------------------------------------------------------------------------------
/web/src/assets/gamepad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/crates/mes-jni/src/utils.rs:
--------------------------------------------------------------------------------
1 | pub trait RefUnwrap {
2 | fn unwrap_ref(&self) -> &T;
3 | }
4 |
5 | #[allow(clippy::mut_from_ref)]
6 | pub trait MutUnwrap {
7 | fn unwrap_mut(&self) -> &mut T;
8 | }
9 |
10 | impl RefUnwrap for *const T {
11 | fn unwrap_ref(&self) -> &T {
12 | unsafe { self.as_ref().expect("null pointer exception") }
13 | }
14 | }
15 |
16 | impl MutUnwrap for *mut T {
17 | fn unwrap_mut(&self) -> &mut T {
18 | unsafe { self.as_mut().expect("null pointer exception") }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/web/src/assets/sparkle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/sequencer.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, Default)]
2 | pub struct Sequencer {
3 | steps: usize,
4 | current: usize,
5 | }
6 |
7 | impl Sequencer {
8 | pub fn new(steps: usize) -> Self {
9 | Self { steps, current: 0 }
10 | }
11 |
12 | pub fn step(&mut self) {
13 | self.current = (self.current + 1) % self.steps;
14 | }
15 |
16 | pub fn index(&self) -> usize {
17 | self.current
18 | }
19 |
20 | pub fn reset(&mut self) {
21 | self.current = 0;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/nes_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/crates/mes-core/src/controller/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitFlag;
2 |
3 | #[derive(Debug, Default)]
4 | pub struct ControllerState {
5 | state: [u8; 2],
6 | shift: [u8; 2],
7 | }
8 |
9 | impl ControllerState {
10 | pub fn set_state(&mut self, id: usize, value: u8) {
11 | self.state[id] = value;
12 | }
13 |
14 | pub fn reload_shift_registers(&mut self) {
15 | self.shift = self.state;
16 | }
17 |
18 | pub fn poll_button(&mut self, id: usize) -> u8 {
19 | let value = self.shift[id].get(7);
20 | self.shift[id] <<= 1;
21 | value
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val Typography = Typography(
10 | bodyLarge = TextStyle(
11 | fontFamily = FontFamily.Default,
12 | fontWeight = FontWeight.Normal,
13 | fontSize = 16.sp,
14 | lineHeight = 24.sp,
15 | letterSpacing = 0.5.sp
16 | )
17 | )
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | branches: ["main"]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Initialize submodules
20 | run: git submodule update --init
21 |
22 | - name: Build
23 | run: cargo build --verbose
24 |
25 | - name: Test
26 | run: cargo test --workspace --lib --verbose
27 |
28 | - name: Lint
29 | run: cargo clippy -- -D warnings
30 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/lib/Controller.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.lib
2 |
3 | import kotlin.experimental.and
4 | import kotlin.experimental.inv
5 | import kotlin.experimental.or
6 |
7 | class Controller(private var value: Byte = 0b0000_0000) {
8 | fun update(button: Button, state: Boolean): Controller {
9 | val bits = (1 shl button.ordinal).toByte()
10 | val value = if (state) value or bits else value and bits.inv()
11 | return Controller(value)
12 | }
13 |
14 | fun state(): Byte = value
15 | }
16 |
17 | enum class Button { Right, Left, Down, Up, Start, Select, B, A }
--------------------------------------------------------------------------------
/android/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Mes"
23 | include(":app")
24 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true
21 | },
22 | "include": ["src", "public/roms"]
23 | }
24 |
--------------------------------------------------------------------------------
/web/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | .text-hl {
7 | @apply font-semibold text-primary;
8 | }
9 | }
10 |
11 | @layer components {
12 | .key {
13 | @apply rounded-md bg-secondary px-2 py-1 text-sm text-white;
14 | }
15 |
16 | .arrow-key {
17 | @apply flex justify-center rounded-md bg-secondary text-white;
18 | }
19 |
20 | .action-key {
21 | @apply flex h-10 w-10 items-center justify-center rounded-full bg-primary font-semibold text-white;
22 | }
23 |
24 | .menu-key {
25 | @apply rounded-md bg-secondary px-4 py-2 text-[12px] text-white;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/android/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/web/src/workers/audio.ts:
--------------------------------------------------------------------------------
1 | import AudioRingBuffer from "../ringbuffer";
2 |
3 | class NesAudioProcessor extends AudioWorkletProcessor {
4 | private buffer: AudioRingBuffer;
5 |
6 | constructor() {
7 | super();
8 |
9 | this.buffer = new AudioRingBuffer(8192);
10 |
11 | this.port.onmessage = ({ data }) => {
12 | if (data.reset) this.buffer.clear();
13 | if (data.samples) this.buffer.enqueue(data.samples);
14 | };
15 | }
16 |
17 | process(_: Float32Array[][], outputs: Float32Array[][]) {
18 | const channel = outputs[0][0];
19 |
20 | this.buffer.dequeue(channel);
21 |
22 | return true;
23 | }
24 | }
25 |
26 | registerProcessor("nes-audio-processor", NesAudioProcessor);
27 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/mod.rs:
--------------------------------------------------------------------------------
1 | mod envelope;
2 | mod length_counter;
3 | mod sequencer;
4 | mod sweep;
5 | mod timer;
6 |
7 | pub use envelope::Envelope;
8 | pub use length_counter::LengthCounter;
9 | pub use sequencer::Sequencer;
10 | pub use sweep::Sweep;
11 | pub use timer::Timer;
12 |
13 | pub trait Channel {
14 | fn write_register(&mut self, address: u16, value: u8);
15 | fn raw_sample(&self) -> u8;
16 | fn is_active(&self) -> bool;
17 | fn is_mute(&self) -> bool;
18 | fn set_enabled(&mut self, value: bool);
19 |
20 | fn get_sample(&self) -> f32 {
21 | match self.is_mute() {
22 | true => 0.0,
23 | false => self.raw_sample() as f32,
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/crates/mes-core/src/cpu/address.rs:
--------------------------------------------------------------------------------
1 | use super::register::CpuRegister;
2 |
3 | #[derive(Debug, PartialEq, Clone, Copy)]
4 | pub enum Address {
5 | Memory(u16),
6 | Register(CpuRegister),
7 | }
8 |
9 | impl Address {
10 | pub fn to_memory_unchecked(self) -> u16 {
11 | match self {
12 | Address::Memory(address) => address,
13 | Address::Register(_) => panic!("Not a memory address"),
14 | }
15 | }
16 | }
17 |
18 | #[derive(Debug, Clone, Copy)]
19 | pub enum AddressMode {
20 | Implied(CpuRegister),
21 | Immediate,
22 | Absolute,
23 | ZeroPage,
24 | AbsoluteX,
25 | AbsoluteY,
26 | ZeroPageX,
27 | ZeroPageY,
28 | Indirect,
29 | IndirectX,
30 | IndirectY,
31 | Relative,
32 | }
33 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/web_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mes",
3 | "author": "LIOKA Ranarison Fiderana",
4 | "version": "0.1.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host",
8 | "wasm": "wasm-pack build ../crates/mes-wasm --out-dir ../../web/pkg",
9 | "build": "npm run wasm && tsc && vite build",
10 | "preview": "vite preview --host"
11 | },
12 | "devDependencies": {
13 | "@types/audioworklet": "^0.0.60",
14 | "autoprefixer": "^10.4.17",
15 | "postcss": "^8.4.35",
16 | "tailwindcss": "^3.4.1",
17 | "typescript": "^5.0.2",
18 | "vite": "^4.4.5",
19 | "vite-plugin-top-level-await": "^1.3.1",
20 | "vite-plugin-wasm": "^3.2.2",
21 | "wasm-pack": "^0.13.0"
22 | },
23 | "dependencies": {
24 | "mes": "file:pkg"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/timer.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::Clock;
2 |
3 | #[derive(Debug, Default)]
4 | pub struct Timer {
5 | pub period: u16,
6 | pub counter: u16,
7 | }
8 |
9 | impl Timer {
10 | pub fn is_zero(&self) -> bool {
11 | self.counter == 0
12 | }
13 |
14 | pub fn set_period_hi(&mut self, value: u8) {
15 | self.period = (self.period & 0x00FF) | ((value as u16) << 8);
16 | }
17 |
18 | pub fn set_period_lo(&mut self, value: u8) {
19 | self.period = (self.period & 0xFF00) | value as u16;
20 | }
21 | }
22 |
23 | impl Clock for Timer {
24 | fn tick(&mut self) {
25 | if self.counter == 0 {
26 | self.counter = self.period;
27 | } else {
28 | self.counter -= 1;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/internals/oam.rs:
--------------------------------------------------------------------------------
1 | const PRIMARY_OAM_SIZE: usize = 256;
2 | const SECONDARY_OAM_SIZE: usize = 32;
3 |
4 | #[derive(Debug)]
5 | pub struct OamData {
6 | pub address: u8,
7 | pub buffer: u8,
8 | pub primary: [u8; PRIMARY_OAM_SIZE],
9 | pub secondary: [u8; SECONDARY_OAM_SIZE],
10 | pub primary_index: u8,
11 | pub secondary_index: u8,
12 | pub index_overflow: bool,
13 | }
14 |
15 | impl Default for OamData {
16 | fn default() -> Self {
17 | Self {
18 | address: 0,
19 | buffer: 0,
20 | primary: [0; PRIMARY_OAM_SIZE],
21 | secondary: [0; SECONDARY_OAM_SIZE],
22 | primary_index: 0,
23 | secondary_index: 0,
24 | index_overflow: false,
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/app/src/androidTest/java/dev/luckasranarison/mes/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.helloworld", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/android/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/extra/Shortcut.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.extra
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.ShortcutInfo
6 | import android.content.pm.ShortcutManager
7 | import androidx.core.content.ContextCompat.getSystemService
8 | import dev.luckasranarison.mes.MainActivity
9 | import dev.luckasranarison.mes.data.RomFile
10 |
11 | fun createShortcut(ctx: Context, rom: RomFile) {
12 | val shortcutManager = getSystemService(ctx, ShortcutManager::class.java)
13 |
14 | val intent = Intent(Intent.ACTION_VIEW, rom.uri, ctx, MainActivity::class.java)
15 | intent.putExtra("path", rom.uri.toString())
16 |
17 | val shortcut = ShortcutInfo.Builder(ctx, rom.uri.toString())
18 | .setShortLabel(rom.baseName())
19 | .setIntent(intent)
20 | .build()
21 |
22 | shortcutManager?.requestPinShortcut(shortcut, null)
23 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/internals/background.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitPlane;
2 |
3 | #[derive(Debug, Default)]
4 | pub struct BackgroundData {
5 | pub address: u16,
6 | pub pattern_id: u8,
7 | pub palette_id: u8,
8 | pub pattern: BitPlane,
9 | pub pattern_shift: BitPlane,
10 | pub palette_shift: BitPlane,
11 | }
12 |
13 | impl BackgroundData {
14 | pub fn load_shifters(&mut self) {
15 | self.pattern_shift.low |= self.pattern.low as u16;
16 | self.pattern_shift.high |= self.pattern.high as u16;
17 | self.palette_shift.low |= (self.palette_id as u16 & 0b01) * 0xFF;
18 | self.palette_shift.high |= (self.palette_id as u16 & 0b10) * 0xFF;
19 | }
20 |
21 | pub fn update_shifters(&mut self) {
22 | self.pattern_shift.low <<= 1;
23 | self.pattern_shift.high <<= 1;
24 | self.palette_shift.low <<= 1;
25 | self.palette_shift.high <<= 1;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/info/AppIcon.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.info
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.Icon
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.res.painterResource
9 | import androidx.compose.ui.unit.dp
10 | import dev.luckasranarison.mes.R
11 |
12 | @Composable
13 | fun AppIcon() {
14 | Row(
15 | modifier = Modifier.fillMaxWidth(),
16 | horizontalArrangement = Arrangement.Center
17 | ) {
18 | Icon(
19 | painter = painterResource(id = R.drawable.nes_icon),
20 | contentDescription = "Icon",
21 | tint = MaterialTheme.colorScheme.onBackground,
22 | modifier = Modifier
23 | .padding(48.dp)
24 | .size(82.dp),
25 | )
26 | }
27 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/internals/sprite.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitPlane;
2 |
3 | #[derive(Debug, Default)]
4 | pub struct SpriteData {
5 | pub buffer: [u8; 4],
6 | pub address: u16,
7 | pub pattern_shift: [BitPlane; 8],
8 | pub attribute_shift: [u8; 8],
9 | pub offset_shift: [u8; 8],
10 | pub zero_eval: bool,
11 | pub zero_pixel: bool,
12 | }
13 |
14 | impl SpriteData {
15 | pub fn update_shifters(&mut self) {
16 | for i in 0..8 {
17 | if self.offset_shift[i] == 0 {
18 | self.pattern_shift[i].low <<= 1;
19 | self.pattern_shift[i].high <<= 1;
20 | } else {
21 | self.offset_shift[i] -= 1;
22 | }
23 | }
24 | }
25 |
26 | pub fn horizontal_reverse(&mut self, index: usize) {
27 | self.pattern_shift[index].low = self.pattern_shift[index].low.reverse_bits();
28 | self.pattern_shift[index].high = self.pattern_shift[index].high.reverse_bits();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/shared/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.shared
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
5 | import androidx.compose.material3.ExperimentalMaterial3Api
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.IconButton
8 | import androidx.compose.material3.Text
9 | import androidx.compose.material3.TopAppBar
10 | import androidx.compose.runtime.Composable
11 |
12 | @Composable
13 | @OptIn(ExperimentalMaterial3Api::class)
14 | fun GenericTopAppBar(
15 | title: String,
16 | onExit: () -> Unit,
17 | ) {
18 | TopAppBar(
19 | title = { Text(text = title) },
20 | navigationIcon = {
21 | IconButton(onClick = onExit) {
22 | Icon(
23 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
24 | contentDescription = "Back"
25 | )
26 | }
27 | }
28 | )
29 | }
--------------------------------------------------------------------------------
/android/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/android/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/lib/Audio.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.lib
2 |
3 | import android.media.AudioAttributes
4 | import android.media.AudioFormat
5 | import android.media.AudioManager
6 | import android.media.AudioTrack
7 |
8 | fun createAudioTrack(): AudioTrack {
9 | val sampleRate = 44100
10 | val channelConfig = AudioFormat.CHANNEL_OUT_MONO
11 | val audioFormat = AudioFormat.ENCODING_PCM_FLOAT
12 | val minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat)
13 |
14 | return AudioTrack(
15 | AudioAttributes.Builder()
16 | .setUsage(AudioAttributes.USAGE_MEDIA)
17 | .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
18 | .build(),
19 | AudioFormat.Builder()
20 | .setSampleRate(sampleRate)
21 | .setChannelMask(channelConfig)
22 | .setEncoding(audioFormat)
23 | .build(),
24 | minBufferSize,
25 | AudioTrack.MODE_STREAM,
26 | AudioManager.AUDIO_SESSION_ID_GENERATE
27 | )
28 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/info/InfoSection.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.info
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 |
13 | @Composable
14 | fun Section(title: String, description: String? = null, onClick: () -> Unit) {
15 | Column(
16 | modifier = Modifier
17 | .fillMaxWidth()
18 | .clickable { onClick() }
19 | .padding(horizontal = 16.dp, vertical = 14.dp)
20 | ) {
21 | Text(text = title)
22 |
23 | if (description != null) {
24 | Text(
25 | text = description,
26 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)
27 | )
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 LIOKA Ranarison Fiderana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/home/DirectoryChooser.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.home
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun DirectoryChooser(modifier: Modifier, onChoose: () -> Unit) {
16 | Box(modifier = modifier.fillMaxSize()) {
17 | Column(
18 | modifier = Modifier.align(Alignment.Center),
19 | verticalArrangement = Arrangement.spacedBy(24.dp),
20 | horizontalAlignment = Alignment.CenterHorizontally
21 | ) {
22 | Text(text = "ROM directory is not set. Please choose one")
23 | Button(onClick = onChoose) {
24 | Text(text = "Add directory")
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/web/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/crates/mes-core/src/error/mod.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, Clone, PartialEq, Eq)]
2 | pub enum Error {
3 | UnsupportedFileFormat,
4 | UnsupportedVersion,
5 | UnexpectedEndOfInput { expected: String, length: usize },
6 | UnsupportedMapper(u8),
7 | }
8 |
9 | impl Error {
10 | pub fn eof(expected: &str, length: usize) -> Self {
11 | Self::UnexpectedEndOfInput {
12 | expected: expected.to_owned(),
13 | length,
14 | }
15 | }
16 | }
17 |
18 | impl std::fmt::Display for Error {
19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 | match self {
21 | Error::UnsupportedFileFormat => write!(f, "The loaded file is not an iNES file"),
22 | Error::UnsupportedVersion => write!(f, "iNES 2.0 is not supported"),
23 | Error::UnexpectedEndOfInput { expected, length } => {
24 | write!(
25 | f,
26 | "Unexpected end of input, expected {expected} (length: {length})",
27 | )
28 | }
29 | Error::UnsupportedMapper(id) => write!(f, "Unsupported mapper {id}"),
30 | }
31 | }
32 | }
33 |
34 | impl std::error::Error for Error {}
35 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | paths:
7 | - "web/**"
8 | - "crates/mes-core/**"
9 | - "crates/mes-wasm/**"
10 |
11 | workflow_dispatch:
12 |
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | concurrency:
19 | group: "pages"
20 | cancel-in-progress: true
21 |
22 | jobs:
23 | deploy:
24 | environment:
25 | name: github-pages
26 | url: ${{ steps.deployment.outputs.page_url }}
27 | runs-on: ubuntu-latest
28 | defaults:
29 | run:
30 | working-directory: web
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Set up Node
35 | uses: actions/setup-node@v4
36 | with:
37 | node-version: 20
38 | cache: "npm"
39 | cache-dependency-path: "**/package-lock.json"
40 | - name: Install dependencies
41 | run: npm install
42 | - name: Build
43 | run: npm run build
44 | - name: Setup Pages
45 | uses: actions/configure-pages@v5
46 | - name: Upload artifact
47 | uses: actions/upload-pages-artifact@v3
48 | with:
49 | path: "web/dist"
50 | - name: Deploy to GitHub Pages
51 | id: deployment
52 | uses: actions/deploy-pages@v4
53 |
--------------------------------------------------------------------------------
/web/src/ringbuffer.ts:
--------------------------------------------------------------------------------
1 | class AudioRingBuffer {
2 | private buffer: Float32Array;
3 | private writeIndex: number;
4 | private readIndex: number;
5 | private capacity: number;
6 |
7 | constructor(capacity: number) {
8 | this.buffer = new Float32Array(capacity);
9 | this.capacity = capacity;
10 | this.writeIndex = 0;
11 | this.readIndex = 0;
12 | }
13 |
14 | enqueue(samples: Float32Array) {
15 | if (this.writeLength() < samples.length) {
16 | throw new Error("Ring buffer overflow");
17 | }
18 |
19 | for (let i = 0; i < samples.length; i++) {
20 | this.buffer[this.writeIndex] = samples[i];
21 | this.writeIndex = (this.writeIndex + 1) % this.capacity;
22 | }
23 | }
24 |
25 | dequeue(channel: Float32Array) {
26 | const length = Math.min(channel.length, this.readLength());
27 |
28 | for (let i = 0; i < length; i++) {
29 | channel[i] = this.buffer[this.readIndex];
30 | this.readIndex = (this.readIndex + 1) % this.capacity;
31 | }
32 | }
33 |
34 | readLength() {
35 | return (this.writeIndex - this.readIndex + this.capacity) % this.capacity;
36 | }
37 |
38 | writeLength() {
39 | return this.capacity - this.readLength();
40 | }
41 |
42 | clear() {
43 | this.writeIndex = 0;
44 | this.readIndex = 0;
45 | }
46 | }
47 |
48 | export default AudioRingBuffer;
49 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/envelope.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/APU_Envelope
2 |
3 | use crate::utils::{BitFlag, Clock};
4 |
5 | #[derive(Debug, Default)]
6 | pub struct Envelope {
7 | loop_flag: bool,
8 | const_flag: bool,
9 | volume: u8,
10 | start: bool,
11 | decay_level: u8,
12 | counter: u8,
13 | }
14 |
15 | impl Envelope {
16 | pub fn write(&mut self, value: u8) {
17 | self.loop_flag = value.contains(5);
18 | self.const_flag = value.contains(4);
19 | self.volume = value.get_range(0..4);
20 | }
21 |
22 | pub fn volume(&self) -> u8 {
23 | match self.const_flag {
24 | true => self.volume,
25 | false => self.decay_level,
26 | }
27 | }
28 |
29 | pub fn restart(&mut self) {
30 | self.start = true;
31 | }
32 | }
33 |
34 | impl Clock for Envelope {
35 | fn tick(&mut self) {
36 | if self.start {
37 | self.start = false;
38 | self.decay_level = 15;
39 | } else if self.counter == 0 {
40 | self.counter = self.volume;
41 |
42 | if self.decay_level > 0 {
43 | self.decay_level -= 1;
44 | } else if self.loop_flag {
45 | self.decay_level = 15;
46 | }
47 | } else {
48 | self.counter -= 1;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/length_counter.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/APU_Length_Counter
2 |
3 | use crate::utils::Clock;
4 |
5 | #[derive(Debug, Default)]
6 | pub struct LengthCounter {
7 | counter: u8,
8 | halted: bool,
9 | enabled: bool,
10 | }
11 |
12 | impl LengthCounter {
13 | #[rustfmt::skip]
14 | const LENGTH_TABLE: [u8; 32] = [
15 | 0x0A, 0xFE, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06,
16 | 0xA0, 0x08, 0x3C, 0x0A, 0x0E, 0x0C, 0x1A, 0x0E,
17 | 0x0C, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16,
18 | 0xC0, 0x18, 0x48, 0x1A, 0x10, 0x1C, 0x20, 0x1E,
19 | ];
20 |
21 | pub fn set_length(&mut self, index: u8) {
22 | if self.enabled {
23 | self.counter = Self::LENGTH_TABLE[index as usize];
24 | }
25 | }
26 |
27 | pub fn set_halt(&mut self, value: bool) {
28 | self.halted = value;
29 | }
30 |
31 | pub fn set_enabled(&mut self, value: bool) {
32 | self.enabled = value;
33 |
34 | if !self.enabled {
35 | self.counter = 0;
36 | }
37 | }
38 |
39 | pub fn is_active(&self) -> bool {
40 | self.enabled && self.counter > 0
41 | }
42 | }
43 |
44 | impl Clock for LengthCounter {
45 | fn tick(&mut self) {
46 | if !self.halted && self.counter > 0 {
47 | self.counter -= 1;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/anim/Animation.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.anim
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.animation.core.FastOutSlowInEasing
6 | import androidx.compose.animation.core.FiniteAnimationSpec
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.animation.slideInHorizontally
9 | import androidx.compose.animation.slideOutHorizontally
10 | import androidx.compose.ui.unit.IntOffset
11 |
12 | object Animations {
13 | private val animationSpec: FiniteAnimationSpec = tween(
14 | durationMillis = 300,
15 | easing = FastOutSlowInEasing
16 | )
17 |
18 | val EnterTransition: EnterTransition = slideInHorizontally(
19 | initialOffsetX = { it },
20 | animationSpec = animationSpec
21 | )
22 |
23 | val ExitTransition: ExitTransition = slideOutHorizontally(
24 | targetOffsetX = { -it },
25 | animationSpec = animationSpec
26 | )
27 |
28 | val PopEnterTransition: EnterTransition = slideInHorizontally(
29 | initialOffsetX = { -it },
30 | animationSpec = animationSpec
31 | )
32 |
33 | val PopExitTransition: ExitTransition = slideOutHorizontally(
34 | targetOffsetX = { it },
35 | animationSpec = animationSpec
36 | )
37 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/mappers/mapper_000.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/NROM
2 |
3 | use super::Mapper;
4 | use crate::{
5 | cartridge::{Cartridge, ChrPage, Mirroring, PrgPage},
6 | utils::Reset,
7 | };
8 |
9 | #[derive(Debug)]
10 | pub struct NRom {
11 | cartridge: Cartridge,
12 | }
13 |
14 | impl NRom {
15 | pub fn new(cartridge: Cartridge) -> Self {
16 | Self { cartridge }
17 | }
18 | }
19 |
20 | impl Mapper for NRom {
21 | fn read(&self, address: u16) -> u8 {
22 | match address {
23 | 0x0000..=0x1FFF => self.cartridge.read_chr(address, ChrPage::Index8(0)),
24 | 0x4020..=0x5FFF => 0,
25 | 0x6000..=0x7FFF => self.cartridge.read_prg_ram(address),
26 | 0x8000..=0xBFFF => self.cartridge.read_prg_rom(address, PrgPage::Index16(0)),
27 | 0xC000..=0xFFFF => self.cartridge.read_prg_rom(address, PrgPage::Last16),
28 | _ => panic!("Trying to read from an invalid address: 0x{address:x}"),
29 | }
30 | }
31 |
32 | fn write(&mut self, address: u16, value: u8) {
33 | if let 0x6000..=0x7FFF = address {
34 | self.cartridge.write_prg_ram(address, value);
35 | }
36 | }
37 |
38 | fn get_mirroring(&self) -> Mirroring {
39 | self.cartridge.header.mirroring
40 | }
41 | }
42 |
43 | impl Reset for NRom {
44 | fn reset(&mut self) {}
45 | }
46 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/rom/InitialBox.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.rom
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.dp
14 | import dev.luckasranarison.mes.ui.theme.Typography
15 |
16 | @Composable
17 | fun InitialBox(
18 | name: String,
19 | modifier: Modifier,
20 | foreground: Color,
21 | background: Color
22 | ) {
23 | Box(
24 | modifier = modifier
25 | .size(40.dp)
26 | .clip(RoundedCornerShape(8.dp))
27 | .background(background),
28 | contentAlignment = Alignment.Center
29 | ) {
30 | Text(
31 | text = name
32 | .split(" ")
33 | .take(3)
34 | .mapNotNull { it.firstOrNull() }
35 | .joinToString("")
36 | .ifEmpty { "NES" },
37 | style = Typography.titleSmall,
38 | color = foreground,
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/data/Types.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.data
2 |
3 | import android.net.Uri
4 | import androidx.documentfile.provider.DocumentFile
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.json.Json
8 |
9 | @Serializable
10 | data class RomHeader(
11 | @SerialName("prg_rom_pages") val prgRomPages: Byte,
12 | @SerialName("chr_rom_pages") val chrRomPages: Byte,
13 | @SerialName("prg_ram_pages") val prgRamPages: Byte,
14 | val mirroring: String,
15 | val battery: Boolean,
16 | val trainer: Boolean,
17 | val mapper: Short
18 | )
19 |
20 | data class RomFile(
21 | val name: String,
22 | val uri: Uri,
23 | val size: Long,
24 | val header: RomHeader
25 | ) {
26 | private val attributesRegex = Regex("\\((.*?)\\)|\\[(.*?)]")
27 |
28 | constructor(file: DocumentFile, metadata: String) : this(
29 | name = file.name ?: "Unknown",
30 | uri = file.uri,
31 | size = file.length(),
32 | header = Json.decodeFromString(metadata)
33 | )
34 |
35 | fun getAttributes() = attributesRegex
36 | .findAll(name)
37 | .mapNotNull { it.groups[1]?.value }
38 | .toList()
39 |
40 | fun baseName() = name
41 | .replace(".nes", "", ignoreCase = true)
42 | .replace(attributesRegex, "")
43 | }
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/license/License.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.license
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.material3.Scaffold
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.navigation.NavHostController
11 | import com.mikepenz.aboutlibraries.ui.compose.m3.rememberLibraries
12 | import dev.luckasranarison.mes.ui.shared.GenericTopAppBar
13 | import dev.luckasranarison.mes.R
14 |
15 | @Composable
16 | fun License(controller: NavHostController) {
17 | val ctx = LocalContext.current
18 |
19 | val libs by rememberLibraries {
20 | ctx.resources
21 | .openRawResource(R.raw.aboutlibraries)
22 | .readBytes()
23 | .decodeToString()
24 | }
25 |
26 | Scaffold(
27 | topBar = {
28 | GenericTopAppBar(
29 | title = "Open source license",
30 | onExit = { controller.popBackStack() }
31 | )
32 | }
33 | ) { innerPadding ->
34 | LazyColumn(modifier = Modifier.padding(innerPadding)) {
35 | items(libs?.libraries?.size ?: 0) { index ->
36 | LibraryContainer(libs!!.libraries[index])
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/web/src/controller.ts:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 | enum Button {
3 | A = 0b1000_0000,
4 | B = 0b0100_0000,
5 | Select = 0b0010_0000,
6 | Start = 0b0001_0000,
7 | Up = 0b0000_1000,
8 | Down = 0b0000_0100,
9 | Left = 0b0000_0010,
10 | Right = 0b0000_0001,
11 | }
12 |
13 | type ButtonMap = Record;
14 |
15 | class Controller {
16 | private value: number;
17 | private mappings: ButtonMap;
18 |
19 | constructor(mappings: ButtonMap) {
20 | this.value = 0b0000_0000;
21 | this.mappings = mappings;
22 | }
23 |
24 | handleKeyEvent(event: KeyboardEvent, state: boolean) {
25 | const button = this.mappings[event.code];
26 |
27 | if (button) {
28 | this.updateButton(button, state);
29 | }
30 |
31 | return button;
32 | }
33 |
34 | updateButton(button: Button, state: boolean) {
35 | if (state) {
36 | this.value |= button;
37 | } else {
38 | this.value &= ~button;
39 | }
40 | }
41 |
42 | state() {
43 | return this.value;
44 | }
45 |
46 | static playerOne() {
47 | return new Controller({
48 | KeyA: Button.A,
49 | KeyZ: Button.B,
50 | KeyQ: Button.A,
51 | KeyW: Button.B,
52 | Space: Button.Select,
53 | Enter: Button.Start,
54 | ArrowUp: Button.Up,
55 | ArrowDown: Button.Down,
56 | ArrowLeft: Button.Left,
57 | ArrowRight: Button.Right,
58 | });
59 | }
60 | }
61 |
62 | export { Controller };
63 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/gamepad/MenuButton.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.gamepad
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.shape.RoundedCornerShape
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.clip
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.unit.sp
13 | import dev.luckasranarison.mes.lib.Button
14 |
15 | @Composable
16 | fun MenuButton(text: String, onPress: (Boolean) -> Unit) {
17 | BaseButton(
18 | modifier = Modifier.clip(RoundedCornerShape(5.dp)),
19 | onPress = { state -> onPress(state) },
20 | ) {
21 | Text(
22 | text = text,
23 | fontSize = 10.sp,
24 | modifier = Modifier.padding(vertical = 2.dp, horizontal = 10.dp),
25 | fontWeight = FontWeight.Bold,
26 | color = Color.White
27 | )
28 | }
29 | }
30 |
31 | @Composable
32 | fun MenuPad(modifier: Modifier, onPress: (Button, Boolean) -> Unit) {
33 | Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
34 | MenuButton(text = "SELECT") { state -> onPress(Button.Select, state) }
35 | MenuButton(text = "START") { state -> onPress(Button.Start, state) }
36 | }
37 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/settings/FloatingSettings.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.settings
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxHeight
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material3.Card
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.window.Dialog
14 | import androidx.compose.ui.window.DialogProperties
15 | import dev.luckasranarison.mes.vm.EmulatorViewModel
16 |
17 | @Composable
18 | fun FloatingSettings(viewModel: EmulatorViewModel, onExit: () -> Unit) {
19 | Dialog(
20 | onDismissRequest = onExit,
21 | properties = DialogProperties(
22 | usePlatformDefaultWidth = false,
23 | )
24 | ) {
25 | Box(
26 | modifier = Modifier
27 | .fillMaxWidth(0.6f)
28 | .fillMaxHeight()
29 | ) {
30 | Card(
31 | modifier = Modifier
32 | .clip(RoundedCornerShape(24.dp))
33 | .align(Alignment.Center)
34 | ) {
35 | Settings(viewModel = viewModel, onExit = onExit)
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/home/FloatingButton.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.home
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.material3.FloatingActionButton
8 | import androidx.compose.material3.FloatingActionButtonDefaults
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.painterResource
15 | import androidx.compose.ui.unit.dp
16 | import dev.luckasranarison.mes.R
17 |
18 | @Composable
19 | fun FloatingButton(onClick: () -> Unit) {
20 | Box(
21 | modifier = Modifier
22 | .fillMaxSize()
23 | .padding(16.dp),
24 | contentAlignment = Alignment.BottomEnd
25 | ) {
26 | FloatingActionButton (
27 | onClick = onClick,
28 | containerColor = MaterialTheme.colorScheme.primary,
29 | elevation = FloatingActionButtonDefaults.elevation(2.dp)
30 | ) {
31 | Icon(
32 | painter = painterResource(id = R.drawable.nes_icon),
33 | contentDescription = "Upload",
34 | modifier = Modifier.size(24.dp),
35 | )
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/gamepad/ActionButton.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.gamepad
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.clip
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.text.font.FontWeight
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.unit.sp
15 | import dev.luckasranarison.mes.lib.Button
16 |
17 | @Composable
18 | fun ActionButton(text: String, onPress: (Boolean) -> Unit) {
19 | BaseButton(
20 | modifier = Modifier
21 | .clip(CircleShape)
22 | .size(48.dp),
23 | onPress = { state -> onPress(state) },
24 | ) {
25 | Text(
26 | text = text,
27 | fontSize = 20.sp,
28 | fontWeight = FontWeight.Bold,
29 | color = Color.White
30 | )
31 | }
32 | }
33 |
34 | @Composable
35 | fun ActionPad(modifier: Modifier, onPress: (Button, Boolean) -> Unit) {
36 | Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(24.dp)) {
37 | ActionButton(text = "B") { state -> onPress(Button.B, state) }
38 | ActionButton(text = "A") { state -> onPress(Button.A, state) }
39 | }
40 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/home/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.home
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Info
6 | import androidx.compose.material.icons.filled.Settings
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.IconButton
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import androidx.navigation.NavHostController
14 | import dev.luckasranarison.mes.Routes
15 |
16 | @Composable
17 | @OptIn(ExperimentalMaterial3Api::class)
18 | fun HomeTopAppBar(controller: NavHostController) {
19 | TopAppBar(
20 | title = { Text("Mes Emulator") },
21 | actions = {
22 | Row {
23 | IconButton(onClick = { controller.navigate(Routes.INFO) }) {
24 | Icon(
25 | imageVector = Icons.Default.Info,
26 | contentDescription = "Info"
27 | )
28 | }
29 | IconButton(onClick = { controller.navigate(Routes.SETTINGS) }) {
30 | Icon(
31 | imageVector = Icons.Default.Settings,
32 | contentDescription = "Settings"
33 | )
34 | }
35 | }
36 | }
37 | )
38 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/emulator/EmulatorView.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.emulator
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.Canvas
6 | import android.graphics.Paint
7 | import android.view.View
8 | import androidx.core.graphics.scale
9 | import dev.luckasranarison.mes.lib.SCREEN_HEIGHT
10 | import dev.luckasranarison.mes.lib.SCREEN_WIDTH
11 |
12 | class EmulatorView(context: Context) : View(context) {
13 | private val screen: Bitmap =
14 | Bitmap.createBitmap(SCREEN_WIDTH, SCREEN_HEIGHT, Bitmap.Config.ARGB_8888)
15 |
16 | init {
17 | val pixels = Array(SCREEN_WIDTH * SCREEN_HEIGHT) { 0xFFFFFFFF.toInt() }
18 | screen.setPixels(pixels.toIntArray(), 0, SCREEN_WIDTH, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
19 | }
20 |
21 | override fun onDraw(canvas: Canvas) {
22 | super.onDraw(canvas)
23 |
24 | val viewWidth = width.toFloat()
25 | val viewHeight = height.toFloat()
26 |
27 | val aspectRatio = SCREEN_HEIGHT.toFloat() / SCREEN_WIDTH.toFloat()
28 | val scaledWidth = viewHeight / aspectRatio
29 | val scaledBitmap = screen.scale(scaledWidth.toInt(), viewHeight.toInt(), false)
30 | val left = (viewWidth - scaledWidth) / 2
31 |
32 | canvas.drawBitmap(scaledBitmap, left, 0f, Paint())
33 | }
34 |
35 | fun updateScreenData(buffer: IntArray) {
36 | screen.setPixels(buffer, 0, SCREEN_WIDTH, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
37 | invalidate()
38 | }
39 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/registers/mask.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitFlag;
2 |
3 | #[allow(unused)]
4 | #[rustfmt::skip]
5 | mod mask_flag {
6 | pub const G0: u8 = 0;
7 | pub const M0: u8 = 1;
8 | pub const M1: u8 = 2;
9 | pub const B0: u8 = 3;
10 | pub const S: u8 = 4;
11 | pub const R: u8 = 5;
12 | pub const G1: u8 = 6;
13 | pub const B1: u8 = 7;
14 | }
15 |
16 | /// BGRs bMmG
17 | /// |||| ||||
18 | /// |||| |||+- Greyscale (0: normal color, 1: produce a greyscale display)
19 | /// |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide
20 | /// |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
21 | /// |||| +---- 1: Show background
22 | /// |||+------ 1: Show sprites
23 | /// ||+------- Emphasize red (green on PAL/Dendy)
24 | /// |+-------- Emphasize green (red on PAL/Dendy)
25 | /// +--------- Emphasize blue
26 | #[derive(Debug, Default)]
27 | pub struct MaskRegister(u8);
28 |
29 | impl MaskRegister {
30 | pub fn write(&mut self, value: u8) {
31 | self.0 = value;
32 | }
33 |
34 | pub fn is_rendering(&self) -> bool {
35 | self.show_background() || self.show_sprites()
36 | }
37 |
38 | pub fn show_background_leftmost(&self) -> bool {
39 | self.0.contains(mask_flag::M0)
40 | }
41 |
42 | pub fn show_sprites_leftmost(&self) -> bool {
43 | self.0.contains(mask_flag::M1)
44 | }
45 |
46 | pub fn show_background(&self) -> bool {
47 | self.0.contains(mask_flag::B0)
48 | }
49 |
50 | pub fn show_sprites(&self) -> bool {
51 | self.0.contains(mask_flag::S)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorScheme = darkColorScheme(
10 | primary = ColorScheme.Primary,
11 | secondary = ColorScheme.Secondary,
12 | tertiary = ColorScheme.Smoke,
13 | background = ColorScheme.Dark,
14 | onBackground = ColorScheme.Light,
15 | onPrimary = ColorScheme.Light,
16 | onSecondary = ColorScheme.Light,
17 | surface = ColorScheme.Dark,
18 | surfaceTint = ColorScheme.Dark,
19 | )
20 |
21 | private val LightColorScheme = lightColorScheme(
22 | primary = ColorScheme.Primary,
23 | secondary = ColorScheme.Secondary,
24 | tertiary = ColorScheme.Smoke,
25 | background = ColorScheme.Light,
26 | onBackground = ColorScheme.Dark,
27 | onPrimary = ColorScheme.Light,
28 | onSecondary = ColorScheme.Light,
29 | surface = ColorScheme.Light,
30 | surfaceTint = ColorScheme.Light,
31 | )
32 |
33 | @Composable
34 | fun MesTheme(
35 | darkTheme: Boolean = isSystemInDarkTheme(),
36 | content: @Composable () -> Unit
37 | ) {
38 | val colorScheme = when {
39 | darkTheme -> DarkColorScheme
40 | else -> LightColorScheme
41 | }
42 |
43 | MaterialTheme(
44 | colorScheme = colorScheme,
45 | typography = Typography,
46 | content = content
47 | )
48 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/data/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.data
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.booleanPreferencesKey
6 | import androidx.datastore.preferences.core.edit
7 | import androidx.datastore.preferences.core.stringPreferencesKey
8 | import androidx.datastore.preferences.core.byteArrayPreferencesKey
9 | import kotlinx.coroutines.flow.map
10 |
11 | class SettingsRepository(private val dataStore: DataStore) {
12 | object Keys {
13 | val ROM_DIR = stringPreferencesKey("rom_dir")
14 | val ENABLE_APU = booleanPreferencesKey("enable_apu")
15 | val COLOR_PALETTE = byteArrayPreferencesKey("color_palette")
16 | }
17 |
18 | suspend fun setRomDirectory(dir: String) {
19 | dataStore.edit { pref -> pref[Keys.ROM_DIR] = dir }
20 | }
21 |
22 | suspend fun toggleApuState() {
23 | dataStore.edit { pref -> pref[Keys.ENABLE_APU] = !(pref[Keys.ENABLE_APU] ?: true) }
24 | }
25 |
26 | suspend fun setColorPalette(palette: ByteArray?) {
27 | if (palette != null) {
28 | dataStore.edit { pref -> pref[Keys.COLOR_PALETTE] = palette }
29 | } else {
30 | dataStore.edit { pref -> pref.remove(Keys.COLOR_PALETTE) }
31 | }
32 | }
33 |
34 | fun getRomDirectory() = dataStore.data.map { pref -> pref[Keys.ROM_DIR] }
35 | fun getApuState() = dataStore.data.map { pref -> pref[Keys.ENABLE_APU] }
36 | fun getColorPalette() = dataStore.data.map { pref -> pref[Keys.COLOR_PALETTE] }
37 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/gamepad/BaseButton.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.gamepad
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.gestures.detectTapGestures
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.BoxScope
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.input.pointer.pointerInput
16 |
17 | @Composable
18 | fun BaseButton(
19 | modifier: Modifier,
20 | onPress: (Boolean) -> Unit,
21 | content: @Composable (BoxScope.() -> Unit) = {}
22 | ) {
23 | var isPressed by remember { mutableStateOf(false) }
24 |
25 | Box(
26 | contentAlignment = Alignment.Center,
27 | modifier = modifier
28 | .background(
29 | Color.Gray.copy(
30 | alpha = if (isPressed) 0.5f else 0.8f
31 | )
32 | )
33 | .pointerInput(Unit) {
34 | detectTapGestures(onPress = {
35 | try {
36 | onPress(true)
37 | isPressed = true
38 | awaitRelease()
39 | } finally {
40 | onPress(false)
41 | isPressed = false
42 | }
43 | })
44 | }
45 | ) { content() }
46 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/common/sweep.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/APU_Sweep
2 |
3 | use crate::utils::BitFlag;
4 |
5 | use super::timer::Timer;
6 |
7 | #[derive(Debug, Default)]
8 | pub struct Sweep {
9 | enabled: bool,
10 | period: u8,
11 | negate: bool,
12 | shift: u8,
13 | counter: u8,
14 | reload: bool,
15 | negate_value: u8,
16 | }
17 |
18 | impl Sweep {
19 | pub fn new(negate_value: u8) -> Self {
20 | Self {
21 | negate_value,
22 | ..Default::default()
23 | }
24 | }
25 |
26 | pub fn write(&mut self, value: u8) {
27 | self.enabled = value.contains(7);
28 | self.period = value.get_range(4..7);
29 | self.negate = value.contains(3);
30 | self.shift = value.get_range(0..3);
31 | self.reload = true;
32 | }
33 |
34 | pub fn update_period(&mut self, timer: &mut Timer) {
35 | if self.counter == 0 && self.enabled && self.shift > 0 && timer.period >= 8 {
36 | let period = self.target_period(timer);
37 |
38 | // set the period if not "muting"
39 | if period <= 0x7FF {
40 | timer.period = period;
41 | }
42 | }
43 |
44 | if self.counter == 0 || self.reload {
45 | self.counter = self.period;
46 | self.reload = false;
47 | } else {
48 | self.counter -= 1;
49 | }
50 | }
51 |
52 | pub fn target_period(&self, timer: &Timer) -> u16 {
53 | let period = timer.period;
54 | let sweep_value = period >> self.shift;
55 |
56 | match self.negate {
57 | true => period - sweep_value - self.negate_value as u16,
58 | false => period + sweep_value,
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/crates/mes-core/src/mappers/mapper_002.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/UxROM
2 |
3 | use super::Mapper;
4 | use crate::{
5 | cartridge::{Cartridge, ChrPage, Mirroring, PrgPage},
6 | utils::Reset,
7 | };
8 |
9 | #[derive(Debug)]
10 | pub struct UxRom {
11 | cartridge: Cartridge,
12 | prg_bank: u8,
13 | }
14 |
15 | impl UxRom {
16 | pub fn new(cartridge: Cartridge) -> Self {
17 | Self {
18 | cartridge,
19 | prg_bank: 0,
20 | }
21 | }
22 | }
23 |
24 | impl Mapper for UxRom {
25 | fn read(&self, address: u16) -> u8 {
26 | match address {
27 | 0x0000..=0x1FFF => self.cartridge.read_chr(address, ChrPage::Index8(0)),
28 | 0x4020..=0x5FFF => 0,
29 | 0x6000..=0x7FFF => self.cartridge.read_prg_ram(address),
30 | 0x8000..=0xBFFF => self
31 | .cartridge
32 | .read_prg_rom(address, PrgPage::Index16(self.prg_bank)),
33 | 0xC000..=0xFFFF => self.cartridge.read_prg_rom(address, PrgPage::Last16),
34 | _ => panic!("Trying to read from an invalid address: 0x{address:x}"),
35 | }
36 | }
37 |
38 | fn write(&mut self, address: u16, value: u8) {
39 | match address {
40 | 0x0000..=0x1FFF => self
41 | .cartridge
42 | .write_chr_ram(address, value, ChrPage::Index8(0)),
43 | 0x6000..=0x7FFF => self.cartridge.write_prg_ram(address, value),
44 | 0x8000..=0xFFFF => self.prg_bank = value & 0b1111,
45 | _ => {}
46 | }
47 | }
48 |
49 | fn get_mirroring(&self) -> Mirroring {
50 | self.cartridge.header.mirroring
51 | }
52 | }
53 |
54 | impl Reset for UxRom {
55 | fn reset(&mut self) {
56 | self.prg_bank = 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/emulator/EmulatorScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.emulator
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.*
5 | import androidx.compose.ui.*
6 | import androidx.compose.ui.platform.LocalContext
7 | import androidx.compose.ui.viewinterop.AndroidView
8 | import androidx.navigation.NavHostController
9 | import dev.luckasranarison.mes.lib.createAudioTrack
10 | import dev.luckasranarison.mes.ui.gamepad.GamePadLayout
11 | import dev.luckasranarison.mes.vm.EmulatorViewModel
12 |
13 | @Composable
14 | fun Emulator(viewModel: EmulatorViewModel, controller: NavHostController) {
15 | val ctx = LocalContext.current
16 | val emulatorView = remember { EmulatorView(ctx) }
17 | val audioTrack = remember { createAudioTrack() }
18 | val isRunning by viewModel.isRunning
19 | val isShortcutLaunch by viewModel.isShortcutLaunch
20 |
21 | DisposableEffect(Unit) {
22 | viewModel.startEmulation()
23 | audioTrack.play()
24 |
25 | onDispose {
26 | audioTrack.stop()
27 | audioTrack.release()
28 | }
29 | }
30 |
31 | LaunchedEffect(isRunning) {
32 | viewModel.runMainLoop(emulatorView, audioTrack)
33 | }
34 |
35 | EmulatorBackHandler(
36 | controller = controller,
37 | pauseEmulation = viewModel::pauseEmulation,
38 | resumeEmulation = viewModel::startEmulation,
39 | isShortcutLaunch = isShortcutLaunch,
40 | )
41 |
42 | FullScreenLandscapeBox {
43 | AndroidView(
44 | factory = { emulatorView },
45 | modifier = Modifier
46 | .align(Alignment.Center)
47 | .fillMaxSize()
48 | )
49 | GamePadLayout(viewModel = viewModel)
50 | }
51 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/emulator/EmulatorBackHandler.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.emulator
2 |
3 | import android.app.Activity
4 | import androidx.activity.compose.BackHandler
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.*
7 | import androidx.compose.ui.platform.LocalContext
8 | import androidx.navigation.NavHostController
9 |
10 | @Composable
11 | fun EmulatorBackHandler(
12 | controller: NavHostController,
13 | pauseEmulation: () -> Unit,
14 | resumeEmulation: () -> Unit,
15 | isShortcutLaunch: Boolean,
16 | ) {
17 | val ctx = LocalContext.current as Activity
18 | var showExitDialog by remember { mutableStateOf(false) }
19 |
20 | BackHandler { showExitDialog = true }
21 |
22 | LaunchedEffect(showExitDialog) {
23 | if (showExitDialog) {
24 | pauseEmulation()
25 | } else {
26 | resumeEmulation()
27 | }
28 | }
29 |
30 | if (showExitDialog) {
31 | AlertDialog(
32 | onDismissRequest = { showExitDialog = false },
33 | title = { Text(text = "Confirm to exit") },
34 | text = { Text(text = "Are you sure to stop the emulation?") },
35 | confirmButton = {
36 | TextButton(onClick = {
37 | when (isShortcutLaunch) {
38 | true -> ctx.finishAffinity()
39 | else -> controller.popBackStack()
40 | }
41 | }) {
42 | Text(text = "Confirm")
43 | }
44 | },
45 | dismissButton = {
46 | TextButton(onClick = { showExitDialog = false }) {
47 | Text(text = "Cancel")
48 | }
49 | },
50 | )
51 | }
52 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/emulator/FullScreenContainer.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.emulator
2 |
3 | import android.app.Activity
4 | import android.content.pm.ActivityInfo
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.BoxScope
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.DisposableEffect
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.platform.LocalView
15 | import androidx.core.view.WindowCompat
16 | import androidx.core.view.WindowInsetsCompat
17 | import androidx.core.view.WindowInsetsControllerCompat
18 |
19 | @Composable
20 | fun FullScreenLandscapeBox(content: @Composable (BoxScope.() -> Unit)) {
21 | val view = LocalView.current
22 | val ctx = LocalContext.current as Activity
23 |
24 | DisposableEffect(Unit) {
25 | val insetsController = WindowCompat.getInsetsController(ctx.window, view)
26 | val systemBars = WindowInsetsCompat.Type.systemBars()
27 |
28 | insetsController.systemBarsBehavior =
29 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
30 |
31 | insetsController.hide(systemBars)
32 | ctx.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
33 |
34 | onDispose {
35 | insetsController.show(systemBars)
36 | ctx.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
37 | }
38 | }
39 |
40 | Box(
41 | modifier = Modifier
42 | .fillMaxSize()
43 | .background(Color.Black)
44 | ) { content() }
45 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/mappers/mapper_003.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/INES_Mapper_003
2 |
3 | use super::Mapper;
4 | use crate::{
5 | cartridge::{Cartridge, ChrPage, Mirroring, PrgPage},
6 | utils::Reset,
7 | };
8 |
9 | #[derive(Debug)]
10 | pub struct CnRom {
11 | cartridge: Cartridge,
12 | chr_bank: u8,
13 | }
14 |
15 | impl CnRom {
16 | pub fn new(cartridge: Cartridge) -> Self {
17 | Self {
18 | cartridge,
19 | chr_bank: 0,
20 | }
21 | }
22 | }
23 |
24 | impl Mapper for CnRom {
25 | fn read(&self, address: u16) -> u8 {
26 | match address {
27 | 0x0000..=0x1FFF => self
28 | .cartridge
29 | .read_chr(address, ChrPage::Index8(self.chr_bank)),
30 | 0x4020..=0x5FFF => 0,
31 | 0x6000..=0x7FFF => self.cartridge.read_prg_ram(address),
32 | 0x8000..=0xBFFF => self.cartridge.read_prg_rom(address, PrgPage::Index16(0)),
33 | 0xC000..=0xFFFF => self.cartridge.read_prg_rom(address, PrgPage::Last16),
34 | _ => panic!("Trying to read from an invalid address: 0x{address:x}"),
35 | }
36 | }
37 |
38 | fn write(&mut self, address: u16, value: u8) {
39 | match address {
40 | 0x0000..=0x1FFF => {
41 | self.cartridge
42 | .write_chr_ram(address, value, ChrPage::Index8(self.chr_bank))
43 | }
44 | 0x6000..=0x7FFF => self.cartridge.write_prg_ram(address, value),
45 | 0x8000..=0xFFFF => self.chr_bank = value,
46 | _ => {}
47 | }
48 | }
49 |
50 | fn get_mirroring(&self) -> Mirroring {
51 | self.cartridge.header.mirroring
52 | }
53 | }
54 |
55 | impl Reset for CnRom {
56 | fn reset(&mut self) {
57 | self.chr_bank = 0;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/crates/mes-core/src/cpu/register.rs:
--------------------------------------------------------------------------------
1 | // https://www.masswerk.at/6502/6502_instruction_set.html#registers
2 |
3 | #[cfg(feature = "json")]
4 | use serde::Serialize;
5 |
6 | use crate::utils::BitFlag;
7 |
8 | #[derive(Debug, PartialEq, Clone, Copy)]
9 | pub enum CpuRegister {
10 | PC,
11 | AC,
12 | X,
13 | Y,
14 | SR,
15 | SP,
16 | }
17 |
18 | #[rustfmt::skip]
19 | pub mod status_flag {
20 | pub const C: u8 = 0;
21 | pub const Z: u8 = 1;
22 | pub const I: u8 = 2;
23 | pub const D: u8 = 3;
24 | pub const B: u8 = 4;
25 | pub const __: u8 = 5;
26 | pub const V: u8 = 6;
27 | pub const N: u8 = 7;
28 | }
29 |
30 | #[cfg_attr(feature = "json", derive(Serialize))]
31 | #[cfg_attr(feature = "json", serde(transparent))]
32 | pub struct StatusRegister(u8);
33 |
34 | impl Default for StatusRegister {
35 | fn default() -> Self {
36 | Self(0b0010_0000)
37 | }
38 | }
39 |
40 | impl StatusRegister {
41 | pub fn value(&self) -> u8 {
42 | self.0
43 | }
44 |
45 | pub fn get(&self, flag: u8) -> u8 {
46 | self.0.get(flag)
47 | }
48 |
49 | pub fn contains(&self, flag: u8) -> bool {
50 | self.0.contains(flag)
51 | }
52 |
53 | pub fn assign(&mut self, value: u8) {
54 | self.0 = value
55 | }
56 |
57 | pub fn update(&mut self, flag: u8, cond: bool) {
58 | self.0.update(flag, cond)
59 | }
60 |
61 | pub fn set(&mut self, flag: u8) {
62 | self.0.update(flag, true)
63 | }
64 |
65 | pub fn clear(&mut self, flag: u8) {
66 | self.0.update(flag, false)
67 | }
68 |
69 | pub fn update_zero(&mut self, value: u8) {
70 | self.0.update(status_flag::Z, value == 0);
71 | }
72 |
73 | pub fn update_negative(&mut self, value: u8) {
74 | self.0.update(status_flag::N, value >> 7 == 1);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/filters.rs:
--------------------------------------------------------------------------------
1 | use std::{cell::RefCell, f32::consts::PI};
2 |
3 | const SAMPLE_RATE: f32 = 44100.0;
4 |
5 | #[derive(Debug)]
6 | pub struct Filter {
7 | b0: f32,
8 | b1: f32,
9 | a1: f32,
10 | prev_x: f32,
11 | prev_y: f32,
12 | }
13 |
14 | impl Filter {
15 | pub fn low_pass(sample_rate: f32, cutoff_freq: f32) -> Self {
16 | let c = sample_rate / (cutoff_freq * PI);
17 | let a0 = 1.0 / (1.0 + c);
18 |
19 | Self {
20 | b0: a0,
21 | b1: a0,
22 | a1: (1.0 - c) * a0,
23 | prev_x: 0.0,
24 | prev_y: 0.0,
25 | }
26 | }
27 |
28 | pub fn high_pass(sample_rate: f32, cutoff_freq: f32) -> Self {
29 | let c = sample_rate / (cutoff_freq * PI);
30 | let a0 = 1.0 / (1.0 + c);
31 |
32 | Self {
33 | b0: c * a0,
34 | b1: -c * a0,
35 | a1: (1.0 - c) * a0,
36 | prev_x: 0.0,
37 | prev_y: 0.0,
38 | }
39 | }
40 |
41 | pub fn process(&mut self, x: f32) -> f32 {
42 | let y = self.b0 * x + self.b1 * self.prev_x - self.a1 * self.prev_y;
43 |
44 | self.prev_x = x;
45 | self.prev_y = y;
46 |
47 | y
48 | }
49 | }
50 |
51 | #[derive(Debug)]
52 | pub struct FilterChain(RefCell<[Filter; 3]>);
53 |
54 | // https://www.nesdev.org/wiki/APU_Mixer
55 | impl Default for FilterChain {
56 | fn default() -> Self {
57 | Self(RefCell::new([
58 | Filter::high_pass(SAMPLE_RATE, 90.0),
59 | Filter::high_pass(SAMPLE_RATE, 440.0),
60 | Filter::low_pass(SAMPLE_RATE, 14000.0),
61 | ]))
62 | }
63 | }
64 |
65 | impl FilterChain {
66 | pub fn process(&self, sample: f32) -> f32 {
67 | self.0
68 | .borrow_mut()
69 | .iter_mut()
70 | .fold(sample, |acc, f| f.process(acc))
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/rom/sheet/BottomSheet.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.rom.sheet
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.rememberCoroutineScope
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import dev.luckasranarison.mes.data.RomFile
10 | import dev.luckasranarison.mes.ui.theme.Typography
11 | import kotlinx.coroutines.launch
12 |
13 | @Composable
14 | @OptIn(ExperimentalMaterial3Api::class)
15 | fun BottomSheet(
16 | rom: RomFile,
17 | onClose: () -> Unit,
18 | ) {
19 | val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
20 | val scope = rememberCoroutineScope()
21 |
22 | ModalBottomSheet(
23 | onDismissRequest = { onClose() },
24 | sheetState = sheetState,
25 | containerColor = MaterialTheme.colorScheme.surface,
26 | ) {
27 | Column(
28 | modifier = Modifier
29 | .fillMaxWidth()
30 | .padding(16.dp),
31 | ) {
32 | TopRow(rom = rom)
33 |
34 | Spacer(modifier = Modifier.height(16.dp))
35 |
36 | MetadataList(rom = rom)
37 |
38 | Spacer(modifier = Modifier.height(16.dp))
39 |
40 | Button(
41 | onClick = {
42 | scope.launch { sheetState.hide(); onClose() }
43 | },
44 | modifier = Modifier.fillMaxWidth(),
45 | colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary)
46 | ) {
47 | Text(
48 | "Close",
49 | style = Typography.bodyMedium,
50 | color = MaterialTheme.colorScheme.onPrimary
51 | )
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes
2 |
3 | import android.net.Uri
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.activity.enableEdgeToEdge
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.activity.viewModels
10 | import dev.luckasranarison.mes.lib.Rust
11 | import dev.luckasranarison.mes.ui.theme.MesTheme
12 | import dev.luckasranarison.mes.vm.EmulatorViewModel
13 |
14 | object Activities {
15 | val GET_CONTENT = ActivityResultContracts.GetContent()
16 | val GET_DIRECTORY = ActivityResultContracts.OpenDocumentTree()
17 | }
18 |
19 | class MainActivity : ComponentActivity() {
20 | private val viewModel: EmulatorViewModel by viewModels { EmulatorViewModel.Factory }
21 |
22 | companion object {
23 | init {
24 | System.loadLibrary("mes_jni")
25 | }
26 | }
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 |
31 | Rust.setPanicHook() // Redirects Rust panics output to Log before crashing
32 | handleShortcutLaunch()
33 | enableEdgeToEdge()
34 |
35 | setContent {
36 | MesTheme {
37 | App(viewModel = viewModel)
38 | }
39 | }
40 | }
41 |
42 | override fun onPause() {
43 | super.onPause()
44 | viewModel.pauseEmulation()
45 | }
46 |
47 | override fun onResume() {
48 | super.onResume()
49 | viewModel.startEmulation()
50 | }
51 |
52 | private fun handleShortcutLaunch() {
53 | val extras = intent.extras
54 | val path = extras?.getString("path")
55 |
56 | if (path !== null) {
57 | viewModel.loadRomFromFile(this, Uri.parse(path))
58 | viewModel.setShortcutLaunch()
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/rom/sheet/TopRow.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.rom.sheet
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.res.painterResource
10 | import androidx.compose.ui.text.font.FontWeight
11 | import androidx.compose.ui.text.style.TextOverflow
12 | import androidx.compose.ui.unit.dp
13 | import dev.luckasranarison.mes.R
14 | import dev.luckasranarison.mes.data.RomFile
15 | import dev.luckasranarison.mes.extra.createShortcut
16 | import dev.luckasranarison.mes.ui.rom.InitialBox
17 | import dev.luckasranarison.mes.ui.theme.Typography
18 |
19 | @Composable
20 | fun TopRow(rom: RomFile) {
21 | val ctx = LocalContext.current
22 |
23 | Row(
24 | verticalAlignment = Alignment.CenterVertically,
25 | modifier = Modifier.fillMaxWidth()
26 | ) {
27 | InitialBox(
28 | name = rom.baseName(),
29 | modifier = Modifier,
30 | foreground = MaterialTheme.colorScheme.onPrimary,
31 | background = MaterialTheme.colorScheme.primary
32 | )
33 |
34 | Spacer(modifier = Modifier.width(16.dp))
35 |
36 | Text(
37 | text = rom.baseName(),
38 | style = Typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
39 | modifier = Modifier.weight(1f),
40 | maxLines = 1,
41 | overflow = TextOverflow.Ellipsis
42 | )
43 |
44 | IconButton(onClick = { createShortcut(ctx, rom) }) {
45 | Icon(
46 | painter = painterResource(id = R.drawable.app_shortcut),
47 | contentDescription = "Shortcut",
48 | modifier = Modifier.size(20.dp)
49 | )
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/license/LibraryContainer.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.license
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.style.TextOverflow
15 | import androidx.compose.ui.unit.dp
16 | import com.mikepenz.aboutlibraries.entity.Library
17 | import com.mikepenz.aboutlibraries.ui.compose.m3.util.author
18 | import dev.luckasranarison.mes.ui.theme.Typography
19 |
20 | @Composable
21 | fun LibraryContainer(lib: Library) {
22 | var showSheet by remember { mutableStateOf(false) }
23 |
24 | if (showSheet) {
25 | BottomSheet(library = lib, onClose = { showSheet = false })
26 | }
27 |
28 | Box(modifier = Modifier
29 | .fillMaxWidth()
30 | .clickable { showSheet = true }
31 | ) {
32 | Row(
33 | modifier = Modifier
34 | .padding(horizontal = 16.dp, vertical = 12.dp)
35 | .fillMaxWidth(),
36 | verticalAlignment = Alignment.CenterVertically,
37 | horizontalArrangement = Arrangement.SpaceBetween
38 | ) {
39 | Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
40 | Text(text = lib.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
41 | Text(
42 | text = lib.author,
43 | style = Typography.titleSmall,
44 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)
45 | )
46 | }
47 | Text(text = lib.artifactVersion ?: "Unknown")
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/registers/status.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitFlag;
2 |
3 | mod status_flag {
4 | pub const O: u8 = 5;
5 | pub const S: u8 = 6;
6 | pub const V: u8 = 7;
7 | }
8 |
9 | /// VSO. ....
10 | /// |||| ||||
11 | /// |||+-++++- PPU open bus. Returns stale PPU bus contents.
12 | /// ||+------- Sprite overflow. The intent was for this flag to be set
13 | /// || whenever more than eight sprites appear on a scanline, but a
14 | /// || hardware bug causes the actual behavior to be more complicated
15 | /// || and generate false positives as well as false negatives; see
16 | /// || PPU sprite evaluation. This flag is set during sprite
17 | /// || evaluation and cleared at dot 1 (the second dot) of the
18 | /// || pre-render line.
19 | /// |+-------- Sprite 0 Hit. Set when a nonzero pixel of sprite 0 overlaps
20 | /// | a nonzero background pixel; cleared at dot 1 of the pre-render
21 | /// | line. Used for raster timing.
22 | /// +--------- Vertical blank has started (0: not in vblank; 1: in vblank).
23 | /// Set at dot 1 of line 241 (the line *after* the post-render
24 | /// line); cleared after reading $2002 and at dot 1 of the
25 | /// pre-render line.
26 | #[derive(Debug, Default)]
27 | pub struct StatusRegister(u8);
28 |
29 | impl StatusRegister {
30 | pub fn read(&self) -> u8 {
31 | self.0
32 | }
33 |
34 | pub fn set_vblank(&mut self) {
35 | self.0.update(status_flag::V, true);
36 | }
37 |
38 | pub fn set_sprite_overflow(&mut self) {
39 | self.0.update(status_flag::O, true);
40 | }
41 |
42 | pub fn set_sprite_zero_hit(&mut self) {
43 | self.0.update(status_flag::S, true);
44 | }
45 |
46 | pub fn clear_vblank(&mut self) {
47 | self.0.update(status_flag::V, false);
48 | }
49 |
50 | pub fn is_vblank(&self) -> bool {
51 | self.0.contains(status_flag::V)
52 | }
53 |
54 | pub fn clear(&mut self) {
55 | self.0 &= 0b0001_1111;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/crates/mes-core/src/utils/test.rs:
--------------------------------------------------------------------------------
1 | use std::{error::Error, fmt};
2 |
3 | use super::BitFlag;
4 |
5 | pub const NESTEST_ROM: &[u8] = include_bytes!("../../../../nes-test-roms/other/nestest.nes");
6 | pub const NESTEST_LOG: &str = include_str!("../../../../nes-test-roms/other/nestest.log");
7 |
8 | /// Represents parsed lines from nestest.log
9 | pub struct LogLine {
10 | pub pc: u16,
11 | pub opcode: u8,
12 | pub a: u8,
13 | pub x: u8,
14 | pub y: u8,
15 | pub sr: u8,
16 | pub sp: u8,
17 | pub cycle: u64,
18 | }
19 |
20 | impl LogLine {
21 | pub fn from_line(line: &str) -> Result> {
22 | Ok(LogLine {
23 | pc: u16::from_str_radix(&line[..4], 16)?,
24 | opcode: u8::from_str_radix(&line[6..8], 16)?,
25 | a: u8::from_str_radix(&line[50..52], 16)?,
26 | x: u8::from_str_radix(&line[55..57], 16)?,
27 | y: u8::from_str_radix(&line[60..62], 16)?,
28 | sr: u8::from_str_radix(&line[65..67], 16)?,
29 | sp: u8::from_str_radix(&line[71..73], 16)?,
30 | cycle: line[90..].parse()?,
31 | })
32 | }
33 | }
34 |
35 | impl fmt::Debug for LogLine {
36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 | writeln!(f, "LogLine {{")?;
38 | writeln!(f, " opcode: 0x{:x},", self.opcode)?;
39 | writeln!(f, " pc: 0x{:x},", self.pc)?;
40 | writeln!(f, " ac: 0x{:x},", self.a)?;
41 | writeln!(f, " x: 0x{:x},", self.x)?;
42 | writeln!(f, " y: 0x{:x},", self.y)?;
43 | writeln!(
44 | f,
45 | " sr: {{ N: {}, V: {}, _: {}, B: {}, D: {}, I: {}, Z: {}, C: {} }},",
46 | self.sr.get(7),
47 | self.sr.get(6),
48 | self.sr.get(5),
49 | self.sr.get(6),
50 | self.sr.get(3),
51 | self.sr.get(2),
52 | self.sr.get(1),
53 | self.sr.get(0)
54 | )?;
55 | writeln!(f, " sp: 0x{:x},", self.sp)?;
56 | writeln!(f, " cycle: {}", self.cycle)?;
57 | writeln!(f, "}}")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/rom/RomList.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.rom
2 |
3 | import android.net.Uri
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.pulltorefresh.*
9 | import androidx.compose.runtime.*
10 | import androidx.compose.runtime.setValue
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import dev.luckasranarison.mes.data.RomFile
15 | import kotlinx.coroutines.launch
16 |
17 | @Composable
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | fun RomList(
20 | modifier: Modifier,
21 | romFiles: List,
22 | onSelect: (Uri) -> Unit,
23 | onRefresh: suspend () -> Unit
24 | ) {
25 | val pullToRefreshState = rememberPullToRefreshState()
26 | val coroutineScope = rememberCoroutineScope()
27 | var isRefreshing by remember { mutableStateOf(false) }
28 |
29 | PullToRefreshBox(
30 | modifier = modifier.fillMaxSize(),
31 | state = pullToRefreshState,
32 | isRefreshing = isRefreshing,
33 | onRefresh = {
34 | coroutineScope.launch {
35 | isRefreshing = true
36 | onRefresh()
37 | isRefreshing = false
38 | }
39 | },
40 | indicator = {
41 | PullToRefreshDefaults.Indicator(
42 | state = pullToRefreshState,
43 | isRefreshing = isRefreshing,
44 | color = Color.White,
45 | containerColor = MaterialTheme.colorScheme.primary,
46 | modifier = Modifier.align(Alignment.TopCenter)
47 | )
48 | }
49 | ) {
50 | LazyColumn(modifier = Modifier.fillMaxSize()) {
51 | items(romFiles.size) { index ->
52 | val rom = romFiles[index]
53 | RomContainer(
54 | rom = rom,
55 | onSelect = { onSelect(rom.uri) },
56 | )
57 | }
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/rom/sheet/Metadata.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.rom.sheet
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.text.style.TextAlign
9 | import androidx.compose.ui.unit.dp
10 | import dev.luckasranarison.mes.data.RomFile
11 | import dev.luckasranarison.mes.lib.CHR_ROM_PAGE_SIZE
12 | import dev.luckasranarison.mes.lib.PRG_RAM_SIZE
13 | import dev.luckasranarison.mes.lib.PRG_ROM_PAGE_SIZE
14 | import dev.luckasranarison.mes.ui.theme.Typography
15 |
16 | fun formatPage(count: Byte, size: Int) =
17 | if (count > 0) "$count (${count * size / 1024} KB)" else "None"
18 |
19 | @Composable
20 | fun Metadata(key: String, value: String) {
21 | Row(
22 | modifier = Modifier
23 | .fillMaxWidth()
24 | .padding(vertical = 8.dp),
25 | horizontalArrangement = Arrangement.SpaceBetween
26 | ) {
27 | Text(
28 | text = key,
29 | style = Typography.bodyMedium,
30 | fontWeight = FontWeight.Bold,
31 | modifier = Modifier.weight(1f),
32 | )
33 | Text(
34 | text = value,
35 | style = Typography.bodyMedium,
36 | modifier = Modifier.weight(1f),
37 | textAlign = TextAlign.End,
38 | color = MaterialTheme.colorScheme.onSurface
39 | )
40 | }
41 | }
42 |
43 | @Composable
44 | fun MetadataList(rom: RomFile) {
45 | val attributes = rom.getAttributes()
46 |
47 | Metadata("Attributes", if (attributes.isEmpty()) "None" else attributes.joinToString())
48 | Metadata("Size", "${rom.size / 1024} KB")
49 | Metadata("Mapper", rom.header.mapper.toString())
50 | Metadata("Mirroring", rom.header.mirroring)
51 | Metadata("Battery", if (rom.header.battery) "Yes" else "No")
52 | Metadata("PRG ROM", formatPage(rom.header.prgRomPages, PRG_ROM_PAGE_SIZE))
53 | Metadata("PRG RAM", formatPage(rom.header.prgRamPages, PRG_RAM_SIZE))
54 | Metadata("CHR ROM", formatPage(rom.header.chrRomPages, CHR_ROM_PAGE_SIZE))
55 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod apu;
2 | pub mod bus;
3 | pub mod cartridge;
4 | pub mod controller;
5 | pub mod cpu;
6 | pub mod error;
7 | pub mod mappers;
8 | pub mod ppu;
9 | pub mod utils;
10 |
11 | mod features;
12 |
13 | #[cfg(feature = "json")]
14 | pub use features::json;
15 |
16 | use std::cell::Ref;
17 |
18 | use bus::MainBus;
19 | use cpu::Cpu;
20 | use error::Error;
21 | use mappers::MapperChip;
22 | use utils::Reset;
23 |
24 | #[derive(Debug)]
25 | pub struct Nes {
26 | pub(crate) cpu: Cpu,
27 | }
28 |
29 | impl Nes {
30 | pub fn new(bytes: &[u8]) -> Result {
31 | let mapper = MapperChip::try_from_bytes(bytes)?;
32 | let bus = MainBus::new(mapper);
33 | let cpu = Cpu::new(bus);
34 |
35 | Ok(Self { cpu })
36 | }
37 |
38 | pub fn with_mapper(mapper: MapperChip) -> Self {
39 | let bus = MainBus::new(mapper);
40 | let cpu = Cpu::new(bus);
41 |
42 | Self { cpu }
43 | }
44 |
45 | pub fn set_mapper(&mut self, mapper: MapperChip) {
46 | self.cpu.bus.set_mapper(mapper);
47 | }
48 |
49 | pub fn set_cartridge(&mut self, bytes: &[u8]) -> Result<(), Error> {
50 | let mapper = MapperChip::try_from_bytes(bytes)?;
51 | self.set_mapper(mapper);
52 |
53 | Ok(())
54 | }
55 |
56 | pub fn reset(&mut self) {
57 | self.cpu.reset();
58 | }
59 |
60 | pub fn step(&mut self) {
61 | self.cpu.step();
62 | }
63 |
64 | pub fn step_frame(&mut self) {
65 | while !self.cpu.bus.ppu.is_vblank() {
66 | self.step();
67 | }
68 | }
69 |
70 | pub fn step_vblank(&mut self) {
71 | while self.cpu.bus.ppu.is_vblank() {
72 | self.step();
73 | }
74 | }
75 |
76 | pub fn get_audio_buffer(&self) -> Ref<[f32]> {
77 | Ref::map(self.cpu.apu.borrow(), |apu| apu.get_buffer())
78 | }
79 |
80 | pub fn clear_audio_buffer(&mut self) {
81 | self.cpu.apu.borrow_mut().clear_buffer();
82 | }
83 |
84 | pub fn get_frame_buffer(&self) -> &[u8] {
85 | self.cpu.bus.ppu.get_frame_buffer()
86 | }
87 |
88 | pub fn set_controller_state(&mut self, id: usize, state: u8) {
89 | self.cpu.bus.controller.set_state(id, state);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/info/InfoScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.info
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.platform.LocalClipboardManager
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.platform.LocalUriHandler
10 | import androidx.compose.ui.text.AnnotatedString
11 | import androidx.compose.ui.unit.dp
12 | import androidx.navigation.NavHostController
13 | import dev.luckasranarison.mes.Routes
14 | import dev.luckasranarison.mes.ui.shared.GenericTopAppBar
15 |
16 | const val AUTHOR_EMAIL = "luckasranarison@gmail.com"
17 | const val REPOSITORY_URL = "https://github.com/luckasranarison/mes"
18 |
19 | @Composable
20 | fun Info(controller: NavHostController) {
21 | val ctx = LocalContext.current
22 | val clipboardManager = LocalClipboardManager.current
23 | val uriHandler = LocalUriHandler.current
24 | val version = remember { getAppVersion(ctx) }
25 |
26 | Scaffold(
27 | topBar = {
28 | GenericTopAppBar(
29 | title = "About",
30 | onExit = { controller.popBackStack() }
31 | )
32 | },
33 | ) { innerPadding ->
34 | Column(
35 | modifier = Modifier
36 | .padding(innerPadding)
37 | .fillMaxSize()
38 | ) {
39 | AppIcon()
40 |
41 | HorizontalDivider(thickness = 1.dp)
42 |
43 | Section(
44 | title = "Version",
45 | description = version,
46 | onClick = { clipboardManager.setText(AnnotatedString("Mes v${version}")) }
47 | )
48 | Section(
49 | title = "Author",
50 | description = AUTHOR_EMAIL,
51 | onClick = { uriHandler.openUri(makeMailMessage(AUTHOR_EMAIL)) }
52 | )
53 | Section(
54 | title = "Source",
55 | description = REPOSITORY_URL,
56 | onClick = { uriHandler.openUri(REPOSITORY_URL) }
57 | )
58 | Section(
59 | title = "Open source license",
60 | onClick = { controller.navigate(Routes.LICENSE) }
61 | )
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/ppu/registers/control.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::BitFlag;
2 |
3 | #[allow(unused)]
4 | #[rustfmt::skip]
5 | mod control_flag {
6 | pub const N0: u8 = 0;
7 | pub const N1: u8 = 1;
8 | pub const I : u8 = 2;
9 | pub const S : u8 = 3;
10 | pub const B : u8 = 4;
11 | pub const H : u8 = 5;
12 | pub const P : u8 = 6;
13 | pub const V : u8 = 7;
14 | }
15 |
16 | /// VPHB SINN
17 | /// |||| ||||
18 | /// |||| ||++- Base nametable address
19 | /// |||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
20 | /// |||| |+--- VRAM address increment per CPU read/write of PPUDATA
21 | /// |||| | (0: add 1, going across; 1: add 32, going down)
22 | /// |||| +---- Sprite pattern table address for 8x8 sprites
23 | /// |||| (0: $0000; 1: $1000; ignored in 8x16 mode)
24 | /// |||+------ Background pattern table address (0: $0000; 1: $1000)
25 | /// ||+------- Sprite size (0: 8x8 pixels; 1: 8x16 pixels – see PPU OAM#Byte 1)
26 | /// |+-------- PPU master/slave select
27 | /// | (0: read backdrop from EXT pins; 1: output color on EXT pins)
28 | /// +--------- Generate an NMI at the start of the
29 | /// vertical blanking interval (0: off; 1: on)
30 | #[derive(Debug, Default)]
31 | pub struct ControlRegister(u8);
32 |
33 | impl ControlRegister {
34 | pub fn write(&mut self, value: u8) {
35 | self.0 = value;
36 | }
37 |
38 | pub fn get_nametable_bits(&self) -> u8 {
39 | self.0.get(control_flag::N1) * 2 + self.0.get(control_flag::N0)
40 | }
41 |
42 | pub fn get_vram_increment_value(&self) -> u8 {
43 | match self.0.contains(control_flag::I) {
44 | true => 32,
45 | false => 1,
46 | }
47 | }
48 |
49 | pub fn get_sprite_pattern_table_address(&self) -> u16 {
50 | match self.0.contains(control_flag::S) {
51 | true => 0x1000,
52 | false => 0x0000,
53 | }
54 | }
55 |
56 | pub fn get_background_pattern_table_address(&self) -> u16 {
57 | match self.0.get(control_flag::B) == 1 {
58 | true => 0x1000,
59 | false => 0x0000,
60 | }
61 | }
62 |
63 | pub fn get_sprite_height(&self) -> u8 {
64 | match self.0.contains(control_flag::H) {
65 | true => 16,
66 | false => 8,
67 | }
68 | }
69 |
70 | pub fn generate_nmi(&self) -> bool {
71 | self.0.contains(control_flag::V)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/ui/gamepad/GamePadLayout.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.ui.gamepad
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Settings
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.IconButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.dp
14 | import dev.luckasranarison.mes.ui.settings.FloatingSettings
15 | import dev.luckasranarison.mes.vm.EmulatorViewModel
16 |
17 | @Composable
18 | fun GamePadLayout(viewModel: EmulatorViewModel) {
19 | var showSettings by remember { mutableStateOf(false) }
20 |
21 | if (showSettings) {
22 | FloatingSettings(
23 | viewModel = viewModel,
24 | onExit = { showSettings = false }
25 | )
26 | }
27 |
28 | LaunchedEffect(showSettings) {
29 | if (showSettings) {
30 | viewModel.pauseEmulation()
31 | } else {
32 | viewModel.startEmulation()
33 | }
34 | }
35 |
36 | Box(modifier = Modifier.fillMaxSize()) {
37 | DirectionPad(
38 | modifier = Modifier
39 | .align(Alignment.CenterStart)
40 | .padding(start = 72.dp, top = 72.dp),
41 | onPress = viewModel::updateController
42 | )
43 | MenuPad(
44 | modifier = Modifier
45 | .align(Alignment.BottomCenter)
46 | .padding(bottom = 16.dp),
47 | onPress = viewModel::updateController
48 | )
49 | ActionPad(
50 | modifier = Modifier
51 | .align(Alignment.CenterEnd)
52 | .padding(end = 48.dp, top = 72.dp),
53 | onPress = viewModel::updateController
54 | )
55 | IconButton(
56 | onClick = { showSettings = true },
57 | modifier = Modifier
58 | .align(Alignment.TopEnd)
59 | .padding(24.dp)
60 | ) {
61 | Icon(
62 | imageVector = Icons.Default.Settings,
63 | contentDescription = "Settings",
64 | tint = Color.Gray,
65 | modifier = Modifier.size(32.dp)
66 | )
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/Application.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import androidx.navigation.compose.NavHost
9 | import androidx.navigation.compose.composable
10 | import androidx.navigation.compose.rememberNavController
11 | import dev.luckasranarison.mes.anim.Animations
12 | import dev.luckasranarison.mes.ui.emulator.Emulator
13 | import dev.luckasranarison.mes.vm.EmulatorViewModel
14 | import dev.luckasranarison.mes.ui.home.Home
15 | import dev.luckasranarison.mes.ui.info.Info
16 | import dev.luckasranarison.mes.ui.license.License
17 | import dev.luckasranarison.mes.ui.settings.Settings
18 |
19 | data object Routes {
20 | const val HOME = "home"
21 | const val EMULATOR = "emulator"
22 | const val SETTINGS = "settings"
23 | const val INFO = "info"
24 | const val LICENSE = "licenses"
25 | }
26 |
27 | @Composable
28 | fun App(viewModel: EmulatorViewModel) {
29 | val navController = rememberNavController()
30 | val isShortcutLaunch by remember { viewModel.isShortcutLaunch }
31 |
32 | NavHost(
33 | navController = navController,
34 | startDestination = if (isShortcutLaunch) Routes.EMULATOR else Routes.HOME,
35 | enterTransition = { Animations.EnterTransition },
36 | exitTransition = { Animations.ExitTransition },
37 | popEnterTransition = { Animations.PopEnterTransition },
38 | popExitTransition = { Animations.PopExitTransition }
39 | ) {
40 | composable(Routes.HOME) {
41 | Home(viewModel = viewModel, controller = navController)
42 | }
43 | composable(Routes.EMULATOR, popExitTransition = { ExitTransition.None }) {
44 | Emulator(viewModel = viewModel, controller = navController)
45 | }
46 | composable(Routes.INFO) {
47 | Info(controller = navController)
48 | }
49 | composable(Routes.LICENSE) {
50 | License(controller = navController)
51 | }
52 | composable(Routes.SETTINGS, enterTransition = {
53 | if (initialState.destination.route == Routes.EMULATOR) EnterTransition.None else null
54 | }) {
55 | Settings(viewModel = viewModel, onExit = navController::popBackStack)
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/frame_counter.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/APU_Frame_Counter
2 |
3 | use crate::utils::{BitFlag, Clock};
4 |
5 | mod status_flag {
6 | pub const I: u8 = 6;
7 | pub const M: u8 = 7;
8 | }
9 |
10 | #[derive(Debug, PartialEq, Eq)]
11 | enum Mode {
12 | FourSteps,
13 | FiveSteps,
14 | }
15 |
16 | pub trait ClockFrame {
17 | fn tick_frame(&mut self, frame: &Frame);
18 | }
19 |
20 | #[derive(Debug)]
21 | pub enum Frame {
22 | Quarter,
23 | Half,
24 | }
25 |
26 | impl Frame {
27 | pub fn is_half(&self) -> bool {
28 | matches!(self, Frame::Half)
29 | }
30 | }
31 |
32 | #[derive(Debug, Default)]
33 | pub struct FrameCounter {
34 | flags: u8,
35 | sequencer: u32,
36 | frame: Option,
37 | interrupt: bool,
38 | }
39 |
40 | impl FrameCounter {
41 | pub fn write(&mut self, value: u8) {
42 | self.flags = value;
43 | self.interrupt = self.interrupt && !value.contains(status_flag::I);
44 | self.sequencer = 0; // FIXME: apply 3-4 cycle delay
45 | }
46 |
47 | pub fn take_frame(&mut self) -> Option {
48 | self.frame.take()
49 | }
50 |
51 | pub fn irq(&self) -> bool {
52 | self.interrupt
53 | }
54 |
55 | pub fn clear_irq(&mut self) {
56 | self.interrupt = false;
57 | }
58 |
59 | fn sequencer_mode(&self) -> Mode {
60 | match self.flags.contains(status_flag::M) {
61 | true => Mode::FiveSteps,
62 | false => Mode::FourSteps,
63 | }
64 | }
65 |
66 | fn set_interrupt(&mut self) {
67 | self.interrupt = !self.flags.contains(status_flag::I);
68 | }
69 | }
70 |
71 | impl Clock for FrameCounter {
72 | fn tick(&mut self) {
73 | let mode = self.sequencer_mode();
74 |
75 | match (self.sequencer, mode) {
76 | (14913, _) => self.frame = Some(Frame::Half),
77 | (7457, _) | (22371, _) => self.frame = Some(Frame::Quarter),
78 | (29828, Mode::FourSteps) => self.set_interrupt(),
79 | (29829, Mode::FourSteps) => {
80 | self.set_interrupt();
81 | self.frame = Some(Frame::Half);
82 | }
83 | (29830, Mode::FourSteps) => {
84 | self.set_interrupt();
85 | self.sequencer = 0;
86 | }
87 | (37281, Mode::FiveSteps) => self.frame = Some(Frame::Half),
88 | (37282, Mode::FiveSteps) => self.sequencer = 0,
89 | _ => {}
90 | };
91 |
92 | self.sequencer += 1;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/android/app/src/main/java/dev/luckasranarison/mes/lib/Nes.kt:
--------------------------------------------------------------------------------
1 | package dev.luckasranarison.mes.lib
2 |
3 | import android.util.Log
4 |
5 | typealias NesPtr = Long
6 |
7 | object Nes {
8 | external fun init(): NesPtr
9 | external fun reset(nes: NesPtr)
10 | external fun setCartridge(nes: NesPtr, bytes: ByteArray)
11 | external fun stepFrame(nes: NesPtr)
12 | external fun stepVBlank(nes: NesPtr)
13 | external fun fillAudioBuffer(nes: NesPtr, buffer: FloatArray): Int
14 | external fun clearAudioBuffer(nes: NesPtr)
15 | external fun fillFrameBuffer(nes: NesPtr, buffer: IntArray, palette: ByteArray?)
16 | external fun setControllerState(nes: NesPtr, id: Long, state: Byte)
17 | external fun free(nes: NesPtr)
18 | external fun serializeRomHeader(rom: ByteArray): String
19 | }
20 |
21 | const val AUDIO_BUFFER_SIZE = 1024
22 | const val SCREEN_WIDTH = 256
23 | const val SCREEN_HEIGHT = 240
24 | const val FRAME_BUFFER_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT
25 | const val COLOR_PALETTE_SIZE = 192
26 | const val FRAME_DURATION = 1_000_000_000 / 60
27 | const val PRG_ROM_PAGE_SIZE = 16384;
28 | const val PRG_RAM_SIZE = 8192;
29 | const val CHR_ROM_PAGE_SIZE = 8192;
30 | val INES_ASCII = byteArrayOf(0x4E, 0x45, 0x53, 0x1A)
31 |
32 | class NesObject {
33 | private val ptr = Nes.init()
34 | private val audioBuffer = FloatArray(AUDIO_BUFFER_SIZE)
35 | private val frameBuffer = IntArray(FRAME_BUFFER_SIZE)
36 | private var colorPalette: ByteArray? = null
37 |
38 | init {
39 | Log.i("mes", "Emulator instance was created")
40 | }
41 |
42 | fun reset() = Nes.reset(ptr)
43 | fun setCartridge(bytes: ByteArray) = Nes.setCartridge(ptr, bytes)
44 | fun stepFrame() = Nes.stepFrame(ptr)
45 | fun stepVBlank() = Nes.stepVBlank(ptr)
46 | fun clearAudioBuffer() = Nes.clearAudioBuffer(ptr)
47 | fun setControllerState(id: Long, state: Byte) = Nes.setControllerState(ptr, id, state)
48 |
49 | fun updateFrameBuffer(): IntArray {
50 | Nes.fillFrameBuffer(ptr, frameBuffer, colorPalette)
51 | return frameBuffer
52 | }
53 |
54 | fun updateAudioBuffer(): Pair {
55 | val length = Nes.fillAudioBuffer(ptr, audioBuffer)
56 | return Pair(audioBuffer, length)
57 | }
58 |
59 | fun setColorPalette(palette: ByteArray?) {
60 | if (palette == null || palette.size == COLOR_PALETTE_SIZE) {
61 | colorPalette = palette
62 | } else {
63 | throw Exception("Invalid color palette")
64 | }
65 | }
66 |
67 | fun free() {
68 | Nes.free(ptr)
69 | Log.i("mes", "Emulator instance was destroyed")
70 | }
71 | }
--------------------------------------------------------------------------------
/crates/mes-core/src/apu/channels/noise.rs:
--------------------------------------------------------------------------------
1 | // https://www.nesdev.org/wiki/APU_Noise
2 |
3 | use crate::{
4 | apu::frame_counter::{ClockFrame, Frame},
5 | utils::{BitFlag, Clock},
6 | };
7 |
8 | use super::common::{Channel, Envelope, LengthCounter, Timer};
9 |
10 | #[derive(Debug, Default)]
11 | pub struct Noise {
12 | envelope: Envelope,
13 | timer: Timer,
14 | length_counter: LengthCounter,
15 | mode: bool,
16 | shift: u16,
17 | }
18 |
19 | impl Noise {
20 | #[rustfmt::skip]
21 | const PERIODS: [u16; 16] = [
22 | 0x004, 0x008, 0x010, 0x020, 0x040, 0x060, 0x080, 0x0A0,
23 | 0x0CA, 0x0FE, 0x17C, 0x1FC, 0x2FA, 0x3F8, 0x7F2, 0xFE4,
24 | ];
25 |
26 | pub fn new() -> Self {
27 | Self {
28 | shift: 1,
29 | ..Default::default()
30 | }
31 | }
32 | }
33 |
34 | impl Channel for Noise {
35 | fn write_register(&mut self, address: u16, value: u8) {
36 | match address % 4 {
37 | 0 => {
38 | self.envelope.write(value.get_range(0..5));
39 | self.length_counter.set_halt(value.contains(5));
40 | }
41 | 2 => {
42 | let index = value.get_range(0..4) as usize;
43 | self.mode = value.contains(7);
44 | self.timer.period = Self::PERIODS[index];
45 | }
46 | 3 => {
47 | self.length_counter.set_length(value >> 3);
48 | self.envelope.restart();
49 | }
50 | _ => {} // unused
51 | }
52 | }
53 |
54 | fn raw_sample(&self) -> u8 {
55 | self.envelope.volume()
56 | }
57 |
58 | fn is_active(&self) -> bool {
59 | self.length_counter.is_active()
60 | }
61 |
62 | fn set_enabled(&mut self, value: bool) {
63 | self.length_counter.set_enabled(value);
64 | }
65 |
66 | fn is_mute(&self) -> bool {
67 | !self.length_counter.is_active() || self.shift.contains(0)
68 | }
69 | }
70 |
71 | impl Clock for Noise {
72 | fn tick(&mut self) {
73 | self.timer.tick();
74 |
75 | if self.timer.is_zero() {
76 | let rhs_bit = if self.mode { 6 } else { 1 };
77 | let rhs = self.shift.get(rhs_bit);
78 | let lhs = self.shift.get(0);
79 | let feedback = lhs ^ rhs;
80 | self.shift = (self.shift >> 1) | (feedback << 14);
81 | }
82 | }
83 | }
84 |
85 | impl ClockFrame for Noise {
86 | fn tick_frame(&mut self, frame: &Frame) {
87 | self.envelope.tick();
88 |
89 | if frame.is_half() {
90 | self.length_counter.tick();
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/crates/mes-core/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | use std::ops::{BitAnd, BitAndAssign, BitOrAssign, Not, Range, Shl, Shr, Sub};
2 |
3 | #[cfg(test)]
4 | pub mod test;
5 |
6 | pub trait BitFlag {
7 | fn get(&self, flag: T) -> T;
8 | fn get_range(&self, range: Range) -> T;
9 | fn contains(&self, flag: T) -> bool;
10 | fn set(&mut self, flag: T);
11 | fn clear(&mut self, flag: T);
12 | fn update(&mut self, flag: T, cond: bool);
13 | }
14 |
15 | impl BitFlag for T
16 | where
17 | T: Clone
18 | + Copy
19 | + PartialEq
20 | + Shr
21 | + Shl
22 | + Sub
23 | + BitAnd
24 | + BitAndAssign
25 | + BitOrAssign
26 | + Not