├── .deepsource.toml
├── .github
├── dependabot.yml
└── workflows
│ └── rust.yml
├── .gitignore
├── .rustfmt.toml
├── Cargo.lock
├── Cargo.toml
├── LICENSE.md
├── NOTE.md
├── README.md
├── assets
├── alleyway.gif
├── dmg-acid2.jpg
├── donkeykong.gif
├── frogger.gif
├── galaga.gif
├── mario.gif
├── mario2.gif
├── mortalkombat.gif
├── pacman.gif
├── roadrash.gif
├── spaceinvaders.gif
├── tetris.gif
└── zelda.gif
├── core
├── Cargo.toml
└── src
│ ├── alu.rs
│ ├── callbacks.rs
│ ├── cartridge.rs
│ ├── cgb_dma.rs
│ ├── colour
│ ├── bg_map_attributes.rs
│ ├── colour.rs
│ ├── grey_shades.rs
│ ├── mod.rs
│ └── palette_ram.rs
│ ├── config.rs
│ ├── constants.rs
│ ├── cpu.rs
│ ├── gpu.rs
│ ├── helpers.rs
│ ├── interrupts.rs
│ ├── joypad.rs
│ ├── lcd.rs
│ ├── lib.rs
│ ├── memory
│ ├── battery_backed_ram.rs
│ ├── cgb_speed_switch.rs
│ ├── mbcs
│ │ ├── mbc1.rs
│ │ ├── mbc2.rs
│ │ ├── mbc3.rs
│ │ ├── mbc5.rs
│ │ ├── mod.rs
│ │ └── none.rs
│ ├── memory.rs
│ ├── mod.rs
│ ├── ram.rs
│ ├── rom.rs
│ └── vram.rs
│ ├── registers.rs
│ ├── serial_cable.rs
│ └── sound
│ ├── apu.rs
│ ├── channel1.rs
│ ├── channel2.rs
│ ├── channel3.rs
│ ├── channel4.rs
│ ├── length_function.rs
│ ├── mod.rs
│ ├── registers.rs
│ └── volume_envelope.rs
├── libretro
├── Cargo.toml
├── libgbrs_libretro.info
├── run.sh
└── src
│ └── lib.rs
├── profiling
├── Cargo.toml
└── src
│ └── main.rs
├── roms
├── DMG-ACID2-LICENSE
└── dmg-acid2.gb
├── sdl-gui
├── Cargo.toml
├── build.rs
└── src
│ ├── gui.rs
│ └── main.rs
├── sfml-gui
├── Cargo.toml
└── src
│ ├── control.rs
│ ├── gui.rs
│ └── main.rs
└── wasm-gui
├── Cargo.lock
├── Cargo.toml
├── buildAndServe.sh
├── index.html
└── src
└── lib.rs
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [[analyzers]]
4 | name = "rust"
5 |
6 | [analyzers.meta]
7 | msrv = "stable"
8 |
9 | [[analyzers]]
10 | name = "shell"
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "cargo"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
11 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 |
8 | env:
9 | CARGO_TERM_COLOR: always
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Install dependencies
17 | run: |
18 | sudo apt-get update
19 | sudo apt-get install libpthread-stubs0-dev libgl1-mesa-dev libx11-dev libx11-xcb-dev libxcb-image0-dev libxrandr-dev libxcb-randr0-dev libudev-dev libfreetype6-dev libglew-dev libjpeg8-dev libgpgme11-dev libsndfile1-dev libopenal-dev libjpeg62 libxcursor-dev cmake libclang-dev clang libsfml-dev
20 | - name: Build
21 | run: cargo build
22 | - name: Run tests
23 | run: cargo test
24 | - name: Run tests without default features
25 | run: cargo test --no-default-features
26 | - name: Run clippy
27 | uses: actions-rs/clippy-check@v1
28 | with:
29 | token: ${{ secrets.GITHUB_TOKEN }}
30 |
31 | format:
32 | runs-on: ubuntu-latest
33 | steps:
34 | - uses: actions/checkout@v4
35 | - name: Format Rust code
36 | run: cargo fmt --all -- --check
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .DS_Store
3 | /roms
4 | flamegraph.svg
5 | bytes.sav
6 |
--------------------------------------------------------------------------------
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 80
2 | trailing_comma = "Never"
3 | match_block_trailing_comma = true
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "arbitrary-int"
16 | version = "1.2.7"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "c84fc003e338a6f69fbd4f7fe9f92b535ff13e9af8997f3b14b6ddff8b1df46d"
19 |
20 | [[package]]
21 | name = "autocfg"
22 | version = "1.4.0"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
25 |
26 | [[package]]
27 | name = "base64"
28 | version = "0.22.1"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
31 |
32 | [[package]]
33 | name = "bindgen"
34 | version = "0.63.0"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
37 | dependencies = [
38 | "bitflags 1.3.2",
39 | "cexpr",
40 | "clang-sys",
41 | "lazy_static",
42 | "lazycell",
43 | "log",
44 | "peeking_take_while",
45 | "proc-macro2",
46 | "quote",
47 | "regex",
48 | "rustc-hash",
49 | "shlex",
50 | "syn 1.0.109",
51 | "which",
52 | ]
53 |
54 | [[package]]
55 | name = "bitbybit"
56 | version = "1.3.2"
57 | source = "registry+https://github.com/rust-lang/crates.io-index"
58 | checksum = "fb157f9753a7cddfcf4a4f5fed928fbf4ce1b7b64b6bcc121d7a9f95d698997b"
59 | dependencies = [
60 | "arbitrary-int",
61 | "proc-macro2",
62 | "quote",
63 | "syn 2.0.87",
64 | ]
65 |
66 | [[package]]
67 | name = "bitflags"
68 | version = "1.3.2"
69 | source = "registry+https://github.com/rust-lang/crates.io-index"
70 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
71 |
72 | [[package]]
73 | name = "bitflags"
74 | version = "2.6.0"
75 | source = "registry+https://github.com/rust-lang/crates.io-index"
76 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
77 |
78 | [[package]]
79 | name = "bumpalo"
80 | version = "3.16.0"
81 | source = "registry+https://github.com/rust-lang/crates.io-index"
82 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
83 |
84 | [[package]]
85 | name = "c_utf8"
86 | version = "0.1.0"
87 | source = "registry+https://github.com/rust-lang/crates.io-index"
88 | checksum = "f747ed2575d426b7cbf0fcba5872db319a600d597391c339779a3d9835d1ea4d"
89 | dependencies = [
90 | "version_check",
91 | ]
92 |
93 | [[package]]
94 | name = "cc"
95 | version = "1.2.1"
96 | source = "registry+https://github.com/rust-lang/crates.io-index"
97 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
98 | dependencies = [
99 | "shlex",
100 | ]
101 |
102 | [[package]]
103 | name = "cexpr"
104 | version = "0.6.0"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
107 | dependencies = [
108 | "nom",
109 | ]
110 |
111 | [[package]]
112 | name = "cfg-if"
113 | version = "1.0.0"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
116 |
117 | [[package]]
118 | name = "clang-sys"
119 | version = "1.8.1"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
122 | dependencies = [
123 | "glob",
124 | "libc",
125 | "libloading",
126 | ]
127 |
128 | [[package]]
129 | name = "cmake"
130 | version = "0.1.51"
131 | source = "registry+https://github.com/rust-lang/crates.io-index"
132 | checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a"
133 | dependencies = [
134 | "cc",
135 | ]
136 |
137 | [[package]]
138 | name = "console_error_panic_hook"
139 | version = "0.1.7"
140 | source = "registry+https://github.com/rust-lang/crates.io-index"
141 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
142 | dependencies = [
143 | "cfg-if",
144 | "wasm-bindgen",
145 | ]
146 |
147 | [[package]]
148 | name = "either"
149 | version = "1.13.0"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
152 |
153 | [[package]]
154 | name = "errno"
155 | version = "0.3.9"
156 | source = "registry+https://github.com/rust-lang/crates.io-index"
157 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
158 | dependencies = [
159 | "libc",
160 | "windows-sys",
161 | ]
162 |
163 | [[package]]
164 | name = "gbrs-core"
165 | version = "0.2.0"
166 | dependencies = [
167 | "smallvec",
168 | "spin",
169 | ]
170 |
171 | [[package]]
172 | name = "gbrs-libretro"
173 | version = "0.1.0"
174 | dependencies = [
175 | "gbrs-core",
176 | "libretro-rs",
177 | "spin",
178 | ]
179 |
180 | [[package]]
181 | name = "gbrs-sdl-gui"
182 | version = "0.2.0"
183 | dependencies = [
184 | "gbrs-core",
185 | "sdl2",
186 | ]
187 |
188 | [[package]]
189 | name = "gbrs-sfml-gui"
190 | version = "0.2.0"
191 | dependencies = [
192 | "gbrs-core",
193 | "sfml",
194 | "spin",
195 | ]
196 |
197 | [[package]]
198 | name = "gbrs-wasm-gui"
199 | version = "0.1.0"
200 | dependencies = [
201 | "base64",
202 | "console_error_panic_hook",
203 | "gbrs-core",
204 | "wasm-bindgen",
205 | "web-sys",
206 | ]
207 |
208 | [[package]]
209 | name = "glob"
210 | version = "0.3.1"
211 | source = "registry+https://github.com/rust-lang/crates.io-index"
212 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
213 |
214 | [[package]]
215 | name = "home"
216 | version = "0.5.9"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
219 | dependencies = [
220 | "windows-sys",
221 | ]
222 |
223 | [[package]]
224 | name = "js-sys"
225 | version = "0.3.77"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
228 | dependencies = [
229 | "once_cell",
230 | "wasm-bindgen",
231 | ]
232 |
233 | [[package]]
234 | name = "lazy_static"
235 | version = "1.5.0"
236 | source = "registry+https://github.com/rust-lang/crates.io-index"
237 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
238 |
239 | [[package]]
240 | name = "lazycell"
241 | version = "1.3.0"
242 | source = "registry+https://github.com/rust-lang/crates.io-index"
243 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
244 |
245 | [[package]]
246 | name = "libc"
247 | version = "0.2.164"
248 | source = "registry+https://github.com/rust-lang/crates.io-index"
249 | checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
250 |
251 | [[package]]
252 | name = "libflac-sys"
253 | version = "0.3.1"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "5b0d9b582c1affe84b051d5a0faf3665e4da0c0b7b0e3b391213e3a5a670365b"
256 | dependencies = [
257 | "cmake",
258 | "libc",
259 | ]
260 |
261 | [[package]]
262 | name = "libloading"
263 | version = "0.8.5"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
266 | dependencies = [
267 | "cfg-if",
268 | "windows-targets",
269 | ]
270 |
271 | [[package]]
272 | name = "libretro-rs"
273 | version = "0.2.0-SNAPSHOT"
274 | source = "git+https://github.com/libretro-rs/libretro-rs.git#8ebf60c023d2f1d36e41eb8beec3ff86524ca500"
275 | dependencies = [
276 | "arbitrary-int",
277 | "bitbybit",
278 | "c_utf8",
279 | "libretro-rs-ffi",
280 | ]
281 |
282 | [[package]]
283 | name = "libretro-rs-ffi"
284 | version = "0.1.0"
285 | source = "git+https://github.com/libretro-rs/libretro-rs.git#8ebf60c023d2f1d36e41eb8beec3ff86524ca500"
286 | dependencies = [
287 | "bindgen",
288 | "sptr",
289 | ]
290 |
291 | [[package]]
292 | name = "link-cplusplus"
293 | version = "1.0.9"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
296 | dependencies = [
297 | "cc",
298 | ]
299 |
300 | [[package]]
301 | name = "linux-raw-sys"
302 | version = "0.4.14"
303 | source = "registry+https://github.com/rust-lang/crates.io-index"
304 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
305 |
306 | [[package]]
307 | name = "lock_api"
308 | version = "0.4.12"
309 | source = "registry+https://github.com/rust-lang/crates.io-index"
310 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
311 | dependencies = [
312 | "autocfg",
313 | "scopeguard",
314 | ]
315 |
316 | [[package]]
317 | name = "log"
318 | version = "0.4.22"
319 | source = "registry+https://github.com/rust-lang/crates.io-index"
320 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
321 |
322 | [[package]]
323 | name = "memchr"
324 | version = "2.7.4"
325 | source = "registry+https://github.com/rust-lang/crates.io-index"
326 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
327 |
328 | [[package]]
329 | name = "minimal-lexical"
330 | version = "0.2.1"
331 | source = "registry+https://github.com/rust-lang/crates.io-index"
332 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
333 |
334 | [[package]]
335 | name = "nom"
336 | version = "7.1.3"
337 | source = "registry+https://github.com/rust-lang/crates.io-index"
338 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
339 | dependencies = [
340 | "memchr",
341 | "minimal-lexical",
342 | ]
343 |
344 | [[package]]
345 | name = "num-traits"
346 | version = "0.2.19"
347 | source = "registry+https://github.com/rust-lang/crates.io-index"
348 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
349 | dependencies = [
350 | "autocfg",
351 | ]
352 |
353 | [[package]]
354 | name = "once_cell"
355 | version = "1.20.2"
356 | source = "registry+https://github.com/rust-lang/crates.io-index"
357 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
358 |
359 | [[package]]
360 | name = "peeking_take_while"
361 | version = "0.1.2"
362 | source = "registry+https://github.com/rust-lang/crates.io-index"
363 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
364 |
365 | [[package]]
366 | name = "pkg-config"
367 | version = "0.3.31"
368 | source = "registry+https://github.com/rust-lang/crates.io-index"
369 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
370 |
371 | [[package]]
372 | name = "proc-macro2"
373 | version = "1.0.89"
374 | source = "registry+https://github.com/rust-lang/crates.io-index"
375 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
376 | dependencies = [
377 | "unicode-ident",
378 | ]
379 |
380 | [[package]]
381 | name = "profiling"
382 | version = "0.1.0"
383 | dependencies = [
384 | "gbrs-core",
385 | ]
386 |
387 | [[package]]
388 | name = "quote"
389 | version = "1.0.37"
390 | source = "registry+https://github.com/rust-lang/crates.io-index"
391 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
392 | dependencies = [
393 | "proc-macro2",
394 | ]
395 |
396 | [[package]]
397 | name = "regex"
398 | version = "1.11.1"
399 | source = "registry+https://github.com/rust-lang/crates.io-index"
400 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
401 | dependencies = [
402 | "aho-corasick",
403 | "memchr",
404 | "regex-automata",
405 | "regex-syntax",
406 | ]
407 |
408 | [[package]]
409 | name = "regex-automata"
410 | version = "0.4.9"
411 | source = "registry+https://github.com/rust-lang/crates.io-index"
412 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
413 | dependencies = [
414 | "aho-corasick",
415 | "memchr",
416 | "regex-syntax",
417 | ]
418 |
419 | [[package]]
420 | name = "regex-syntax"
421 | version = "0.8.5"
422 | source = "registry+https://github.com/rust-lang/crates.io-index"
423 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
424 |
425 | [[package]]
426 | name = "rustc-hash"
427 | version = "1.1.0"
428 | source = "registry+https://github.com/rust-lang/crates.io-index"
429 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
430 |
431 | [[package]]
432 | name = "rustix"
433 | version = "0.38.41"
434 | source = "registry+https://github.com/rust-lang/crates.io-index"
435 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
436 | dependencies = [
437 | "bitflags 2.6.0",
438 | "errno",
439 | "libc",
440 | "linux-raw-sys",
441 | "windows-sys",
442 | ]
443 |
444 | [[package]]
445 | name = "rustversion"
446 | version = "1.0.19"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
449 |
450 | [[package]]
451 | name = "scopeguard"
452 | version = "1.2.0"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
455 |
456 | [[package]]
457 | name = "sdl2"
458 | version = "0.37.0"
459 | source = "registry+https://github.com/rust-lang/crates.io-index"
460 | checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380"
461 | dependencies = [
462 | "bitflags 1.3.2",
463 | "lazy_static",
464 | "libc",
465 | "sdl2-sys",
466 | ]
467 |
468 | [[package]]
469 | name = "sdl2-sys"
470 | version = "0.37.0"
471 | source = "registry+https://github.com/rust-lang/crates.io-index"
472 | checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3"
473 | dependencies = [
474 | "cfg-if",
475 | "cmake",
476 | "libc",
477 | "version-compare",
478 | ]
479 |
480 | [[package]]
481 | name = "sfml"
482 | version = "0.24.0"
483 | source = "registry+https://github.com/rust-lang/crates.io-index"
484 | checksum = "941169c8be33fd81c006591c0dff6056f94bca0bff6057a262ba946bdb6dc0ed"
485 | dependencies = [
486 | "bitflags 2.6.0",
487 | "cc",
488 | "cmake",
489 | "libflac-sys",
490 | "link-cplusplus",
491 | "num-traits",
492 | "pkg-config",
493 | "widestring",
494 | ]
495 |
496 | [[package]]
497 | name = "shlex"
498 | version = "1.3.0"
499 | source = "registry+https://github.com/rust-lang/crates.io-index"
500 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
501 |
502 | [[package]]
503 | name = "smallvec"
504 | version = "1.15.0"
505 | source = "registry+https://github.com/rust-lang/crates.io-index"
506 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
507 |
508 | [[package]]
509 | name = "spin"
510 | version = "0.9.8"
511 | source = "registry+https://github.com/rust-lang/crates.io-index"
512 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
513 | dependencies = [
514 | "lock_api",
515 | ]
516 |
517 | [[package]]
518 | name = "sptr"
519 | version = "0.3.2"
520 | source = "registry+https://github.com/rust-lang/crates.io-index"
521 | checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
522 |
523 | [[package]]
524 | name = "syn"
525 | version = "1.0.109"
526 | source = "registry+https://github.com/rust-lang/crates.io-index"
527 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
528 | dependencies = [
529 | "proc-macro2",
530 | "quote",
531 | "unicode-ident",
532 | ]
533 |
534 | [[package]]
535 | name = "syn"
536 | version = "2.0.87"
537 | source = "registry+https://github.com/rust-lang/crates.io-index"
538 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
539 | dependencies = [
540 | "proc-macro2",
541 | "quote",
542 | "unicode-ident",
543 | ]
544 |
545 | [[package]]
546 | name = "unicode-ident"
547 | version = "1.0.14"
548 | source = "registry+https://github.com/rust-lang/crates.io-index"
549 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
550 |
551 | [[package]]
552 | name = "version-compare"
553 | version = "0.1.1"
554 | source = "registry+https://github.com/rust-lang/crates.io-index"
555 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
556 |
557 | [[package]]
558 | name = "version_check"
559 | version = "0.1.5"
560 | source = "registry+https://github.com/rust-lang/crates.io-index"
561 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
562 |
563 | [[package]]
564 | name = "wasm-bindgen"
565 | version = "0.2.100"
566 | source = "registry+https://github.com/rust-lang/crates.io-index"
567 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
568 | dependencies = [
569 | "cfg-if",
570 | "once_cell",
571 | "rustversion",
572 | "wasm-bindgen-macro",
573 | ]
574 |
575 | [[package]]
576 | name = "wasm-bindgen-backend"
577 | version = "0.2.100"
578 | source = "registry+https://github.com/rust-lang/crates.io-index"
579 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
580 | dependencies = [
581 | "bumpalo",
582 | "log",
583 | "proc-macro2",
584 | "quote",
585 | "syn 2.0.87",
586 | "wasm-bindgen-shared",
587 | ]
588 |
589 | [[package]]
590 | name = "wasm-bindgen-macro"
591 | version = "0.2.100"
592 | source = "registry+https://github.com/rust-lang/crates.io-index"
593 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
594 | dependencies = [
595 | "quote",
596 | "wasm-bindgen-macro-support",
597 | ]
598 |
599 | [[package]]
600 | name = "wasm-bindgen-macro-support"
601 | version = "0.2.100"
602 | source = "registry+https://github.com/rust-lang/crates.io-index"
603 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
604 | dependencies = [
605 | "proc-macro2",
606 | "quote",
607 | "syn 2.0.87",
608 | "wasm-bindgen-backend",
609 | "wasm-bindgen-shared",
610 | ]
611 |
612 | [[package]]
613 | name = "wasm-bindgen-shared"
614 | version = "0.2.100"
615 | source = "registry+https://github.com/rust-lang/crates.io-index"
616 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
617 | dependencies = [
618 | "unicode-ident",
619 | ]
620 |
621 | [[package]]
622 | name = "web-sys"
623 | version = "0.3.77"
624 | source = "registry+https://github.com/rust-lang/crates.io-index"
625 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
626 | dependencies = [
627 | "js-sys",
628 | "wasm-bindgen",
629 | ]
630 |
631 | [[package]]
632 | name = "which"
633 | version = "4.4.2"
634 | source = "registry+https://github.com/rust-lang/crates.io-index"
635 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
636 | dependencies = [
637 | "either",
638 | "home",
639 | "once_cell",
640 | "rustix",
641 | ]
642 |
643 | [[package]]
644 | name = "widestring"
645 | version = "1.1.0"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
648 |
649 | [[package]]
650 | name = "windows-sys"
651 | version = "0.52.0"
652 | source = "registry+https://github.com/rust-lang/crates.io-index"
653 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
654 | dependencies = [
655 | "windows-targets",
656 | ]
657 |
658 | [[package]]
659 | name = "windows-targets"
660 | version = "0.52.6"
661 | source = "registry+https://github.com/rust-lang/crates.io-index"
662 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
663 | dependencies = [
664 | "windows_aarch64_gnullvm",
665 | "windows_aarch64_msvc",
666 | "windows_i686_gnu",
667 | "windows_i686_gnullvm",
668 | "windows_i686_msvc",
669 | "windows_x86_64_gnu",
670 | "windows_x86_64_gnullvm",
671 | "windows_x86_64_msvc",
672 | ]
673 |
674 | [[package]]
675 | name = "windows_aarch64_gnullvm"
676 | version = "0.52.6"
677 | source = "registry+https://github.com/rust-lang/crates.io-index"
678 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
679 |
680 | [[package]]
681 | name = "windows_aarch64_msvc"
682 | version = "0.52.6"
683 | source = "registry+https://github.com/rust-lang/crates.io-index"
684 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
685 |
686 | [[package]]
687 | name = "windows_i686_gnu"
688 | version = "0.52.6"
689 | source = "registry+https://github.com/rust-lang/crates.io-index"
690 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
691 |
692 | [[package]]
693 | name = "windows_i686_gnullvm"
694 | version = "0.52.6"
695 | source = "registry+https://github.com/rust-lang/crates.io-index"
696 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
697 |
698 | [[package]]
699 | name = "windows_i686_msvc"
700 | version = "0.52.6"
701 | source = "registry+https://github.com/rust-lang/crates.io-index"
702 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
703 |
704 | [[package]]
705 | name = "windows_x86_64_gnu"
706 | version = "0.52.6"
707 | source = "registry+https://github.com/rust-lang/crates.io-index"
708 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
709 |
710 | [[package]]
711 | name = "windows_x86_64_gnullvm"
712 | version = "0.52.6"
713 | source = "registry+https://github.com/rust-lang/crates.io-index"
714 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
715 |
716 | [[package]]
717 | name = "windows_x86_64_msvc"
718 | version = "0.52.6"
719 | source = "registry+https://github.com/rust-lang/crates.io-index"
720 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
721 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["core", "libretro", "profiling", "sdl-gui", "sfml-gui", "wasm-gui"]
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2022 Adam Soutar
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/NOTE.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | If you lock a Mac (CMD+CTRL+Q) while gbrs is running from `cargo run --release`,
4 | `cargo` will segfault.
5 |
6 | Dr. Mario locks up, seemingly looking for the GPU to enter OAMSearch status,
7 | but when it checks we're always reporting that we're in VBlank
8 |
9 | F-1 Race seems to run too slow - maybe the timers aren't right?
10 |
11 | Donkey Kong's audio is waaaaay too slow - again a timer issue?
12 |
13 | Space Invaders and Zelda seem to have the same issue where they can only make
14 | certain APU Channel 4 sounds once - Space Invaders only makes one shot fire
15 | noise, and Zelda only makes one sword slash noise. I think it might be because
16 | I haven't implemented _reading_ from APU channel addresses.
17 |
18 | ## Optimisation ideas
19 |
20 | In Memory::Step, which is 10% of runtime according to `cargo-flamegraph`, can
21 | probably have its loops unrolled slightly. Instead of running per-cycle, we
22 | could probably step timers and the like by doing addition rather than repeated
23 | increments. ✅
24 |
25 | MBCs probably do not need to be stepped per-cycle either. They can likely be
26 | stepped per frame, or _even per second_ or something, and still be fine.
27 | This step is only for save files and real-time clocks (not implemented).
28 | MBC::Step _may_ currently be slow due to MBCs being allocated on the heap and
29 | using indirection due to traits. - This turned out not to be very important.
30 | Even not stepping an MBC at all barely impacts performance.
31 |
32 | There may be optimisation to be found in the fact that, if we have a sprite
33 | pixel, there is no need to go and calculate a background pixel colour. - This
34 | isn't terribly useful as sprites don't cover tonnes of the screen.
35 |
36 | The screen buffer likely does not need to be fully copied every frame.
37 | Since we're not at all multi-threaded (yet?), frame data will not be modified
38 | during rendering. - This also doesn't seem to be much quicker.
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gbrs
2 |
3 | A Rust GameBoy emulator!
4 |
5 |
6 |
7 |
8 |
9 | Tetris | Zelda: Link's Awakening |
10 |
11 |
12 |  |
13 |  |
14 |
15 |
16 | Super Mario Land | Super Mario Land 2 |
17 |
18 |
19 |  |
20 |  |
21 |
22 |
23 | Galaga & Galaxian | Mortal Kombat |
24 |
25 |
26 |  |
27 |  |
28 |
29 |
30 | Pac-Man | Alleyway |
31 |
32 |
33 |  |
34 |  |
35 |
36 |
37 | Space Invaders | Road Rash |
38 |
39 |
40 |  |
41 |  |
42 |
43 |
44 | Donkey Kong | Frogger |
45 |
46 |
47 |  |
48 |  |
49 |
50 |
51 | dmg-acid2 |
52 |
53 |
54 |  |
55 |
56 |
57 |
58 | ## Support
59 |
60 | gbrs supports:
61 |
62 | - Mid-frame scanline effects (required for games like Road Rash)
63 | - The Window (a GPU feature required for Pac Man and Zelda)
64 | - Cycle-accurate CPU & counters
65 | - Save files & saved games (Zelda & Super Mario Land 2 use these)
66 | - The Window internal line counter (an unusual quirk required for perfect DMG-ACID2 rendering)
67 | - LCD Stat interrupt bug (a bug present on the real Gameboy hardware required for Road Rash)
68 | - Memory Board Controller 1 (MBCs are required for some more complex games)
69 | - Memory Board Controller 2
70 | - Memory Board Controller 3 (Real-time clock WIP)
71 | - Sound!
72 |
73 | & more!
74 |
75 | ## Progress so far
76 |
77 | I'm still working on gbrs (and having a **_tonne_** of fun doing it!).
78 |
79 | The main thing(s) I'm working on:
80 |
81 | - MBC3 RTC for real-world timekeeping in Pokemon
82 | - Laying the foundations for GameBoy Color support
83 | - Performance optimisations for bare-metal ports
84 |
85 | ## Building from source
86 |
87 | gbrs is not yet finished enough to distribute binaries, but if you want to try it out:
88 |
89 | The repo contains ports for multiple graphics backends. SDL is the easiest to build.
90 |
91 | ### SDL
92 |
93 | The SDL port comes with everything you need to compile & run in one
94 | command. If you experience issues with screen tearing or cracking
95 | sound, check out the SFML port instead.
96 |
97 | ```bash
98 | git clone https://github.com/adamsoutar/gbrs
99 | cd gbrs/sdl-gui
100 | cargo run --release ROM_PATH
101 | ```
102 |
103 | (Replace ROM_PATH with the path to a .gb file)
104 |
105 | ### SFML
106 |
107 | You'll need SFML set up, which you can find instructions for [here](https://github.com/jeremyletang/rust-sfml/wiki).
108 |
109 | Afterwards, in a terminal, you can execute these commands, assuming you have a
110 | [Rust](https://rustlang.org) toolchain installed.
111 |
112 | ```
113 | git clone https://github.com/adamsoutar/gbrs
114 | cd gbrs/sfml-gui
115 | cargo run --release ROM_PATH
116 | ```
117 |
118 | ## Ports to non-PC platforms
119 |
120 | gbrs is written to be ported to other platforms. Its default GUIs for Windows,
121 | macOS and Linux are just modules that it doesn't _have_ to use.
122 |
123 | You can port [gbrs-core](./core) to almost anything - especially since it
124 | supports running _without_ the Rust StdLib.
125 |
126 | All a port needs to do is:
127 |
128 | ```rust
129 | use gbrs_core::cpu::Cpu;
130 |
131 | let mut gameboy = Cpu::from_rom_bytes(
132 | include_bytes!("./tetris.gb").to_vec()
133 | );
134 |
135 | // Each frame:
136 | gameboy.step_one_frame();
137 | draw_screen(&gameboy.gpu.finished_frame);
138 | // (where draw_screen is a platform-specific function left to the reader)
139 | ```
140 |
141 | ---
142 |
143 | By Adam Soutar
144 |
--------------------------------------------------------------------------------
/assets/alleyway.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/alleyway.gif
--------------------------------------------------------------------------------
/assets/dmg-acid2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/dmg-acid2.jpg
--------------------------------------------------------------------------------
/assets/donkeykong.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/donkeykong.gif
--------------------------------------------------------------------------------
/assets/frogger.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/frogger.gif
--------------------------------------------------------------------------------
/assets/galaga.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/galaga.gif
--------------------------------------------------------------------------------
/assets/mario.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mario.gif
--------------------------------------------------------------------------------
/assets/mario2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mario2.gif
--------------------------------------------------------------------------------
/assets/mortalkombat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mortalkombat.gif
--------------------------------------------------------------------------------
/assets/pacman.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/pacman.gif
--------------------------------------------------------------------------------
/assets/roadrash.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/roadrash.gif
--------------------------------------------------------------------------------
/assets/spaceinvaders.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/spaceinvaders.gif
--------------------------------------------------------------------------------
/assets/tetris.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/tetris.gif
--------------------------------------------------------------------------------
/assets/zelda.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/zelda.gif
--------------------------------------------------------------------------------
/core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gbrs-core"
3 | version = "0.2.0"
4 | authors = ["Adam Soutar "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | smallvec = "1.15.0"
9 | spin = { version = "0.9.8", features = ["spin_mutex"] }
10 |
11 | [features]
12 | default = ["std", "sound"]
13 | std = []
14 | sound = []
15 |
--------------------------------------------------------------------------------
/core/src/alu.rs:
--------------------------------------------------------------------------------
1 | // CPU Arithmetic Logic Unit
2 | use crate::cpu::Cpu;
3 |
4 | const ALU_ADD: u8 = 0b000;
5 | const ALU_ADC: u8 = 0b001;
6 | const ALU_SUB: u8 = 0b010;
7 | const ALU_SBC: u8 = 0b011;
8 | const ALU_AND: u8 = 0b100;
9 | const ALU_XOR: u8 = 0b101;
10 | const ALU_OR: u8 = 0b110;
11 | const ALU_CP: u8 = 0b111;
12 |
13 | impl Cpu {
14 | pub fn alu(&mut self, operation: u8, n: u8) {
15 | let a = self.regs.a;
16 | let c = self.regs.get_carry_flag();
17 |
18 | match operation {
19 | ALU_ADD => {
20 | // ADD
21 | let res = a.wrapping_add(n);
22 | self.regs.set_carry_flag((a as u16 + n as u16 > 0xFF) as u8);
23 | self.regs.set_half_carry_flag(
24 | ((a & 0x0F) + (n & 0x0F) > 0x0F) as u8,
25 | );
26 | self.regs.set_zero_flag((res == 0) as u8);
27 | self.regs.set_operation_flag(0);
28 | self.regs.a = res;
29 | },
30 | ALU_ADC => {
31 | // ADC
32 | let res = a.wrapping_add(n).wrapping_add(c);
33 | self.regs.set_carry_flag(
34 | (a as u16 + n as u16 + c as u16 > 0xFF) as u8,
35 | );
36 | self.regs.set_half_carry_flag(
37 | ((a & 0x0F) + (n & 0x0F) + c > 0x0F) as u8,
38 | );
39 | self.regs.set_zero_flag((res == 0) as u8);
40 | self.regs.set_operation_flag(0);
41 | self.regs.a = res;
42 | },
43 | ALU_SUB => {
44 | // SUB
45 | let res = a.wrapping_sub(n);
46 | self.regs.set_carry_flag((a < n) as u8);
47 | self.regs
48 | .set_half_carry_flag(((a & 0x0F) < (n & 0x0F)) as u8);
49 | self.regs.set_operation_flag(1);
50 | self.regs.set_zero_flag((res == 0) as u8);
51 | self.regs.a = res;
52 | },
53 | ALU_SBC => {
54 | // SBC
55 | let res = a.wrapping_sub(n).wrapping_sub(c);
56 | self.regs
57 | .set_carry_flag(((a as u16) < (n as u16 + c as u16)) as u8);
58 | self.regs
59 | .set_half_carry_flag(((a & 0x0F) < (n & 0x0F) + c) as u8);
60 | self.regs.set_operation_flag(1);
61 | self.regs.set_zero_flag((res == 0) as u8);
62 | self.regs.a = res;
63 | },
64 | ALU_AND => {
65 | // AND
66 | let res = a & n;
67 | self.regs.set_carry_flag(0);
68 | self.regs.set_half_carry_flag(1);
69 | self.regs.set_operation_flag(0);
70 | self.regs.set_zero_flag((res == 0) as u8);
71 | self.regs.a = res;
72 | },
73 | ALU_XOR => {
74 | // XOR
75 | let res = a ^ n;
76 | self.regs.set_carry_flag(0);
77 | self.regs.set_half_carry_flag(0);
78 | self.regs.set_operation_flag(0);
79 | self.regs.set_zero_flag((res == 0) as u8);
80 | self.regs.a = res;
81 | },
82 | ALU_OR => {
83 | // OR
84 | let res = a | n;
85 | self.regs.set_carry_flag(0);
86 | self.regs.set_half_carry_flag(0);
87 | self.regs.set_operation_flag(0);
88 | self.regs.set_zero_flag((res == 0) as u8);
89 | self.regs.a = res;
90 | },
91 | ALU_CP => {
92 | // CP ("Compare")
93 | // It's a subtraction in terms of flags, but it throws away the result
94 | self.alu(ALU_SUB, n);
95 | self.regs.a = a;
96 | },
97 | _ => panic!("Unsupported ALU operation {:b}", operation),
98 | }
99 | }
100 |
101 | pub fn alu_dec(&mut self, n: u8) -> u8 {
102 | let r = n.wrapping_sub(1);
103 | self.regs
104 | .set_half_carry_flag((n.trailing_zeros() >= 4) as u8);
105 | self.regs.set_operation_flag(1);
106 | self.regs.set_zero_flag((r == 0) as u8);
107 | r
108 | }
109 | pub fn alu_inc(&mut self, n: u8) -> u8 {
110 | let r = n.wrapping_add(1);
111 | self.regs
112 | .set_half_carry_flag(((n & 0x0f) + 0x01 > 0x0f) as u8);
113 | self.regs.set_operation_flag(0);
114 | self.regs.set_zero_flag((r == 0) as u8);
115 | r
116 | }
117 | pub fn alu_add_hl(&mut self, n: u16) {
118 | let hl = self.regs.get_hl();
119 | let r = hl.wrapping_add(n);
120 |
121 | self.regs.set_carry_flag((hl > 0xffff - n) as u8);
122 | self.regs
123 | .set_half_carry_flag(((hl & 0x0fff) + (n & 0x0fff) > 0x0fff) as u8);
124 | self.regs.set_operation_flag(0);
125 |
126 | self.regs.set_hl(r);
127 | }
128 |
129 | // R for "Rotate" (Bitshift)
130 | fn alu_rlc(&mut self, n: u8) -> u8 {
131 | let c = (n & 0b10000000) >> 7;
132 | let r = (n << 1) | c;
133 | self.regs.set_carry_flag(c);
134 | self.regs.set_operation_flag(0);
135 | self.regs.set_half_carry_flag(0);
136 | self.regs.set_zero_flag((r == 0) as u8);
137 | r
138 | }
139 | fn alu_rl(&mut self, n: u8) -> u8 {
140 | let c = (n & 0b10000000) >> 7;
141 | let r = (n << 1) | self.regs.get_carry_flag();
142 | self.regs.set_carry_flag(c);
143 | self.regs.set_operation_flag(0);
144 | self.regs.set_half_carry_flag(0);
145 | self.regs.set_zero_flag((r == 0) as u8);
146 | r
147 | }
148 | fn alu_rrc(&mut self, n: u8) -> u8 {
149 | let c = n & 1;
150 | let r = (n >> 1) | (c << 7);
151 | self.regs.set_carry_flag(c);
152 | self.regs.set_operation_flag(0);
153 | self.regs.set_half_carry_flag(0);
154 | self.regs.set_zero_flag((r == 0) as u8);
155 | r
156 | }
157 | fn alu_rr(&mut self, n: u8) -> u8 {
158 | let c = n & 1;
159 | let r = (n >> 1) | (self.regs.get_carry_flag() << 7);
160 | self.regs.set_carry_flag(c);
161 | self.regs.set_half_carry_flag(0);
162 | self.regs.set_operation_flag(0);
163 | self.regs.set_zero_flag((r == 0) as u8);
164 | r
165 | }
166 |
167 | fn alu_sla(&mut self, n: u8) -> u8 {
168 | let c = (n & 0x80) >> 7;
169 | let r = n << 1;
170 | self.regs.set_carry_flag(c);
171 | self.regs.set_half_carry_flag(0);
172 | self.regs.set_operation_flag(0);
173 | self.regs.set_zero_flag((r == 0) as u8);
174 | r
175 | }
176 | fn alu_sra(&mut self, n: u8) -> u8 {
177 | let c = n & 1;
178 | let r = (n >> 1) | (n & 0x80);
179 | self.regs.set_carry_flag(c);
180 | self.regs.set_half_carry_flag(0);
181 | self.regs.set_operation_flag(0);
182 | self.regs.set_zero_flag((r == 0) as u8);
183 | r
184 | }
185 | pub fn alu_special_rotate(&mut self, right: bool, n: u8) -> u8 {
186 | if right {
187 | self.alu_sra(n)
188 | } else {
189 | self.alu_sla(n)
190 | }
191 | }
192 |
193 | pub fn alu_srl(&mut self, n: u8) -> u8 {
194 | let c = n & 1;
195 | let r = n >> 1;
196 | self.regs.set_carry_flag(c);
197 | self.regs.set_half_carry_flag(0);
198 | self.regs.set_operation_flag(0);
199 | self.regs.set_zero_flag((r == 0) as u8);
200 | r
201 | }
202 |
203 | pub fn alu_rotate_val(&mut self, right: bool, carry: bool, a: u8) -> u8 {
204 | if !right {
205 | if carry {
206 | self.alu_rlc(a)
207 | } else {
208 | self.alu_rl(a)
209 | }
210 | } else {
211 | if carry {
212 | self.alu_rrc(a)
213 | } else {
214 | self.alu_rr(a)
215 | }
216 | }
217 | }
218 | pub fn alu_rotate(&mut self, right: bool, carry: bool) {
219 | let a = self.regs.a;
220 | self.regs.a = self.alu_rotate_val(right, carry, a);
221 | }
222 |
223 | // DAA is proper weird. For this one, I had to look at:
224 | // https://github.com/mohanson/gameboy/blob/master/src/cpu.rs#L325
225 | pub fn alu_daa(&mut self) {
226 | let mut a = self.regs.a;
227 |
228 | let mut adjust = if self.regs.get_carry_flag() == 1 {
229 | 0x60
230 | } else {
231 | 0
232 | };
233 |
234 | if self.regs.get_half_carry_flag() == 1 {
235 | adjust |= 0x06;
236 | }
237 |
238 | if self.regs.get_operation_flag() == 0 {
239 | if a & 0x0f > 0x09 {
240 | adjust |= 0x06;
241 | };
242 | if a > 0x99 {
243 | adjust |= 0x60;
244 | };
245 | a = a.wrapping_add(adjust);
246 | } else {
247 | a = a.wrapping_sub(adjust);
248 | }
249 |
250 | self.regs.set_carry_flag((adjust >= 0x60) as u8);
251 | self.regs.set_half_carry_flag(0);
252 | self.regs.set_zero_flag((a == 0) as u8);
253 |
254 | self.regs.a = a;
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/core/src/callbacks.rs:
--------------------------------------------------------------------------------
1 | // This allows ports to register functions for things like logging as well as
2 | // saving/loading battery-backed RAM.
3 |
4 | #[cfg(not(feature = "std"))]
5 | use alloc::{vec, vec::Vec};
6 | use spin::mutex::spin::SpinMutex;
7 | #[cfg(feature = "std")]
8 | use std::{fs, io::Read, path::PathBuf};
9 |
10 | pub type LogCallback = fn(log_str: &str);
11 | pub type SaveCallback =
12 | fn(game_name: &str, rom_path: &str, save_data: &Vec);
13 | pub type LoadCallback =
14 | fn(game_name: &str, rom_path: &str, expected_size: usize) -> Vec;
15 |
16 | #[derive(Clone)]
17 | pub struct Callbacks {
18 | pub log: LogCallback,
19 | pub save: SaveCallback,
20 | pub load: LoadCallback,
21 | }
22 |
23 | #[cfg(feature = "std")]
24 | fn get_save_file_path(rom_path: &str) -> String {
25 | let mut sav_path = PathBuf::from(rom_path);
26 | sav_path.set_extension("sav");
27 |
28 | sav_path.to_string_lossy().to_string()
29 | }
30 |
31 | #[cfg(feature = "std")]
32 | pub static CALLBACKS: SpinMutex = SpinMutex::new(Callbacks {
33 | log: |log_str| println!("{}", log_str),
34 | save: |_game_name, rom_path, save_data| {
35 | let save_path = get_save_file_path(rom_path);
36 | fs::write(&save_path, save_data).expect("Failed to write save file");
37 | },
38 | load: |_game_name, rom_path, expected_size| {
39 | let save_path = get_save_file_path(rom_path);
40 | let mut buffer = vec![];
41 | let file_result = fs::File::open(save_path);
42 |
43 | if let Ok(mut file) = file_result {
44 | file.read_to_end(&mut buffer)
45 | .expect("Unable to read save file");
46 | buffer
47 | } else {
48 | // The save file likely does not exist
49 | vec![0; expected_size]
50 | }
51 | },
52 | });
53 |
54 | #[cfg(not(feature = "std"))]
55 | pub static CALLBACKS: SpinMutex = SpinMutex::new(Callbacks {
56 | log: |_log_str| {},
57 | save: |_game_name, _rom_path, _save_data| {},
58 | load: |_game_name, _rom_path, expected_size| vec![0; expected_size],
59 | });
60 |
61 | pub fn set_callbacks(cbs: Callbacks) {
62 | *CALLBACKS.lock() = cbs;
63 | }
64 |
--------------------------------------------------------------------------------
/core/src/cartridge.rs:
--------------------------------------------------------------------------------
1 | // Parses the cartridge header
2 | use crate::log;
3 |
4 | #[cfg(not(feature = "std"))]
5 | use alloc::{string::String, vec, vec::Vec};
6 |
7 | #[derive(Clone)]
8 | pub enum CGBSupportType {
9 | None,
10 | Optional,
11 | Required,
12 | }
13 |
14 | #[derive(Clone)]
15 | pub struct Cartridge {
16 | pub title: String,
17 | pub rom_path: String,
18 | pub cart_type: u8,
19 |
20 | pub rom_size: usize,
21 | pub ram_size: usize,
22 |
23 | pub cgb_support: CGBSupportType,
24 | }
25 |
26 | impl Cartridge {
27 | pub fn parse(buffer: &Vec, rom_path: String) -> Cartridge {
28 | let title = get_title(buffer);
29 |
30 | let cart_type = buffer[0x0147];
31 |
32 | let rom_size_id = buffer[0x0148];
33 | let ram_size_id = buffer[0x0149];
34 |
35 | let rom_size = 32768 << (rom_size_id as usize);
36 | let ram_size = match ram_size_id {
37 | 0 => 0,
38 | 1 => {
39 | log!("[WARN] Unofficial 2KB RAM size not used by any officially published game.");
40 | 2_048
41 | },
42 | 2 => 8_192,
43 | 3 => 32_768,
44 | 4 => {
45 | log!("[WARN] RAM size is larger than a u16. Internal implementations such as BatteryBackedRam may fail.");
46 | 131_072
47 | },
48 | 5 => 65_536,
49 | _ => {
50 | panic!("Unknown RAM size id for cartridge {:#04x}", ram_size_id)
51 | },
52 | };
53 |
54 | let cgb_support = match buffer[0x0143] {
55 | 0x80 => CGBSupportType::Optional,
56 | 0xC0 => CGBSupportType::Required,
57 | _ => CGBSupportType::None,
58 | };
59 |
60 | Cartridge {
61 | title,
62 | rom_path,
63 | cart_type,
64 | rom_size,
65 | ram_size,
66 | cgb_support,
67 | }
68 | }
69 | }
70 |
71 | fn get_title(buffer: &Vec) -> String {
72 | let mut out_buff = vec![];
73 | for i in 0x0134..=0x0143 {
74 | // A null byte terminates the title string
75 | // Also, later games have non-ascii values in their titles used for
76 | // flags like GameBoy Color support.
77 | if buffer[i] == 0 || buffer[i] > 0x7F {
78 | break;
79 | }
80 | out_buff.push(buffer[i]);
81 | }
82 | String::from_utf8(out_buff).expect("ROM title isn't valid UTF-8")
83 | }
84 |
--------------------------------------------------------------------------------
/core/src/cgb_dma.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, PartialEq)]
2 | pub enum CgbDmaType {
3 | GeneralPurpose,
4 | HBlank,
5 | }
6 |
7 | pub struct CgbDmaConfig {
8 | pub source: u16,
9 | pub dest: u16,
10 | pub dma_type: CgbDmaType,
11 | pub bytes_copied: u16,
12 | pub bytes_left: u16,
13 | pub transfer_done: bool,
14 | }
15 |
16 | impl CgbDmaConfig {
17 | pub fn set_config_byte(&mut self, value: u8) {
18 | self.transfer_done = false;
19 | self.dma_type = if value & 0x80 == 0x80 {
20 | CgbDmaType::HBlank
21 | } else {
22 | CgbDmaType::GeneralPurpose
23 | };
24 | self.bytes_left = ((value & 0x7F) + 1) as u16 * 0x10;
25 | self.bytes_copied = 0;
26 | }
27 | pub fn get_config_byte(&self) -> u8 {
28 | if self.transfer_done {
29 | return 0xFF;
30 | }
31 | // TODO: Not sure this is quite the correct calculation
32 | ((self.bytes_left / 0x10) - 1) as u8
33 | }
34 |
35 | pub fn is_hblank_dma(&self) -> bool {
36 | self.dma_type == CgbDmaType::HBlank
37 | }
38 |
39 | pub fn get_source_upper(&self) -> u8 {
40 | (self.source >> 8) as u8
41 | }
42 | pub fn get_source_lower(&self) -> u8 {
43 | (self.source & 0xFF) as u8
44 | }
45 | pub fn set_source_upper(&mut self, value: u8) {
46 | self.source = (self.source & 0x00FF) | ((value as u16) << 8);
47 | }
48 | pub fn set_source_lower(&mut self, value: u8) {
49 | // Lower 4 bits of address are ignored
50 | self.source = (self.source & 0xFF00) | ((value & 0xF0) as u16);
51 | }
52 |
53 | pub fn get_dest_upper(&self) -> u8 {
54 | (self.dest >> 8) as u8
55 | }
56 | pub fn get_dest_lower(&self) -> u8 {
57 | (self.dest & 0xFF) as u8
58 | }
59 | pub fn set_dest_upper(&mut self, value: u8) {
60 | // This algo makes sure that the destination address is in the range
61 | // 0x8000 - 0x9FFF, ensuring that the destianation is in VRAM.
62 | self.dest =
63 | // Keep lower byte
64 | (self.dest & 0x00FF) |
65 | // Set upper byte
66 | (((
67 | // Ignore upper 3 bits of value, making range 0x0000 - 0x1FFF
68 | (value & 0x1F)
69 | // Set the top bit, adding 0x8000
70 | | 0x80) as u16
71 | ) << 8);
72 | }
73 | pub fn set_dest_lower(&mut self, value: u8) {
74 | // Lower 4 bits of address are ignored
75 | self.dest = (self.dest & 0xFF00) | ((value & 0xF0) as u16);
76 | }
77 |
78 | pub fn new() -> CgbDmaConfig {
79 | CgbDmaConfig {
80 | source: 0,
81 | dest: 0,
82 | dma_type: CgbDmaType::GeneralPurpose,
83 | bytes_left: 0,
84 | bytes_copied: 0,
85 | transfer_done: false,
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/core/src/colour/bg_map_attributes.rs:
--------------------------------------------------------------------------------
1 | // Data pertaining to rendering coloured background/window tiles
2 | // Defined by writing to VRAM bank 1 0x9800 to 0x9FFF
3 |
4 | const BG_MAP_ATTRIBUTE_TABLE_SIZE: usize = 0x800; // 0x9FFF - 0x9800 + 0th addr
5 |
6 | #[derive(Clone, Copy)]
7 | pub struct BgMapAttributeEntry {
8 | pub priority: bool,
9 | pub y_flip: bool,
10 | pub x_flip: bool,
11 | // This is an unused bit, but the hardware keeps track of it
12 | // Games could use it for their own unusual hackery
13 | pub bit_four: bool,
14 | pub vram_bank: u8, // Either 0 or 1
15 | pub palette: u8,
16 | }
17 |
18 | impl BgMapAttributeEntry {
19 | pub fn as_u8(&self) -> u8 {
20 | let mut val = 0;
21 | if self.priority {
22 | val |= 0b1000_0000;
23 | }
24 | if self.y_flip {
25 | val |= 0b0100_0000;
26 | }
27 | if self.x_flip {
28 | val |= 0b0010_0000;
29 | }
30 | if self.bit_four {
31 | val |= 0b0001_0000;
32 | }
33 | val |= self.vram_bank << 3;
34 | val |= self.palette & 0b0000_0111;
35 | val
36 | }
37 |
38 | pub fn from_u8(val: u8) -> BgMapAttributeEntry {
39 | BgMapAttributeEntry {
40 | priority: (0b1000_0000 & val) > 0,
41 | y_flip: (0b0100_0000 & val) > 0,
42 | x_flip: (0b0010_0000 & val) > 0,
43 | bit_four: (0b0001_0000 & val) > 0,
44 | vram_bank: (0b0000_1000 & val) >> 3,
45 | palette: 0b0000_0111 & val,
46 | }
47 | }
48 |
49 | pub fn new() -> BgMapAttributeEntry {
50 | BgMapAttributeEntry {
51 | priority: false,
52 | y_flip: false,
53 | x_flip: false,
54 | bit_four: false,
55 | vram_bank: 0,
56 | palette: 0,
57 | }
58 | }
59 | }
60 |
61 | pub struct BgMapAttributeTable {
62 | entries: [BgMapAttributeEntry; BG_MAP_ATTRIBUTE_TABLE_SIZE],
63 | }
64 |
65 | impl BgMapAttributeTable {
66 | pub fn get_entry(&self, address: u16) -> BgMapAttributeEntry {
67 | self.entries[address as usize]
68 | }
69 |
70 | pub fn read(&self, address: u16) -> u8 {
71 | self.get_entry(address).as_u8()
72 | }
73 |
74 | pub fn write(&mut self, address: u16, value: u8) {
75 | self.entries[address as usize] = BgMapAttributeEntry::from_u8(value);
76 | }
77 |
78 | pub fn new() -> BgMapAttributeTable {
79 | BgMapAttributeTable {
80 | entries: [BgMapAttributeEntry::new(); BG_MAP_ATTRIBUTE_TABLE_SIZE],
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/core/src/colour/colour.rs:
--------------------------------------------------------------------------------
1 | #[derive(Clone, Copy)]
2 | pub struct Colour {
3 | pub red: u8,
4 | pub green: u8,
5 | pub blue: u8,
6 | }
7 |
8 | impl Colour {
9 | // Colour space conversion algo from
10 | // https://gamedev.stackexchange.com/a/196834
11 | pub fn from_16_bit_colour(val: u16) -> Colour {
12 | let mut red = ((val % 32) * 8) as u8;
13 | red = red + red / 32;
14 | let mut green = (((val / 32) % 32) * 8) as u8;
15 | green = green + green / 32;
16 | let mut blue = (((val / 1024) % 32) * 8) as u8;
17 | blue = blue + blue / 32;
18 | Colour { red, green, blue }
19 | }
20 |
21 | pub fn new(red: u8, green: u8, blue: u8) -> Colour {
22 | Colour { red, green, blue }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/colour/grey_shades.rs:
--------------------------------------------------------------------------------
1 | use super::colour::Colour;
2 |
3 | pub fn white() -> Colour {
4 | Colour::new(0xDD, 0xDD, 0xDD)
5 | }
6 | pub fn light_grey() -> Colour {
7 | Colour::new(0xAA, 0xAA, 0xAA)
8 | }
9 | pub fn dark_grey() -> Colour {
10 | Colour::new(0x88, 0x88, 0x88)
11 | }
12 | pub fn black() -> Colour {
13 | Colour::new(0x55, 0x55, 0x55)
14 | }
15 |
16 | pub fn colour_from_grey_shade_id(id: u8) -> Colour {
17 | match id {
18 | 0 => white(),
19 | 1 => light_grey(),
20 | 2 => dark_grey(),
21 | 3 => black(),
22 | _ => panic!("Invalid grey shade id {}", id),
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/colour/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod bg_map_attributes;
2 | pub mod colour;
3 | pub mod grey_shades;
4 | pub mod palette_ram;
5 |
--------------------------------------------------------------------------------
/core/src/colour/palette_ram.rs:
--------------------------------------------------------------------------------
1 | use super::colour::Colour;
2 | use crate::{combine_u8, cpu::EmulationTarget, memory::ram::Ram};
3 |
4 | fn palette_spec_read(address: u16, auto_increment: bool) -> u8 {
5 | // This should never be higher than 64 anyway, but let's be safe
6 | let lower_address = (address & 0b0001_1111) as u8;
7 | let auto_inc_bit = if auto_increment { 1 } else { 0 };
8 | lower_address | (auto_inc_bit << 7)
9 | }
10 |
11 | fn palette_spec_write(address: &mut u16, value: u8, auto_increment: &mut bool) {
12 | *auto_increment = (value & 0b1000_0000) > 0;
13 | *address = (value & 0b0001_1111) as u16;
14 | }
15 |
16 | fn palette_data_write(
17 | ram: &mut Ram,
18 | address: &mut u16,
19 | value: u8,
20 | auto_increment: bool,
21 | ) {
22 | ram.write(*address, value);
23 | if auto_increment {
24 | *address = (*address + 1) % 64;
25 | }
26 | }
27 |
28 | pub struct PaletteRam {
29 | // If this is false, we're a DMG.
30 | cgb_features: bool,
31 | bg_palette_ram: Ram,
32 | bg_address: u16,
33 | bg_auto_increment: bool,
34 | obj_palette_ram: Ram,
35 | obj_address: u16,
36 | obj_auto_increment: bool,
37 | }
38 |
39 | impl PaletteRam {
40 | fn read_colour(&self, ram: &Ram, address: u16) -> Colour {
41 | let col0 = ram.read(address);
42 | let col1 = ram.read(address + 1);
43 | Colour::from_16_bit_colour(combine_u8!(col1, col0))
44 | }
45 |
46 | pub fn get_bg_palette_colour(
47 | &self,
48 | palette_id: u16,
49 | colour_id: u16,
50 | ) -> Colour {
51 | let base_offset = 8 * palette_id;
52 | self.read_colour(&self.bg_palette_ram, base_offset + colour_id * 2)
53 | }
54 |
55 | pub fn get_obj_palette_colour(
56 | &self,
57 | palette_id: u16,
58 | colour_id: u16,
59 | ) -> Colour {
60 | let base_offset = 8 * palette_id;
61 | self.read_colour(&self.obj_palette_ram, base_offset + colour_id * 2)
62 | }
63 |
64 | pub fn raw_read(&self, address: u16) -> u8 {
65 | if !self.cgb_features {
66 | return 0xFF;
67 | }
68 |
69 | match address {
70 | 0xFF68 => {
71 | palette_spec_read(self.bg_address, self.bg_auto_increment)
72 | },
73 | 0xFF69 => self.bg_palette_ram.read(self.bg_address),
74 |
75 | 0xFF6A => {
76 | palette_spec_read(self.obj_address, self.obj_auto_increment)
77 | },
78 | 0xFF6B => self.obj_palette_ram.read(self.obj_address),
79 |
80 | _ => panic!("CGB Palette RAM read at {:#06x}", address),
81 | }
82 | }
83 |
84 | pub fn raw_write(&mut self, address: u16, value: u8) {
85 | if !self.cgb_features {
86 | return;
87 | }
88 |
89 | match address {
90 | 0xFF68 => palette_spec_write(
91 | &mut self.bg_address,
92 | value,
93 | &mut self.bg_auto_increment,
94 | ),
95 | 0xFF69 => palette_data_write(
96 | &mut self.bg_palette_ram,
97 | &mut self.bg_address,
98 | value,
99 | self.bg_auto_increment,
100 | ),
101 |
102 | 0xFF6A => palette_spec_write(
103 | &mut self.obj_address,
104 | value,
105 | &mut self.obj_auto_increment,
106 | ),
107 | 0xFF6B => palette_data_write(
108 | &mut self.obj_palette_ram,
109 | &mut self.obj_address,
110 | value,
111 | self.obj_auto_increment,
112 | ),
113 |
114 | _ => panic!(
115 | "CGB Palette RAM write at {:#06x} (value: {:#04x})",
116 | address, value
117 | ),
118 | }
119 | }
120 |
121 | pub fn new(target: &EmulationTarget) -> PaletteRam {
122 | PaletteRam {
123 | cgb_features: target.has_cgb_features(),
124 | // All background colours are white at boot
125 | bg_palette_ram: Ram::with_filled_value(64, 0xFF),
126 | bg_address: 0,
127 | bg_auto_increment: false,
128 | // Object memory is garbage on boot, but slot 0 is always 0
129 | obj_palette_ram: Ram::new(64),
130 | obj_address: 0,
131 | obj_auto_increment: false,
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/core/src/config.rs:
--------------------------------------------------------------------------------
1 | // Config for creating CPUs
2 | // This helps with ports
3 | use crate::memory::rom::Rom;
4 |
5 | #[derive(Clone)]
6 | pub struct Config {
7 | pub sound_buffer_size: usize,
8 | pub sound_sample_rate: usize,
9 | pub rom: Rom,
10 | }
11 |
--------------------------------------------------------------------------------
/core/src/constants.rs:
--------------------------------------------------------------------------------
1 | // "WRAM" is Work RAM, not Wave RAM
2 | pub const WRAM_BANK_SIZE: usize = 4096;
3 | pub const VRAM_BANK_SIZE: usize = 8192;
4 | pub const HRAM_SIZE: usize = 127;
5 | pub const OAM_SIZE: usize = 160;
6 | pub const WAVE_RAM_SIZE: usize = 16;
7 |
8 | // Excluding invisible areas such as those above and to
9 | // the left of the screen
10 | pub const SCREEN_WIDTH: usize = 160;
11 | pub const SCREEN_HEIGHT: usize = 144;
12 |
13 | pub const SCREEN_BUFFER_SIZE: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
14 | pub const SCREEN_RGBA_SLICE_SIZE: usize = SCREEN_BUFFER_SIZE * 4;
15 |
16 | pub const CLOCK_SPEED: usize = 4194304;
17 | pub const DEFAULT_FRAME_RATE: usize = 60;
18 |
19 | // The amount of sound samples we collect before firing them off for
20 | // playback. This number is essentially guessed.
21 | pub const SOUND_BUFFER_SIZE: usize = 2048;
22 | pub const SOUND_SAMPLE_RATE: usize = 48000;
23 | // The amount of APU step()s we should run before
24 | // we sample for audio.
25 | pub const APU_SAMPLE_CLOCKS: usize = CLOCK_SPEED / SOUND_SAMPLE_RATE;
26 |
27 | // MBC_ROM_START is 0
28 | pub const MBC_ROM_END: u16 = 0x7FFF;
29 |
30 | pub const MBC_RAM_START: u16 = 0xA000;
31 | pub const MBC_RAM_END: u16 = 0xBFFF;
32 |
33 | pub const VRAM_START: u16 = 0x8000;
34 | // For CGB BG Map Attribute Table
35 | pub const VRAM_BG_MAP_START: u16 = 0x9800;
36 | pub const VRAM_END: u16 = 0x9FFF;
37 |
38 | pub const WRAM_LOWER_BANK_START: u16 = 0xC000;
39 | pub const WRAM_LOWER_BANK_END: u16 = 0xCFFF;
40 | pub const WRAM_UPPER_BANK_START: u16 = 0xD000;
41 | pub const WRAM_UPPER_BANK_END: u16 = 0xDFFF;
42 |
43 | pub const ECHO_RAM_START: u16 = 0xE000;
44 | pub const ECHO_RAM_END: u16 = 0xFDFF;
45 |
46 | pub const OAM_START: u16 = 0xFE00;
47 | pub const OAM_END: u16 = 0xFE9F;
48 |
49 | pub const UNUSABLE_MEMORY_START: u16 = 0xFEA0;
50 | pub const UNUSABLE_MEMORY_END: u16 = 0xFEFF;
51 |
52 | pub const LINK_CABLE_SB: u16 = 0xFF01;
53 | pub const LINK_CABLE_SC: u16 = 0xFF02;
54 |
55 | pub const APU_START: u16 = 0xFF10;
56 | pub const APU_END: u16 = 0xFF3F;
57 |
58 | pub const WAVE_RAM_START: u16 = 0xFF30;
59 | pub const WAVE_RAM_END: u16 = 0xFF3F;
60 |
61 | pub const HRAM_START: u16 = 0xFF80;
62 | pub const HRAM_END: u16 = 0xFFFE;
63 |
64 | pub const LCD_DATA_START: u16 = 0xFF40;
65 | pub const LCD_DATA_END: u16 = 0xFF4C;
66 |
67 | pub const CGB_DMA_START: u16 = 0xFF51;
68 | pub const CGB_DMA_END: u16 = 0xFF55;
69 |
70 | pub const CGB_PALETTE_DATA_START: u16 = 0xFF68;
71 | pub const CGB_PALETTE_DATA_END: u16 = 0xFF6B;
72 |
73 | pub const INTERRUPT_ENABLE_ADDRESS: u16 = 0xFFFF;
74 | pub const INTERRUPT_FLAG_ADDRESS: u16 = 0xFF0F;
75 |
76 | pub const HALT_INSTRUCTION_OPCODE: u8 = 0x76;
77 | pub const SPEED_SWITCH_HALT_CYCLES: usize = 8200;
78 |
79 | pub mod gpu_timing {
80 | // Total line size incl. HBlank
81 | pub const HTOTAL: u16 = 456;
82 |
83 | // lx coordinate where Transfer begins
84 | pub const HTRANSFER_ON: u16 = 80;
85 |
86 | // Start of HBlank
87 | pub const HBLANK_ON: u16 = 252;
88 |
89 | // Total vertical lines incl. VBlank
90 | pub const VTOTAL: u8 = 154;
91 | // Start of VBlank
92 | pub const VBLANK_ON: u8 = 144;
93 |
94 | // Number of CPU cycles it takes to do a DMA
95 | pub const DMA_CYCLES: u8 = 160;
96 | }
97 |
--------------------------------------------------------------------------------
/core/src/gpu.rs:
--------------------------------------------------------------------------------
1 | use crate::cgb_dma::CgbDmaConfig;
2 | use crate::colour::colour::Colour;
3 | use crate::colour::grey_shades;
4 | use crate::colour::grey_shades::colour_from_grey_shade_id;
5 | use crate::combine_u8;
6 | use crate::constants::*;
7 | use crate::interrupts::*;
8 | use crate::lcd::*;
9 | use crate::log;
10 | use crate::memory::memory::Memory;
11 | use crate::memory::ram::Ram;
12 |
13 | use smallvec::SmallVec;
14 |
15 | #[derive(Clone)]
16 | pub struct Sprite {
17 | pub y_pos: i32,
18 | pub x_pos: i32,
19 | pub pattern_id: u8,
20 |
21 | pub above_bg: bool,
22 | pub y_flip: bool,
23 | pub x_flip: bool,
24 | pub use_palette_0: bool,
25 |
26 | // CGB-specific attributes
27 | pub use_upper_vram_bank: bool,
28 | pub cgb_palette: u8,
29 | }
30 |
31 | pub struct Gpu {
32 | cgb_features: bool,
33 | // This is the WIP frame that the GPU draws to
34 | frame: [Colour; SCREEN_BUFFER_SIZE],
35 | // This is the last rendered frame displayed on the LCD, only updated
36 | // in VBlank. GUI implementations can read it to show the display.
37 | pub finished_frame: [Colour; SCREEN_BUFFER_SIZE],
38 |
39 | // X and Y of background position
40 | scy: u8,
41 | scx: u8,
42 |
43 | // X and Y of the Window
44 | wy: u8,
45 | wx: u8,
46 |
47 | // The scan-line Y co-ordinate
48 | ly: u8,
49 | // If ly is lyc ("compare") and the interrupt is enabled,
50 | // an LCD Status interrupt is flagged
51 | lyc: u8,
52 | // The "Window internal line counter" - relied upon by a handful of
53 | // unusual games and DMG-ACID2.
54 | window_line_counter: u8,
55 |
56 | // Scan-line X co-ordinate
57 | // This isn't a real readable Gameboy address, it's just for internal tracking
58 | lx: u16,
59 |
60 | bg_pallette: u8,
61 | sprite_pallete_1: u8,
62 | sprite_pallete_2: u8,
63 |
64 | status: LcdStatus,
65 | control: LcdControl,
66 |
67 | // "Object Attribute Memory" - Sprite properties
68 | oam: Ram,
69 |
70 | dma_source: u8,
71 | dma_cycles: u8,
72 |
73 | cgb_dma: CgbDmaConfig,
74 |
75 | // The global 40-sprite OAM cache
76 | // SmallVec doesn't do blocks of 40 so we leave 24 empty slots, it's still
77 | // more performant than allocating.
78 | sprite_cache: SmallVec<[Sprite; 64]>,
79 | // The per-scanline 10-sprite cache
80 | // TODO: These come straight from sprite_cache. Maybe they can be &Sprite?
81 | // Would that be faster?
82 | sprites_on_line: SmallVec<[Sprite; 10]>,
83 | }
84 |
85 | impl Gpu {
86 | // Function complexity warning here is due to the massive switch statement.
87 | // Such a thing is expected in an emulator.
88 | // skipcq: RS-R1000
89 | pub fn raw_write(
90 | &mut self,
91 | raw_address: u16,
92 | value: u8,
93 | ints: &mut Interrupts,
94 | ) {
95 | match raw_address {
96 | OAM_START..=OAM_END => {
97 | self.oam.write(raw_address - OAM_START, value)
98 | },
99 |
100 | 0xFF40 => {
101 | let original_display_enable = self.control.display_enable;
102 | self.control = LcdControl::from(value);
103 |
104 | if original_display_enable && !self.control.display_enable {
105 | // The LCD has just been turned off
106 | self.ly = 0;
107 | self.status.set_mode(LcdMode::HBlank);
108 | if self.status.hblank_interrupt {
109 | ints.raise_interrupt(InterruptReason::LCDStat)
110 | }
111 | self.lyc = 0;
112 | }
113 | if !original_display_enable && self.control.display_enable {
114 | // The LCD has just been turned on
115 | self.status.set_mode(LcdMode::OAMSearch);
116 | if self.status.oam_interrupt {
117 | ints.raise_interrupt(InterruptReason::LCDStat);
118 | }
119 | self.cache_all_sprites();
120 | }
121 | },
122 | 0xFF41 => self.status.set_data(value, ints),
123 | 0xFF42 => self.scy = value,
124 | 0xFF43 => self.scx = value,
125 | // The Y Scanline is read only.
126 | // Space Invaders writes here. As a bug?
127 | 0xFF44 => {},
128 | 0xFF45 => self.lyc = value,
129 |
130 | 0xFF46 => self.begin_dma(value),
131 |
132 | 0xFF47 => self.bg_pallette = value,
133 | 0xFF48 => self.sprite_pallete_1 = value,
134 | 0xFF49 => self.sprite_pallete_2 = value,
135 |
136 | 0xFF4A => self.wy = value,
137 | 0xFF4B => self.wx = value,
138 |
139 | 0xFF4C => log!(
140 | "[WARN] Unknown LCD register write at {:#06x} (value: {:#04x})",
141 | raw_address,
142 | value
143 | ),
144 |
145 | 0xFF51 => self.cgb_dma.set_source_upper(value),
146 | 0xFF52 => self.cgb_dma.set_source_lower(value),
147 | 0xFF53 => self.cgb_dma.set_dest_upper(value),
148 | 0xFF54 => self.cgb_dma.set_dest_lower(value),
149 | 0xFF55 => self.cgb_dma.set_config_byte(value),
150 |
151 | _ => panic!(
152 | "Unsupported GPU write at {:#06x} (value: {:#04x})",
153 | raw_address, value
154 | ),
155 | }
156 | }
157 | pub fn raw_read(&self, raw_address: u16) -> u8 {
158 | match raw_address {
159 | OAM_START..=OAM_END => self.oam.read(raw_address - OAM_START),
160 |
161 | 0xFF40 => u8::from(self.control),
162 | 0xFF41 => u8::from(self.status),
163 | 0xFF42 => self.scy,
164 | 0xFF43 => self.scx,
165 | 0xFF44 => self.ly,
166 | 0xFF45 => self.lyc,
167 |
168 | 0xFF46 => self.dma_source,
169 |
170 | 0xFF4A => self.wy,
171 | 0xFF4B => self.wx,
172 |
173 | 0xFF47 => self.bg_pallette,
174 | 0xFF48 => self.sprite_pallete_1,
175 | 0xFF49 => self.sprite_pallete_2,
176 |
177 | // High and low bits of a 16-bit register
178 | 0xFF51 => self.cgb_dma.get_source_upper(),
179 | 0xFF52 => self.cgb_dma.get_source_lower(),
180 | 0xFF53 => self.cgb_dma.get_dest_upper(),
181 | 0xFF54 => self.cgb_dma.get_dest_lower(),
182 | 0xFF55 => self.cgb_dma.get_config_byte(),
183 |
184 | _ => {
185 | log!("Unsupported GPU read at {:#06x}", raw_address);
186 | 0xFF
187 | },
188 | }
189 | }
190 |
191 | fn cache_all_sprites(&mut self) {
192 | // There's room for 40 sprites in the OAM table
193 | let mut i = 0;
194 | while i < 40 {
195 | let address: u16 = i as u16 * 4;
196 |
197 | let y_pos = self.oam.read(address) as i32 - 16;
198 | let x_pos = self.oam.read(address + 1) as i32 - 8;
199 | let pattern_id = self.oam.read(address + 2);
200 | let attribs = self.oam.read(address + 3);
201 |
202 | let above_bg = (attribs & 0b1000_0000) == 0;
203 | let y_flip = (attribs & 0b0100_0000) > 0;
204 | let x_flip = (attribs & 0b0010_0000) > 0;
205 | let use_palette_0 = (attribs & 0b0001_0000) == 0;
206 | let use_upper_vram_bank = (attribs & 0b0000_1000) > 0;
207 | let cgb_palette = attribs & 0b0000_0111;
208 |
209 | if self.sprite_cache.len() > i {
210 | self.sprite_cache[i] = Sprite {
211 | y_pos,
212 | x_pos,
213 | pattern_id,
214 | above_bg,
215 | y_flip,
216 | x_flip,
217 | use_palette_0,
218 | use_upper_vram_bank,
219 | cgb_palette,
220 | };
221 | } else {
222 | self.sprite_cache.push(Sprite {
223 | y_pos,
224 | x_pos,
225 | pattern_id,
226 | above_bg,
227 | y_flip,
228 | x_flip,
229 | use_palette_0,
230 | use_upper_vram_bank,
231 | cgb_palette,
232 | });
233 | }
234 |
235 | i += 1;
236 | }
237 |
238 | self.sprite_cache.truncate(i);
239 | }
240 |
241 | fn begin_dma(&mut self, source: u8) {
242 | // Really, we should be disabling access to anything but HRAM now,
243 | // but if the rom is nice then there shouldn't be an issue.
244 | if self.dma_cycles != 0 {
245 | log!("INTERRUPTING DMA!")
246 | }
247 | self.dma_source = source;
248 | self.dma_cycles = gpu_timing::DMA_CYCLES;
249 | }
250 |
251 | fn update_cgb_generic_dma(
252 | &mut self,
253 | ints: &mut Interrupts,
254 | mem: &mut Memory,
255 | ) {
256 | if self.cgb_dma.bytes_left > 0 && !self.cgb_dma.is_hblank_dma() {
257 | for i in 0..self.cgb_dma.bytes_left {
258 | let value = mem.read(ints, self, self.cgb_dma.source + i);
259 | mem.write(ints, self, self.cgb_dma.dest + i, value);
260 | }
261 | self.cgb_dma.bytes_left = 0;
262 | }
263 | }
264 |
265 | fn update_cgb_hblank_dma(
266 | &mut self,
267 | ints: &mut Interrupts,
268 | mem: &mut Memory,
269 | ) {
270 | if self.cgb_dma.bytes_left > 0 && self.cgb_dma.is_hblank_dma() {
271 | for i in 0..0x10 {
272 | // TODO: This shouldn't be called, all DMAs are a multiple of 0x10 long
273 | if self.cgb_dma.bytes_left == 0 {
274 | break;
275 | }
276 | let value = mem.read(
277 | ints,
278 | self,
279 | self.cgb_dma.source + self.cgb_dma.bytes_copied + i,
280 | );
281 | mem.write(
282 | ints,
283 | self,
284 | self.cgb_dma.dest + self.cgb_dma.bytes_copied + i,
285 | value,
286 | );
287 | }
288 | self.cgb_dma.bytes_left -= 0x10;
289 | self.cgb_dma.bytes_copied += 0x10;
290 | }
291 | }
292 |
293 | fn update_dma(&mut self, ints: &mut Interrupts, mem: &mut Memory) {
294 | if self.cgb_features {
295 | self.update_cgb_generic_dma(ints, mem)
296 | }
297 |
298 | // There isn't one pending
299 | if self.dma_cycles == 0 {
300 | return;
301 | }
302 |
303 | self.dma_cycles -= 1;
304 | // Ready to actually perform DMA?
305 | if self.dma_cycles == 0 {
306 | let source = (self.dma_source as u16) * 0x100;
307 |
308 | for i in 0x00..=0x9F {
309 | let data = mem.read(ints, self, source + i);
310 | self.oam.write(i, data);
311 | }
312 | }
313 | }
314 |
315 | fn enter_vblank(&mut self, ints: &mut Interrupts) {
316 | ints.raise_interrupt(InterruptReason::VBlank);
317 |
318 | // TODO: This seems like odd behaviour to me.
319 | if self.status.vblank_interrupt {
320 | ints.raise_interrupt(InterruptReason::LCDStat);
321 | }
322 |
323 | self.finished_frame.clone_from(&self.frame);
324 | }
325 |
326 | fn run_ly_compare(&mut self, ints: &mut Interrupts) {
327 | if self.ly == self.lyc {
328 | self.status.coincidence_flag = true;
329 |
330 | if self.status.lyc {
331 | ints.raise_interrupt(InterruptReason::LCDStat);
332 | }
333 | }
334 | }
335 |
336 | pub fn step(&mut self, ints: &mut Interrupts, mem: &mut Memory) {
337 | // TODO: Check that a DMA is performed even with display off
338 | self.update_dma(ints, mem);
339 |
340 | if !self.control.display_enable {
341 | return;
342 | }
343 |
344 | self.lx += 1;
345 | if self.lx == gpu_timing::HTOTAL {
346 | self.lx = 0;
347 | }
348 |
349 | let mode = self.status.get_mode();
350 |
351 | if mode == LcdMode::VBlank {
352 | if self.lx == 0 {
353 | self.ly += 1;
354 | if self.ly == gpu_timing::VTOTAL {
355 | self.ly = 0;
356 | }
357 |
358 | self.run_ly_compare(ints);
359 |
360 | if self.ly == 0 {
361 | self.window_line_counter = 0;
362 | if self.status.oam_interrupt {
363 | ints.raise_interrupt(InterruptReason::LCDStat);
364 | }
365 | self.status.set_mode(LcdMode::OAMSearch);
366 | self.cache_all_sprites();
367 | self.draw_line_if_necessary(ints, mem);
368 | }
369 | }
370 | return;
371 | }
372 |
373 | if self.lx == 0 {
374 | // Unusual GPU implementation detail. This is only
375 | // incremented when the Window was drawn on this scanline.
376 | // TODO: Relate these magic numbers to constants.
377 | if self.control.window_enable
378 | && self.wx < 166
379 | && self.wy < 143
380 | && self.ly >= self.wy
381 | {
382 | self.window_line_counter += 1;
383 | }
384 |
385 | self.ly += 1;
386 |
387 | self.run_ly_compare(ints);
388 | // Done with frame, enter VBlank
389 | if self.ly == gpu_timing::VBLANK_ON {
390 | self.enter_vblank(ints);
391 | self.status.set_mode(LcdMode::VBlank);
392 | } else {
393 | if mode != LcdMode::OAMSearch {
394 | self.status.set_mode(LcdMode::OAMSearch);
395 | self.cache_all_sprites();
396 | self.draw_line_if_necessary(ints, mem);
397 | }
398 | }
399 | return;
400 | }
401 |
402 | if self.lx == gpu_timing::HTRANSFER_ON {
403 | self.status.set_mode(LcdMode::Transfer);
404 | self.draw_line_if_necessary(ints, mem);
405 | return;
406 | }
407 |
408 | if self.lx == gpu_timing::HBLANK_ON {
409 | self.update_cgb_hblank_dma(ints, mem);
410 | if self.status.hblank_interrupt {
411 | ints.raise_interrupt(InterruptReason::LCDStat)
412 | }
413 | self.status.set_mode(LcdMode::HBlank);
414 | return;
415 | }
416 |
417 | self.draw_line_if_necessary(ints, mem);
418 | }
419 |
420 | #[inline(always)]
421 | fn draw_line_if_necessary(
422 | &mut self,
423 | ints: &mut Interrupts,
424 | mem: &mut Memory,
425 | ) {
426 | let line_start =
427 | gpu_timing::HTRANSFER_ON + if self.ly == 0 { 160 } else { 48 };
428 |
429 | if self.lx == line_start {
430 | // Draw the current line
431 | // TODO: Move these draw_pixel calls into the mode switch
432 | // to allow mid-scanline visual effects
433 | self.cache_sprites_on_line(self.ly);
434 | for x in 0..(SCREEN_WIDTH as u8) {
435 | self.draw_pixel(ints, mem, x, self.ly);
436 | }
437 | }
438 | }
439 |
440 | fn draw_pixel(&mut self, ints: &Interrupts, mem: &Memory, x: u8, y: u8) {
441 | let ux = x as usize;
442 | let uy = y as usize;
443 | let idx = uy * SCREEN_WIDTH + ux;
444 |
445 | let bg_col: Colour;
446 | let bg_col_id = if self.cgb_features || self.control.bg_display {
447 | let (new_col, id) = self.get_background_colour_at(ints, mem, x, y);
448 | bg_col = new_col;
449 | id
450 | } else {
451 | bg_col = grey_shades::white();
452 | 0
453 | };
454 |
455 | // If there's a non-transparent sprite here, use its colour
456 | let s_col = self.get_sprite_colour_at(mem, bg_col, bg_col_id, x, y);
457 |
458 | self.frame[idx] = s_col;
459 | }
460 |
461 | fn get_colour_id_in_line(&self, tile_line: u16, subx: u8) -> u16 {
462 | let lower = tile_line & 0xFF;
463 | let upper = (tile_line & 0xFF00) >> 8;
464 |
465 | let shift_amnt = 7 - subx;
466 | let mask = 1 << shift_amnt;
467 | let u = (upper & mask) >> shift_amnt;
468 | let l = (lower & mask) >> shift_amnt;
469 | let pixel_colour_id = (u << 1) | l;
470 |
471 | pixel_colour_id
472 | }
473 |
474 | fn get_shade_from_colour_id(
475 | &self,
476 | pixel_colour_id: u16,
477 | palette: u8,
478 | ) -> Colour {
479 | let shift_2 = pixel_colour_id * 2;
480 | let shade = (palette & (0b11 << shift_2)) >> shift_2;
481 |
482 | colour_from_grey_shade_id(shade)
483 | }
484 |
485 | fn get_background_colour_at(
486 | &self,
487 | ints: &Interrupts,
488 | mem: &Memory,
489 | x: u8,
490 | y: u8,
491 | ) -> (Colour, u16) {
492 | let is_window = self.control.window_enable
493 | && x as isize > self.wx as isize - 8
494 | && y >= self.wy;
495 |
496 | let tilemap_select = if is_window {
497 | self.control.window_tile_map_display_select
498 | } else {
499 | self.control.bg_tile_map_display_select
500 | };
501 |
502 | let tilemap_base = if tilemap_select { 0x9C00 } else { 0x9800 };
503 |
504 | // This is which tile ID our pixel is in
505 | let x16: u16;
506 | let y16: u16;
507 |
508 | if is_window {
509 | // TODO: Check this saturating_sub, it's a guess.
510 | // Super Mario Bros Deluxe pause menu crashes without it
511 | x16 = x.wrapping_sub(self.wx.saturating_sub(7)) as u16;
512 | y16 = self.window_line_counter as u16;
513 | } else {
514 | x16 = x.wrapping_add(self.scx) as u16;
515 | y16 = y.wrapping_add(self.scy) as u16;
516 | }
517 |
518 | let tx = x16 / 8;
519 | let ty = y16 / 8;
520 | // NOTE: Things like y16 % 8 is equivalent to y16 - ty * 8
521 | // However, this is not more performant. I think the compiler
522 | // is smart enough to recognise that.
523 | let mut subx = (x16 % 8) as u8;
524 | let mut suby = y16 % 8;
525 |
526 | let byte_offset = ty * 32 + tx;
527 | let tilemap_address = tilemap_base + byte_offset;
528 | let tile_metadata = mem.vram.bg_map_attributes.get_entry(byte_offset);
529 |
530 | let tile_id_raw = mem.read(ints, self, tilemap_address);
531 | let tile_id: u16;
532 |
533 | if self.control.bg_and_window_data_select {
534 | // 0x8000 addressing mode
535 | tile_id = tile_id_raw as u16;
536 | } else {
537 | // 0x8800 addressing mode
538 | if tile_id_raw < 128 {
539 | tile_id = (tile_id_raw as u16) + 256;
540 | } else {
541 | tile_id = tile_id_raw as u16
542 | }
543 | }
544 |
545 | // BG tile flipping is a CGB-exclusive feature
546 | if self.cgb_features {
547 | if tile_metadata.x_flip {
548 | subx = 7 - subx;
549 | }
550 | if tile_metadata.y_flip {
551 | suby = 7 - suby;
552 | }
553 | }
554 |
555 | let tile_byte_offset = tile_id * 16;
556 | let tile_line_offset = tile_byte_offset + (suby * 2);
557 |
558 | // This is the line of the tile data that out pixel resides on
559 | let tiledata_base = 0x8000;
560 | let tile_address = tiledata_base + tile_line_offset;
561 |
562 | let bank = if self.cgb_features {
563 | tile_metadata.vram_bank as u16
564 | } else {
565 | 0
566 | };
567 | let tile_line0 = mem.vram.read_arbitrary_bank(bank, tile_address);
568 | let tile_line1 = mem.vram.read_arbitrary_bank(bank, tile_address + 1);
569 | let tile_line = combine_u8!(tile_line1, tile_line0);
570 |
571 | let col_id = self.get_colour_id_in_line(tile_line, subx);
572 |
573 | if self.cgb_features {
574 | let colour = mem
575 | .palette_ram
576 | .get_bg_palette_colour(tile_metadata.palette as u16, col_id);
577 | (colour, col_id)
578 | } else {
579 | (
580 | self.get_shade_from_colour_id(col_id, self.bg_pallette),
581 | col_id,
582 | )
583 | }
584 | }
585 |
586 | fn get_sprite_colour_at(
587 | &self,
588 | mem: &Memory,
589 | bg_col: Colour,
590 | bg_col_id: u16,
591 | x: u8,
592 | y: u8,
593 | ) -> Colour {
594 | // Sprites are hidden for this scanline
595 | if !self.control.obj_enable {
596 | return bg_col;
597 | }
598 |
599 | let sprite_height = if self.control.obj_size { 16 } else { 8 };
600 |
601 | let ix = x as i32;
602 | let iy = y as i32;
603 |
604 | let mut maybe_colour: Option = None;
605 | let mut min_x: i32 = SCREEN_WIDTH as i32 + 8;
606 | for sprite in &self.sprites_on_line {
607 | let mut above_bg = sprite.above_bg;
608 | // In CGB mode, bg_display off means sprites always get priority.
609 | if self.cgb_features && !self.control.bg_display {
610 | above_bg = true;
611 | }
612 |
613 | if sprite.x_pos <= ix
614 | && (sprite.x_pos + 8) > ix
615 | && sprite.x_pos < min_x
616 | {
617 | if !above_bg && bg_col_id != 0 {
618 | continue;
619 | }
620 |
621 | let mut subx = (ix - sprite.x_pos) as u8;
622 | let mut suby = iy - sprite.y_pos;
623 |
624 | // Tile address for 8x8 mode
625 | let mut pattern = sprite.pattern_id;
626 |
627 | if sprite_height == 16 {
628 | if suby > 7 {
629 | suby -= 8;
630 |
631 | if sprite.y_flip {
632 | pattern = sprite.pattern_id & 0xFE;
633 | } else {
634 | pattern = sprite.pattern_id | 0x01;
635 | }
636 | } else {
637 | if sprite.y_flip {
638 | pattern = sprite.pattern_id | 0x01;
639 | } else {
640 | pattern = sprite.pattern_id & 0xFE;
641 | }
642 | }
643 | }
644 |
645 | if sprite.x_flip {
646 | subx = 7 - subx
647 | }
648 | // TODO: Not sure if this applies to vertically flipped 8x16 mode sprites
649 | if sprite.y_flip {
650 | suby = 7 - suby
651 | }
652 |
653 | let tile_address = 0x8000 + (pattern as u16) * 16;
654 | let line_we_need = suby as u16 * 2;
655 | let bank = if self.cgb_features && sprite.use_upper_vram_bank {
656 | 1
657 | } else {
658 | 0
659 | };
660 | let tile_address = tile_address + line_we_need;
661 | // log!("Sprite at [{},{}] is using upper VRAM bank? {:?}, pattern_id {}, address: {:#06x}", sprite.x_pos, sprite.y_pos, sprite.use_upper_vram_bank, sprite.pattern_id, tile_address);
662 |
663 | let tile_line0 =
664 | mem.vram.read_arbitrary_bank(bank, tile_address);
665 | let tile_line1 =
666 | mem.vram.read_arbitrary_bank(bank, tile_address + 1);
667 | let tile_line = combine_u8!(tile_line1, tile_line0);
668 |
669 | let col_id = self.get_colour_id_in_line(tile_line, subx);
670 |
671 | if col_id == 0 {
672 | // This pixel is transparent
673 | continue;
674 | } else {
675 | if self.cgb_features {
676 | let colour = mem.palette_ram.get_obj_palette_colour(
677 | sprite.cgb_palette as u16,
678 | col_id,
679 | );
680 | maybe_colour = Some(colour)
681 | } else {
682 | let palette = if sprite.use_palette_0 {
683 | self.sprite_pallete_1
684 | } else {
685 | self.sprite_pallete_2
686 | };
687 |
688 | min_x = sprite.x_pos;
689 | maybe_colour =
690 | Some(self.get_shade_from_colour_id(col_id, palette))
691 | }
692 | }
693 | }
694 | }
695 |
696 | match maybe_colour {
697 | Some(col) => col,
698 | None => bg_col,
699 | }
700 | }
701 |
702 | // Will be used later for get_sprite_pixel
703 | fn cache_sprites_on_line(&mut self, y: u8) {
704 | let sprite_height = if self.control.obj_size { 16 } else { 8 };
705 |
706 | let iy = y as i32;
707 | self.sprites_on_line.truncate(0);
708 | for s in &self.sprite_cache {
709 | if s.y_pos <= iy && (s.y_pos + sprite_height) > iy {
710 | self.sprites_on_line.push(s.clone());
711 | }
712 | if self.sprites_on_line.len() == 10 {
713 | break;
714 | }
715 | }
716 | }
717 |
718 | pub fn get_rgba_frame(&self) -> [u8; SCREEN_RGBA_SLICE_SIZE] {
719 | let mut out_array = [0; SCREEN_RGBA_SLICE_SIZE];
720 | for i in 0..SCREEN_BUFFER_SIZE {
721 | let start = i * 4;
722 | out_array[start] = self.finished_frame[i].red;
723 | out_array[start + 1] = self.finished_frame[i].green;
724 | out_array[start + 2] = self.finished_frame[i].blue;
725 | out_array[start + 3] = 0xFF;
726 | }
727 | out_array
728 | }
729 |
730 | pub fn new(cgb_features: bool) -> Gpu {
731 | let empty_frame = [grey_shades::white(); SCREEN_BUFFER_SIZE];
732 | Gpu {
733 | cgb_features,
734 | frame: empty_frame,
735 | finished_frame: empty_frame.clone(),
736 | window_line_counter: 0,
737 | scy: 0,
738 | scx: 0,
739 | ly: 0,
740 | lx: 0,
741 | lyc: 0,
742 | wy: 0,
743 | wx: 0,
744 | bg_pallette: 0,
745 | sprite_pallete_1: 0,
746 | sprite_pallete_2: 0,
747 | status: LcdStatus::new(),
748 | control: LcdControl::new(),
749 | oam: Ram::new(OAM_SIZE),
750 | dma_source: 0,
751 | dma_cycles: 0,
752 | cgb_dma: CgbDmaConfig::new(),
753 | sprite_cache: SmallVec::with_capacity(40),
754 | sprites_on_line: SmallVec::with_capacity(10),
755 | }
756 | }
757 | }
758 |
--------------------------------------------------------------------------------
/core/src/helpers.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! combine_u8(
3 | ($x:expr, $y:expr) => (
4 | (($x as u16) << 8) | $y as u16
5 | )
6 | );
7 | #[macro_export]
8 | macro_rules! split_u16(
9 | ($n:expr) => ({
10 | let b1 = ($n & 0x00FF) as u8;
11 | let b2 = (($n & 0xFF00) >> 8) as u8;
12 | (b1, b2)
13 | })
14 | );
15 | #[macro_export]
16 | macro_rules! set_bit(
17 | ($number:expr, $bit_index:expr, $bit:expr) => (
18 | $number = ($number & !(1 << $bit_index)) | $bit << $bit_index;
19 | )
20 | );
21 |
22 | #[macro_export]
23 | macro_rules! log {
24 | ($($a:expr),*) => {
25 | {
26 | #[cfg(not(feature = "std"))]
27 | use alloc::format;
28 | ($crate::callbacks::CALLBACKS.lock().log)(&format!($($a,)*)[..])
29 | }
30 | };
31 | }
32 |
33 | // Macro for bit-matching
34 | // https://www.reddit.com/r/rust/comments/2d7rrj/comment/cjo2c7t/?context=3
35 | #[macro_export]
36 | macro_rules! compute_mask {
37 | (0) => {
38 | 1
39 | };
40 | (1) => {
41 | 1
42 | };
43 | (_) => {
44 | 0
45 | };
46 | }
47 | #[macro_export]
48 | macro_rules! compute_equal {
49 | (0) => {
50 | 0
51 | };
52 | (1) => {
53 | 1
54 | };
55 | (_) => {
56 | 0
57 | };
58 | }
59 | #[macro_export]
60 | macro_rules! bitmatch(
61 | ($x: expr, ($($b: tt),*)) => ({
62 | let mut mask = 0;
63 | let mut val = 0;
64 | $(
65 | mask = (mask << 1) | compute_mask!($b);
66 | val = (val << 1) | compute_equal!($b);
67 | )*
68 | ($x & mask) == val
69 | });
70 | );
71 |
--------------------------------------------------------------------------------
/core/src/interrupts.rs:
--------------------------------------------------------------------------------
1 | pub const INTERRUPT_VECTORS: [u16; 5] = [0x40, 0x48, 0x50, 0x58, 0x60];
2 |
3 | #[derive(Clone)]
4 | pub struct InterruptFields {
5 | pub v_blank: bool,
6 | pub lcd_stat: bool,
7 | pub timer: bool,
8 | pub serial: bool,
9 | pub joypad: bool,
10 | }
11 |
12 | pub enum InterruptReason {
13 | VBlank,
14 | LCDStat,
15 | Timer,
16 | Serial,
17 | Joypad,
18 | }
19 | fn get_interrupt_reason_bitmask(reason: InterruptReason) -> u8 {
20 | match reason {
21 | InterruptReason::VBlank => 0b00000001,
22 | InterruptReason::LCDStat => 0b00000010,
23 | InterruptReason::Timer => 0b00000100,
24 | InterruptReason::Serial => 0b00001000,
25 | InterruptReason::Joypad => 0b00010000,
26 | }
27 | }
28 |
29 | impl InterruptFields {
30 | // TODO: Check if these actually do all start false
31 | pub fn new() -> InterruptFields {
32 | InterruptFields {
33 | v_blank: false,
34 | lcd_stat: false,
35 | timer: false,
36 | serial: false,
37 | joypad: false,
38 | }
39 | }
40 | }
41 | impl From for InterruptFields {
42 | fn from(n: u8) -> InterruptFields {
43 | InterruptFields {
44 | v_blank: (n & 1) == 1,
45 | lcd_stat: ((n >> 1) & 1) == 1,
46 | timer: ((n >> 2) & 1) == 1,
47 | serial: ((n >> 3) & 1) == 1,
48 | joypad: ((n >> 4) & 1) == 1,
49 | }
50 | }
51 | }
52 | impl From for u8 {
53 | fn from(f: InterruptFields) -> u8 {
54 | let b1 = f.v_blank as u8;
55 | let b2 = (f.lcd_stat as u8) << 1;
56 | let b3 = (f.timer as u8) << 2;
57 | let b4 = (f.serial as u8) << 3;
58 | let b5 = (f.joypad as u8) << 4;
59 | b1 | b2 | b3 | b4 | b5
60 | }
61 | }
62 |
63 | pub struct Interrupts {
64 | pub enable: InterruptFields,
65 | pub flag: InterruptFields,
66 |
67 | // "Interrupts master enabled" flag
68 | pub ime: bool,
69 | }
70 |
71 | impl Interrupts {
72 | #[inline(always)]
73 | pub fn raise_interrupt(&mut self, reason: InterruptReason) {
74 | let mut data = self.flag_read();
75 | data |= get_interrupt_reason_bitmask(reason);
76 | self.flag_write(data);
77 | }
78 |
79 | // Called when GB writes to FFFF
80 | #[inline(always)]
81 | pub fn enable_write(&mut self, value: u8) {
82 | // log!("{:08b} written to IE", value);
83 | self.enable = InterruptFields::from(value)
84 | }
85 |
86 | // Called when GB writes to FF0F
87 | #[inline(always)]
88 | pub fn flag_write(&mut self, value: u8) {
89 | self.flag = InterruptFields::from(value)
90 | }
91 |
92 | // Called when GB reads from FFFF
93 | #[inline(always)]
94 | pub fn enable_read(&self) -> u8 {
95 | u8::from(self.enable.clone())
96 | }
97 |
98 | // Called when GB reads from FF0F
99 | #[inline(always)]
100 | pub fn flag_read(&self) -> u8 {
101 | u8::from(self.flag.clone())
102 | }
103 |
104 | pub fn new() -> Interrupts {
105 | Interrupts {
106 | enable: InterruptFields::new(),
107 | flag: InterruptFields::new(),
108 |
109 | ime: false,
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/core/src/joypad.rs:
--------------------------------------------------------------------------------
1 | enum JoypadReadoutMode {
2 | Buttons,
3 | Directions,
4 | Neither,
5 | }
6 |
7 | // TODO: Raise the Joypad interrupt
8 | pub struct Joypad {
9 | readout_mode: JoypadReadoutMode,
10 |
11 | // The GUI writes these values directly via the keyboard
12 | // Every frame.
13 | pub up_pressed: bool,
14 | pub down_pressed: bool,
15 | pub left_pressed: bool,
16 | pub right_pressed: bool,
17 | pub a_pressed: bool,
18 | pub b_pressed: bool,
19 | pub start_pressed: bool,
20 | pub select_pressed: bool,
21 | }
22 |
23 | impl Joypad {
24 | #[inline(always)]
25 | pub fn write(&mut self, n: u8) {
26 | let masked = n & 0b0011_0000;
27 |
28 | self.readout_mode = match masked {
29 | 0b0001_0000 => JoypadReadoutMode::Buttons,
30 | 0b0010_0000 => JoypadReadoutMode::Directions,
31 | _ => JoypadReadoutMode::Neither,
32 | }
33 | }
34 |
35 | #[inline(always)]
36 | fn direction_bits(&self) -> u8 {
37 | (!self.right_pressed as u8)
38 | | ((!self.left_pressed as u8) << 1)
39 | | ((!self.up_pressed as u8) << 2)
40 | | ((!self.down_pressed as u8) << 3)
41 | }
42 |
43 | #[inline(always)]
44 | fn button_bits(&self) -> u8 {
45 | (!self.a_pressed as u8)
46 | | ((!self.b_pressed as u8) << 1)
47 | | ((!self.select_pressed as u8) << 2)
48 | | ((!self.start_pressed as u8) << 3)
49 | }
50 |
51 | #[inline(always)]
52 | fn selection_bits(&self) -> u8 {
53 | match self.readout_mode {
54 | JoypadReadoutMode::Buttons => 0b0010_0000,
55 | JoypadReadoutMode::Directions => 0b0001_0000,
56 | JoypadReadoutMode::Neither => 0,
57 | }
58 | }
59 |
60 | #[inline(always)]
61 | pub fn read(&self) -> u8 {
62 | let n = match self.readout_mode {
63 | JoypadReadoutMode::Buttons => self.button_bits(),
64 | JoypadReadoutMode::Directions => self.direction_bits(),
65 | JoypadReadoutMode::Neither => 0xF,
66 | };
67 |
68 | n | self.selection_bits()
69 | }
70 |
71 | pub fn new() -> Joypad {
72 | Joypad {
73 | readout_mode: JoypadReadoutMode::Buttons,
74 | up_pressed: false,
75 | down_pressed: false,
76 | left_pressed: false,
77 | right_pressed: false,
78 | a_pressed: false,
79 | b_pressed: false,
80 | start_pressed: false,
81 | select_pressed: false,
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/core/src/lcd.rs:
--------------------------------------------------------------------------------
1 | use crate::interrupts::{InterruptReason, Interrupts};
2 |
3 | #[derive(Clone, Copy)]
4 | pub struct LcdControl {
5 | pub display_enable: bool,
6 | pub window_tile_map_display_select: bool,
7 | pub window_enable: bool,
8 | pub bg_and_window_data_select: bool,
9 | pub bg_tile_map_display_select: bool,
10 | pub obj_size: bool,
11 | pub obj_enable: bool,
12 | pub bg_display: bool,
13 | }
14 | impl LcdControl {
15 | pub fn new() -> LcdControl {
16 | // This value is set by the DMG boot rom.
17 | LcdControl::from(0b10000101)
18 | }
19 | }
20 | impl From for LcdControl {
21 | fn from(n: u8) -> LcdControl {
22 | LcdControl {
23 | bg_display: (n & 0b0000_0001) == 0b0000_0001,
24 | obj_enable: (n & 0b0000_0010) == 0b0000_0010,
25 | obj_size: (n & 0b0000_0100) == 0b0000_0100,
26 | bg_tile_map_display_select: (n & 0b0000_1000) == 0b0000_1000,
27 | bg_and_window_data_select: (n & 0b0001_0000) == 0b0001_0000,
28 | window_enable: (n & 0b0010_0000) == 0b0010_0000,
29 | window_tile_map_display_select: (n & 0b0100_0000) == 0b0100_0000,
30 | display_enable: (n & 0b1000_0000) == 0b1000_0000,
31 | }
32 | }
33 | }
34 | impl From for u8 {
35 | fn from(lcd: LcdControl) -> u8 {
36 | lcd.bg_display as u8
37 | | (lcd.obj_enable as u8) << 1
38 | | (lcd.obj_size as u8) << 2
39 | | (lcd.bg_tile_map_display_select as u8) << 3
40 | | (lcd.bg_and_window_data_select as u8) << 4
41 | | (lcd.window_enable as u8) << 5
42 | | (lcd.window_tile_map_display_select as u8) << 6
43 | | (lcd.display_enable as u8) << 7
44 | }
45 | }
46 |
47 | #[derive(PartialEq, Clone, Debug)]
48 | pub enum LcdMode {
49 | HBlank = 0,
50 | VBlank = 1,
51 | OAMSearch = 2,
52 | Transfer = 3,
53 | }
54 |
55 | #[derive(Clone, Copy)]
56 | pub struct LcdStatus {
57 | pub lyc: bool,
58 | pub oam_interrupt: bool,
59 | pub vblank_interrupt: bool,
60 | pub hblank_interrupt: bool,
61 | pub coincidence_flag: bool,
62 | pub mode_flag: u8,
63 | }
64 | impl LcdStatus {
65 | pub fn get_mode(&self) -> LcdMode {
66 | match self.mode_flag {
67 | 0 => LcdMode::HBlank,
68 | 1 => LcdMode::VBlank,
69 | 2 => LcdMode::OAMSearch,
70 | 3 => LcdMode::Transfer,
71 | _ => panic!("Invalid LCD mode"),
72 | }
73 | }
74 |
75 | #[inline(always)]
76 | pub fn set_data(&mut self, data: u8, ints: &mut Interrupts) {
77 | // There's actually a DMG GPU bug when writing to LCDStat
78 | // where sometimes it fires off an interrupt at the wrong time
79 | // https://robertovaccari.com/blog/2020_09_26_gameboy/
80 | match self.get_mode() {
81 | LcdMode::HBlank | LcdMode::VBlank => {
82 | if self.lyc {
83 | ints.raise_interrupt(InterruptReason::LCDStat)
84 | }
85 | },
86 | _ => {},
87 | }
88 |
89 | let new_stat = LcdStatus::from(data);
90 | self.lyc = new_stat.lyc;
91 | self.oam_interrupt = new_stat.oam_interrupt;
92 | self.vblank_interrupt = new_stat.vblank_interrupt;
93 | self.hblank_interrupt = new_stat.hblank_interrupt;
94 | // NOTE: We *don't* set the coincidence_flag or mode_flag,
95 | // they're read only
96 | }
97 |
98 | #[inline(always)]
99 | pub fn set_mode(&mut self, mode: LcdMode) {
100 | self.mode_flag = mode as u8;
101 | }
102 |
103 | pub fn new() -> LcdStatus {
104 | // LCD starts in OAMSearch, not HBlank
105 | LcdStatus::from(0b000000_10)
106 | }
107 | }
108 | impl From for LcdStatus {
109 | #[inline(always)]
110 | fn from(n: u8) -> LcdStatus {
111 | LcdStatus {
112 | lyc: (n & 0b1000000) == 0b1000000,
113 | oam_interrupt: (n & 0b100000) == 0b100000,
114 | vblank_interrupt: (n & 0b10000) == 0b10000,
115 | hblank_interrupt: (n & 0b1000) == 0b1000,
116 | coincidence_flag: (n & 0b100) == 0b100,
117 | mode_flag: n & 0b11,
118 | }
119 | }
120 | }
121 | impl From for u8 {
122 | #[inline(always)]
123 | fn from(lcd: LcdStatus) -> u8 {
124 | lcd.mode_flag
125 | | (lcd.coincidence_flag as u8) << 2
126 | | (lcd.hblank_interrupt as u8) << 3
127 | | (lcd.vblank_interrupt as u8) << 4
128 | | (lcd.oam_interrupt as u8) << 5
129 | | (lcd.lyc as u8) << 6
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/core/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(feature = "std"), no_std)]
2 |
3 | #[cfg(not(feature = "std"))]
4 | extern crate alloc;
5 |
6 | pub mod alu;
7 | pub mod callbacks;
8 | pub mod cartridge;
9 | pub mod cgb_dma;
10 | pub mod colour; // innit bruv
11 | pub mod config;
12 | pub mod constants;
13 | pub mod cpu;
14 | pub mod gpu;
15 | pub mod helpers;
16 | pub mod interrupts;
17 | pub mod joypad;
18 | pub mod lcd;
19 | pub mod memory;
20 | pub mod registers;
21 | pub mod serial_cable;
22 | pub mod sound;
23 |
--------------------------------------------------------------------------------
/core/src/memory/battery_backed_ram.rs:
--------------------------------------------------------------------------------
1 | // RAM with a save file
2 | use crate::{callbacks::CALLBACKS, cartridge::Cartridge, memory::ram::Ram};
3 |
4 | // The amount of milliseconds we wait before saving our save file
5 | // (otherwise eg. Link's Awakening would write 2,700 save files
6 | // on its first frame)
7 | const DEBOUNCE_MILLIS: usize = 1000;
8 |
9 | pub struct BatteryBackedRam {
10 | pub ram: Ram,
11 | pub size: usize,
12 |
13 | cart: Cartridge,
14 |
15 | battery_enabled: bool,
16 | changed_since_last_save: bool,
17 | last_saved_at: usize,
18 | }
19 |
20 | impl BatteryBackedRam {
21 | pub fn read(&self, address: u16) -> u8 {
22 | self.ram.read(address)
23 | }
24 |
25 | pub fn read_usize(&self, address: usize) -> u8 {
26 | self.ram.bytes[address]
27 | }
28 |
29 | pub fn write(&mut self, address: u16, value: u8) {
30 | self.ram.write(address, value);
31 | self.changed_since_last_save = true;
32 | }
33 |
34 | pub fn write_usize(&mut self, address: usize, value: u8) {
35 | self.ram.bytes[address] = value;
36 | self.changed_since_last_save = true;
37 | }
38 |
39 | pub fn step(&mut self, ms_since_boot: usize) {
40 | if !self.changed_since_last_save || !self.battery_enabled {
41 | return;
42 | }
43 |
44 | let millis_since_last_save = ms_since_boot - self.last_saved_at;
45 |
46 | if millis_since_last_save >= DEBOUNCE_MILLIS {
47 | self.last_saved_at = ms_since_boot;
48 | self.save_ram_contents()
49 | }
50 | }
51 |
52 | fn save_ram_contents(&mut self) {
53 | self.changed_since_last_save = false;
54 |
55 | (CALLBACKS.lock().save)(
56 | &self.cart.title[..],
57 | &self.cart.rom_path[..],
58 | &self.ram.bytes,
59 | );
60 | }
61 |
62 | pub fn new(
63 | cart: Cartridge,
64 | additional_ram_size: usize,
65 | battery_enabled: bool,
66 | ) -> BatteryBackedRam {
67 | // Some MBCs, like MBC2, always have a few bytes of RAM installed.
68 | // The cartridge header only tells us about additional external RAM.
69 | let ram_size = cart.ram_size + additional_ram_size;
70 |
71 | let save_contents = (CALLBACKS.lock().load)(
72 | &cart.title[..],
73 | &cart.rom_path[..],
74 | ram_size,
75 | );
76 |
77 | let ram = Ram::from_bytes(save_contents, ram_size);
78 |
79 | BatteryBackedRam {
80 | ram,
81 | size: ram_size,
82 |
83 | cart,
84 | battery_enabled,
85 | changed_since_last_save: false,
86 |
87 | last_saved_at: 0,
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/core/src/memory/cgb_speed_switch.rs:
--------------------------------------------------------------------------------
1 | use crate::log;
2 |
3 | pub struct CgbSpeedSwitch {
4 | pub armed: bool,
5 | pub current_speed_is_double: bool,
6 | cgb_features: bool,
7 | }
8 |
9 | // TODO: Actually act on this for CPU speed.
10 | // This just tracks the byte state.
11 | impl CgbSpeedSwitch {
12 | pub fn write_switch_byte(&mut self, value: u8) {
13 | if self.cgb_features {
14 | self.armed = value & 1 > 0;
15 | }
16 | }
17 | pub fn read_switch_byte(&self) -> u8 {
18 | let top_bit = if self.current_speed_is_double {
19 | 0x80
20 | } else {
21 | 0x00
22 | };
23 | let bottom_bit = if self.armed { 0x01 } else { 0x00 };
24 | top_bit | bottom_bit
25 | }
26 | pub fn execute_speed_switch(&mut self) {
27 | self.armed = false;
28 | self.current_speed_is_double = !self.current_speed_is_double;
29 | log!(
30 | "Performing CGB speed switch. New speed: {}",
31 | match self.current_speed_is_double {
32 | true => "Double",
33 | false => "Single",
34 | }
35 | );
36 | }
37 |
38 | pub fn new(cgb_features: bool) -> Self {
39 | CgbSpeedSwitch {
40 | armed: false,
41 | current_speed_is_double: false,
42 | cgb_features,
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/mbc1.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::log;
3 | use crate::memory::battery_backed_ram::BatteryBackedRam;
4 | use crate::memory::mbcs::MBC;
5 | use crate::memory::rom::Rom;
6 |
7 | // 16KB (one bank size) in bytes
8 | pub const KB_16: usize = 16_384;
9 |
10 | pub struct MBC1 {
11 | pub rom: Rom,
12 | pub rom_bank: u8,
13 |
14 | pub ram: BatteryBackedRam,
15 | pub ram_enabled: bool,
16 |
17 | has_shown_ram_warning: bool,
18 | }
19 |
20 | impl MBC for MBC1 {
21 | fn read(&self, address: u16) -> u8 {
22 | match address {
23 | 0x0..=0x3FFF => self.read_bank(0, address),
24 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000),
25 | _ => panic!("Unsupported MBC1 read at {:#06x}", address),
26 | }
27 | }
28 |
29 | fn write(&mut self, address: u16, value: u8) {
30 | match address {
31 | 0x0000..=0x1FFF => {
32 | self.ram_enabled = (value & 0x0A) == 0x0A;
33 | },
34 | 0x2000..=0x3FFF => {
35 | // TODO: Bank numbers are masked to the max bank number
36 | // TODO: Upper/RAM banking support
37 | let mut n = value & 0b11111;
38 | if n == 0 {
39 | n = 1
40 | }
41 | self.rom_bank = n
42 | },
43 | // 0x4000 ..= 0x5FFF => {
44 | // panic!("Unsupported upper bank number or RAM banking in MBC1")
45 | // },
46 | // 0x6000 ..= 0x7FFF => {
47 | // panic!("Unsupported MBC1 mode select write")
48 | // },
49 | _ => {}, //panic!("Unsupported MBC1 write at {:#06x} (value: {:#04x})", address, value)
50 | }
51 | }
52 |
53 | fn ram_read(&self, address: u16) -> u8 {
54 | if !self.ram_enabled && !self.has_shown_ram_warning {
55 | log!("[WARN] MBC1 RAM read while disabled");
56 | }
57 |
58 | // When an address outside of RAM space is read, the gameboy
59 | // doesn't seem to be intended to crash.
60 | // Not sure what to return here, but unusable RAM on the GB itself
61 | // returns 0xFF
62 | if address as usize >= self.ram.size {
63 | return 0xFF;
64 | }
65 |
66 | self.ram.read(address)
67 | }
68 |
69 | fn ram_write(&mut self, address: u16, value: u8) {
70 | if !self.ram_enabled && !self.has_shown_ram_warning {
71 | log!("[WARN] MBC1 RAM write while disabled");
72 | // Otherwise the game is slowed down by constant debug printing
73 | self.has_shown_ram_warning = true;
74 | }
75 |
76 | // See note in ram_read
77 | if address as usize >= self.ram.size {
78 | return;
79 | }
80 |
81 | self.ram.write(address, value)
82 | }
83 |
84 | fn step(&mut self, ms_since_boot: usize) {
85 | self.ram.step(ms_since_boot)
86 | }
87 | }
88 |
89 | impl MBC1 {
90 | fn read_bank(&self, bank: u8, address: u16) -> u8 {
91 | let ub = bank as usize;
92 | let ua = address as usize;
93 | self.rom.bytes[KB_16 * ub + ua]
94 | }
95 |
96 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self {
97 | // TODO: Banked RAM
98 | if cart_info.ram_size > 8_192 {
99 | panic!("gbrs doesn't support banked (>=32K) MBC1 RAM");
100 | }
101 |
102 | let has_battery = cart_info.cart_type == 0x03;
103 | MBC1 {
104 | rom,
105 | ram_enabled: false,
106 | rom_bank: 1,
107 | ram: BatteryBackedRam::new(cart_info, 0, has_battery),
108 | has_shown_ram_warning: false,
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/mbc2.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::log;
3 | use crate::memory::battery_backed_ram::BatteryBackedRam;
4 | use crate::memory::mbcs::MBC;
5 | use crate::memory::rom::Rom;
6 |
7 | // 16KB (one bank size) in bytes
8 | pub const KB_16: usize = 16_384;
9 |
10 | pub struct MBC2 {
11 | pub rom: Rom,
12 | pub rom_bank: u8,
13 |
14 | pub ram: BatteryBackedRam,
15 | pub ram_enabled: bool,
16 |
17 | has_shown_ram_warning: bool,
18 | }
19 |
20 | impl MBC for MBC2 {
21 | fn read(&self, address: u16) -> u8 {
22 | match address {
23 | 0x0..=0x3FFF => self.read_bank(0, address),
24 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000),
25 | _ => panic!("Unsupported MBC2 read at {:#06x}", address),
26 | }
27 | }
28 |
29 | fn write(&mut self, address: u16, value: u8) {
30 | match address {
31 | 0x0000..=0x1FFF => {
32 | self.ram_enabled = (value & 0x0A) == 0x0A;
33 | },
34 | 0x2000..=0x3FFF => {
35 | let mut n = value & 0b1111;
36 | if n == 0 {
37 | n = 1
38 | }
39 | self.rom_bank = n
40 | },
41 | _ => {},
42 | }
43 | }
44 |
45 | fn ram_read(&self, address: u16) -> u8 {
46 | if !self.ram_enabled && !self.has_shown_ram_warning {
47 | log!("[WARN] MBC2 RAM read while disabled");
48 | }
49 |
50 | // When an address outside of RAM space is read, the gameboy
51 | // doesn't seem to be intended to crash.
52 | // Not sure what to return here, but unusable RAM on the GB itself
53 | // returns 0xFF
54 | if address as usize >= self.ram.size {
55 | return 0xFF;
56 | }
57 |
58 | self.ram.read(address)
59 | }
60 |
61 | fn ram_write(&mut self, address: u16, value: u8) {
62 | if !self.ram_enabled && !self.has_shown_ram_warning {
63 | log!("[WARN] MBC2 RAM write while disabled");
64 | // Otherwise the game is slowed down by constant debug printing
65 | self.has_shown_ram_warning = true;
66 | }
67 |
68 | // See note in ram_read
69 | if address as usize >= self.ram.size {
70 | return;
71 | }
72 |
73 | // NOTE: Only the bottom 4 bits of the written byte are actually
74 | // saved on an MBC2, it's half-byte RAM. The top half is
75 | // undefined when read.
76 | self.ram.write(address, value & 0xF)
77 | }
78 |
79 | fn step(&mut self, ms_since_boot: usize) {
80 | self.ram.step(ms_since_boot)
81 | }
82 | }
83 |
84 | impl MBC2 {
85 | fn read_bank(&self, bank: u8, address: u16) -> u8 {
86 | let ub = bank as usize;
87 | let ua = address as usize;
88 | self.rom.bytes[KB_16 * ub + ua]
89 | }
90 |
91 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self {
92 | let has_battery = cart_info.cart_type == 0x06;
93 | MBC2 {
94 | rom,
95 | ram_enabled: false,
96 | rom_bank: 1,
97 | // The MBC2 always has 512 (half-)bytes of RAM built-in
98 | ram: BatteryBackedRam::new(cart_info, 512, has_battery),
99 | has_shown_ram_warning: false,
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/mbc3.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::log;
3 | use crate::memory::battery_backed_ram::BatteryBackedRam;
4 | use crate::memory::mbcs::MBC;
5 | use crate::memory::rom::Rom;
6 |
7 | // 8KB (one RAM bank size) in bytes
8 | pub const KB_8: usize = 8_192;
9 | // 16KB (one ROM bank size) in bytes
10 | pub const KB_16: usize = KB_8 * 2;
11 |
12 | pub struct MBC3 {
13 | pub rom: Rom,
14 | pub rom_bank: u8,
15 |
16 | pub ram: BatteryBackedRam,
17 | pub ram_bank: u8,
18 | pub ram_enabled: bool,
19 |
20 | // Unique MBC3 feature, sometimes the RAM addresses can be set up to
21 | // read a Real Time Clock
22 | pub rtc_select: bool,
23 |
24 | has_shown_ram_warning: bool,
25 | }
26 |
27 | impl MBC for MBC3 {
28 | fn read(&self, address: u16) -> u8 {
29 | match address {
30 | 0x0..=0x3FFF => self.read_bank(0, address),
31 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000),
32 | _ => panic!("Unsupported MBC3 read at {:#06x}", address),
33 | }
34 | }
35 |
36 | fn write(&mut self, address: u16, value: u8) {
37 | match address {
38 | 0x0000..=0x1FFF => {
39 | self.ram_enabled = (value & 0x0A) == 0x0A;
40 | },
41 | 0x2000..=0x3FFF => {
42 | let mut n = value & 0b01111111;
43 | let max_bank = (self.rom.bytes.len() / KB_16) as u8;
44 | let bitmask = max_bank - 1;
45 | n = n & bitmask;
46 | if n == 0 {
47 | n = 1
48 | }
49 | // log!("Selecting ROM bank {}", n);
50 | self.rom_bank = n
51 | },
52 | 0x4000..=0x5FFF => {
53 | match value {
54 | 0x00..=0x03 => {
55 | // log!("Selecting RAM bank {}", value);
56 | self.ram_bank = value;
57 | self.rtc_select = false;
58 | },
59 | // TODO: This maps Real Time Clock stuff
60 | 0x08..=0x0C => {
61 | self.rtc_select = true;
62 | },
63 | // This is a noop
64 | _ => {},
65 | }
66 | },
67 | 0x6000..=0x7FFF => {
68 | // TODO: RTC latching
69 | },
70 | _ => {},
71 | }
72 | }
73 |
74 | fn ram_read(&self, address: u16) -> u8 {
75 | if !self.ram_enabled && !self.has_shown_ram_warning {
76 | // log!("[WARN] MBC3 RAM read while disabled");
77 | }
78 |
79 | if self.rtc_select {
80 | // The game has opted to replace RAM with the value of the RTC.
81 | // TODO: Properly emulate the Real Time Clock
82 | // log!("Reading the RTC");
83 | return 0;
84 | }
85 |
86 | self.read_ram_bank(self.ram_bank, address)
87 | }
88 |
89 | fn ram_write(&mut self, address: u16, value: u8) {
90 | if !self.ram_enabled && !self.has_shown_ram_warning {
91 | log!("[WARN] MBC3 RAM write while disabled");
92 | // Otherwise the game is slowed down by constant debug printing
93 | self.has_shown_ram_warning = true;
94 | }
95 |
96 | self.write_ram_bank(self.ram_bank, address, value);
97 | }
98 |
99 | fn step(&mut self, ms_since_boot: usize) {
100 | self.ram.step(ms_since_boot)
101 | }
102 | }
103 |
104 | impl MBC3 {
105 | fn read_bank(&self, bank: u8, address: u16) -> u8 {
106 | let ub = bank as usize;
107 | let ua = address as usize;
108 | let final_addr = KB_16 * ub + ua;
109 |
110 | // if final_addr >= self.rom.bytes.len() {
111 | // return 0xFF;
112 | // }
113 |
114 | self.rom.bytes[final_addr]
115 | }
116 |
117 | fn read_ram_bank(&self, bank: u8, address: u16) -> u8 {
118 | let ub = bank as usize;
119 | let ua = address as usize;
120 | let final_addr = KB_8 * ub + ua;
121 |
122 | // if final_addr >= self.ram.size {
123 | // return 0xFF;
124 | // }
125 |
126 | self.ram.ram.bytes[final_addr]
127 | }
128 |
129 | fn write_ram_bank(&mut self, bank: u8, address: u16, value: u8) {
130 | let ub = bank as usize;
131 | let ua = address as usize;
132 | let final_addr = KB_8 * ub + ua;
133 |
134 | if final_addr >= self.ram.size {
135 | return;
136 | }
137 |
138 | self.ram.write(final_addr as u16, value);
139 | }
140 |
141 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self {
142 | let has_battery = cart_info.cart_type == 0x13;
143 | MBC3 {
144 | rom,
145 | rom_bank: 1,
146 | ram: BatteryBackedRam::new(cart_info, 0, has_battery),
147 | ram_bank: 0,
148 | ram_enabled: false,
149 | rtc_select: false,
150 | has_shown_ram_warning: false,
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/mbc5.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::log;
3 | use crate::memory::battery_backed_ram::BatteryBackedRam;
4 | use crate::memory::mbcs::MBC;
5 | use crate::memory::rom::Rom;
6 |
7 | // 8KB (one RAM bank) in bytes
8 | pub const KB_8: usize = 8_192;
9 | // 16KB (one ROM bank) in bytes
10 | pub const KB_16: usize = 16_384;
11 |
12 | pub struct MBC5 {
13 | pub rom: Rom,
14 | pub rom_bank: u16,
15 |
16 | pub ram: BatteryBackedRam,
17 | pub ram_enabled: bool,
18 | pub ram_bank: u8,
19 |
20 | has_shown_ram_warning: bool,
21 | }
22 |
23 | impl MBC for MBC5 {
24 | fn read(&self, address: u16) -> u8 {
25 | match address {
26 | 0x0..=0x3FFF => self.read_bank(0, address),
27 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000),
28 | _ => panic!("Unsupported MBC5 read at {:#06x}", address),
29 | }
30 | }
31 |
32 | fn write(&mut self, address: u16, value: u8) {
33 | match address {
34 | 0x0000..=0x1FFF => {
35 | self.ram_enabled = (value & 0x0A) == 0x0A;
36 | },
37 | 0x2000..=0x2FFF => {
38 | // TODO: Are bank numbers masked to the max bank number?
39 |
40 | // No zero check. You can map bank 0 twice on MBC5.
41 | self.rom_bank =
42 | (self.rom_bank & 0b0000_0001_0000_0000) | (value as u16);
43 | },
44 | 0x3000..=0x3FFF => {
45 | let bit = if value > 0 { 1 } else { 0 };
46 | self.rom_bank =
47 | (self.rom_bank & 0b0000_0000_1111_1111) | (bit << 8);
48 | },
49 | 0x4000..=0x5FFF => {
50 | // TODO: Rumble. If the MBC has rumble circuitry, this may be
51 | // wrong because we pass on bit 3.
52 | if value > 0x0F {
53 | return;
54 | }
55 | self.ram_bank = value;
56 | },
57 | _ => {},
58 | }
59 | }
60 |
61 | fn ram_read(&self, address: u16) -> u8 {
62 | if !self.ram_enabled && !self.has_shown_ram_warning {
63 | log!("[WARN] MBC5 RAM read while disabled");
64 | }
65 |
66 | let banked_address = address as usize + self.ram_bank as usize * KB_8;
67 |
68 | if banked_address >= self.ram.size {
69 | return 0xFF;
70 | }
71 |
72 | self.ram.read_usize(banked_address)
73 | }
74 |
75 | fn ram_write(&mut self, address: u16, value: u8) {
76 | if !self.ram_enabled && !self.has_shown_ram_warning {
77 | log!("[WARN] MBC5 RAM write while disabled");
78 | // Otherwise the game is slowed down by constant debug printing
79 | self.has_shown_ram_warning = true;
80 | }
81 |
82 | let banked_address = address as usize + self.ram_bank as usize * KB_8;
83 |
84 | if banked_address >= self.ram.size {
85 | return;
86 | }
87 |
88 | self.ram.write_usize(banked_address, value)
89 | }
90 |
91 | fn step(&mut self, ms_since_boot: usize) {
92 | self.ram.step(ms_since_boot)
93 | }
94 | }
95 |
96 | impl MBC5 {
97 | fn read_bank(&self, bank: u16, address: u16) -> u8 {
98 | let ub = bank as usize;
99 | let ua = address as usize;
100 | self.rom.bytes[KB_16 * ub + ua]
101 | }
102 |
103 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self {
104 | // TODO: Banked RAM
105 | if cart_info.ram_size > 8_192 {
106 | panic!("gbrs doesn't support banked (>=32K) MBC5 RAM");
107 | }
108 |
109 | let has_battery = cart_info.cart_type == 0x03;
110 | MBC5 {
111 | rom,
112 | rom_bank: 1,
113 | ram: BatteryBackedRam::new(cart_info, 0, has_battery),
114 | ram_enabled: false,
115 | ram_bank: 0,
116 | has_shown_ram_warning: false,
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::log;
3 | use crate::memory::rom::Rom;
4 |
5 | #[cfg(not(feature = "std"))]
6 | use alloc::boxed::Box;
7 |
8 | pub trait MBC {
9 | fn read(&self, address: u16) -> u8;
10 | fn write(&mut self, address: u16, value: u8);
11 |
12 | fn ram_read(&self, address: u16) -> u8;
13 | fn ram_write(&mut self, address: u16, value: u8);
14 |
15 | // Mostly used to debounce battery-backed RAM saves
16 | fn step(&mut self, ms_since_boot: usize);
17 | }
18 |
19 | mod mbc1;
20 | mod mbc2;
21 | mod mbc3;
22 | mod mbc5;
23 | mod none;
24 |
25 | pub fn mbc_from_info(cart_info: Cartridge, rom: Rom) -> Box {
26 | log!("Loading game \"{}\"", cart_info.title);
27 | log!("Extra chips: {}", get_cart_type_string(&cart_info));
28 | log!("ROM size: {}KB", cart_info.rom_size / 1024);
29 | log!("RAM size: {}KB", cart_info.ram_size / 1024);
30 |
31 | match cart_info.cart_type {
32 | 0x00 => Box::new(none::MBCNone::new(rom)),
33 | 0x01 ..= 0x03 => Box::new(mbc1::MBC1::new(cart_info, rom)),
34 | 0x05 ..= 0x06 => Box::new(mbc2::MBC2::new(cart_info, rom)),
35 | 0x0F ..= 0x13 => Box::new(mbc3::MBC3::new(cart_info, rom)),
36 | 0x19 ..= 0x1E => Box::new(mbc5::MBC5::new(cart_info, rom)),
37 | _ => panic!("gbrs doesn't support this cartridge's memory controller ({:#04x}).", cart_info.cart_type)
38 | }
39 | }
40 |
41 | fn get_cart_type_string(cart_info: &Cartridge) -> &str {
42 | match cart_info.cart_type {
43 | 0x00 => "None",
44 | 0x01 => "MBC1",
45 | 0x02 => "MBC1 + RAM",
46 | 0x03 => "MBC1 + RAM + BATTERY",
47 | // There are some gaps where Pan Docs doesn't define what they are
48 | 0x05 => "MBC2",
49 | 0x06 => "MBC2 + BATTERY",
50 |
51 | 0x08 => "ROM + RAM (Unofficial)", // No gameboy game uses these
52 | 0x09 => "ROM + RAM + BATTERY (Unofficial)",
53 |
54 | 0x0B => "MMM01",
55 | 0x0C => "MMM01 + RAM",
56 | 0x0D => "MMM01 + RAM + BATTERY",
57 |
58 | 0x0F => "MBC3 + TIMER + BATTERY",
59 | 0x10 => "MBC3 + TIMER + RAM + BATTERY",
60 | 0x11 => "MBC3",
61 | 0x12 => "MBC3 + RAM",
62 | 0x13 => "MBC3 + RAM + BATTERY",
63 |
64 | // There is no MBC4. There is superstition about the number 4 in Japan.
65 |
66 | 0x19 => "MBC5",
67 | 0x1A => "MBC5 + RAM",
68 | 0x1B => "MBC5 + RAM + BATTERY",
69 | 0x1C => "MBC5 + RUMBLE",
70 | 0x1D => "MBC5 + RUMBLE + RAM",
71 | 0x1E => "MBC5 + RUMBLE + RAM + BATTERY",
72 |
73 | _ => panic!("gbrs doesn't know the name of this cartridge's memory controller ({:#04x}).", cart_info.cart_type)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/core/src/memory/mbcs/none.rs:
--------------------------------------------------------------------------------
1 | use crate::memory::mbcs::MBC;
2 | use crate::memory::rom::Rom;
3 |
4 | pub struct MBCNone {
5 | pub rom: Rom,
6 | }
7 |
8 | impl MBC for MBCNone {
9 | fn read(&self, address: u16) -> u8 {
10 | self.rom.read(address)
11 | }
12 |
13 | fn write(&mut self, _: u16, _: u8) {
14 | // No MBC ignores writes
15 | }
16 |
17 | fn ram_read(&self, _: u16) -> u8 {
18 | // Unused Gameboy RAM reads as 0xFF
19 | 0xFF
20 | }
21 |
22 | fn ram_write(&mut self, _: u16, _: u8) {
23 | // We don't have RAM
24 | }
25 |
26 | fn step(&mut self, _ms_since_boot: usize) {
27 | // We don't need to do anything here
28 | }
29 | }
30 |
31 | impl MBCNone {
32 | pub fn new(rom: Rom) -> Self {
33 | MBCNone { rom }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/memory/memory.rs:
--------------------------------------------------------------------------------
1 | use crate::cartridge::Cartridge;
2 | use crate::colour::palette_ram::PaletteRam;
3 | use crate::constants::*;
4 | use crate::cpu::EmulationTarget;
5 | use crate::gpu::Gpu;
6 | use crate::interrupts::*;
7 | use crate::joypad::Joypad;
8 | use crate::log;
9 | use crate::memory::cgb_speed_switch::CgbSpeedSwitch;
10 | use crate::memory::mbcs::*;
11 | use crate::memory::ram::Ram;
12 | use crate::memory::rom::Rom;
13 | use crate::memory::vram::VRam;
14 | use crate::serial_cable::SerialCable;
15 | use crate::sound::apu::APU;
16 | use crate::{combine_u8, split_u16};
17 |
18 | #[cfg(not(feature = "std"))]
19 | use alloc::boxed::Box;
20 |
21 | // TODO: Rename this to something more appropriate
22 | // (I've seen an emu call a similar struct 'Interconnect')
23 | pub struct Memory {
24 | cgb_features: bool,
25 |
26 | mbc: Box,
27 |
28 | // TODO: Move VRAM to GPU?
29 | pub vram: VRam,
30 | // Includes all banks contiguously
31 | wram: Ram,
32 | // On DMG, this is always 1. On CGB, it's 1-7 inclusive
33 | upper_wram_bank: usize,
34 | hram: Ram,
35 | // Used in CGB mode only
36 | pub palette_ram: PaletteRam,
37 |
38 | serial_cable: SerialCable,
39 |
40 | timer_divider_increase: u16,
41 | timer_divider: u8,
42 |
43 | timer_counter_increase: u32,
44 | timer_counter: u8,
45 |
46 | timer_modulo: u8,
47 |
48 | timer_control: u8,
49 |
50 | pub joypad: Joypad,
51 |
52 | pub apu: APU,
53 | pub speed_switch: CgbSpeedSwitch,
54 | }
55 |
56 | impl Memory {
57 | // Memory has a step command for timers & MBCs
58 | pub fn step(
59 | &mut self,
60 | cycles: usize,
61 | ints: &mut Interrupts,
62 | ms_since_boot: usize,
63 | ) {
64 | // These two timers are safe to implement like this vs per-cycle
65 | // because the CPU will never do more than about 16 cycles per step,
66 | // let alone >256
67 | self.timer_divider_increase += cycles as u16;
68 | if self.timer_divider_increase >= 256 {
69 | self.timer_divider_increase -= 256;
70 | self.timer_divider = self.timer_divider.wrapping_add(1);
71 | }
72 |
73 | let enabled = (self.timer_control >> 2) == 1;
74 | if enabled {
75 | self.timer_counter_increase += cycles as u32;
76 |
77 | let step = match self.timer_control & 0b11 {
78 | 0b00 => 1024,
79 | 0b01 => 16,
80 | 0b10 => 64,
81 | 0b11 => 256,
82 | _ => unreachable!(),
83 | };
84 |
85 | while self.timer_counter_increase >= step {
86 | self.timer_counter = self.timer_counter.wrapping_add(1);
87 | if self.timer_counter == 0 {
88 | self.timer_counter = self.timer_modulo;
89 | ints.raise_interrupt(InterruptReason::Timer);
90 | }
91 | self.timer_counter_increase -= step;
92 | }
93 | }
94 |
95 | self.serial_cable.step(ints, cycles);
96 |
97 | self.mbc.step(ms_since_boot);
98 | }
99 |
100 | #[inline(always)]
101 | pub fn read(&self, ints: &Interrupts, gpu: &Gpu, address: u16) -> u8 {
102 | match address {
103 | // Cartridge memory starts at the 0 address
104 | 0..=MBC_ROM_END => self.mbc.read(address),
105 |
106 | VRAM_START..=VRAM_END => self.vram.raw_read(address),
107 |
108 | MBC_RAM_START..=MBC_RAM_END => {
109 | self.mbc.ram_read(address - MBC_RAM_START)
110 | },
111 |
112 | WRAM_LOWER_BANK_START..=WRAM_LOWER_BANK_END => {
113 | self.wram.read(address - WRAM_LOWER_BANK_START)
114 | },
115 | WRAM_UPPER_BANK_START..=WRAM_UPPER_BANK_END => {
116 | self.wram.bytes[self.upper_wram_bank * WRAM_BANK_SIZE
117 | + (address - WRAM_UPPER_BANK_START) as usize]
118 | },
119 | // TODO: How does upper echo RAM work with CGB bank switching?
120 | ECHO_RAM_START..=ECHO_RAM_END => self.read(
121 | ints,
122 | gpu,
123 | address - (ECHO_RAM_START - WRAM_LOWER_BANK_START),
124 | ),
125 |
126 | OAM_START..=OAM_END => gpu.raw_read(address),
127 |
128 | UNUSABLE_MEMORY_START..=UNUSABLE_MEMORY_END => 0xFF,
129 |
130 | LINK_CABLE_SB | LINK_CABLE_SC => self.serial_cable.read(address),
131 |
132 | APU_START..=APU_END => self.apu.read(address),
133 |
134 | LCD_DATA_START..=LCD_DATA_END => gpu.raw_read(address),
135 | CGB_DMA_START..=CGB_DMA_END => gpu.raw_read(address),
136 | CGB_PALETTE_DATA_START..=CGB_PALETTE_DATA_END => {
137 | self.palette_ram.raw_read(address)
138 | },
139 | HRAM_START..=HRAM_END => self.hram.read(address - HRAM_START),
140 |
141 | 0xFF00 => self.joypad.read(),
142 |
143 | // Timers
144 | 0xFF04 => self.timer_divider,
145 | 0xFF05 => self.timer_counter,
146 | 0xFF06 => self.timer_modulo,
147 | 0xFF07 => self.timer_control,
148 |
149 | 0xFF4D => self.speed_switch.read_switch_byte(),
150 |
151 | 0xFF4F => self.vram.bank as u8,
152 |
153 | 0xFF70 => self.upper_wram_bank as u8,
154 |
155 | INTERRUPT_ENABLE_ADDRESS => ints.enable_read(),
156 | INTERRUPT_FLAG_ADDRESS => ints.flag_read(),
157 |
158 | _ => {
159 | log!("Unsupported memory read at {:#06x}", address);
160 | 0xFF
161 | },
162 | }
163 | }
164 |
165 | #[inline(always)]
166 | // Function complexity warning here is due to the massive switch statement.
167 | // Such a thing is expected in an emulator.
168 | // skipcq: RS-R1000
169 | pub fn write(
170 | &mut self,
171 | ints: &mut Interrupts,
172 | gpu: &mut Gpu,
173 | address: u16,
174 | value: u8,
175 | ) {
176 | match address {
177 | 0..=MBC_ROM_END => self.mbc.write(address, value),
178 |
179 | // TODO: Disable writing to VRAM if GPU is reading it
180 | VRAM_START..=VRAM_END => self.vram.raw_write(address, value),
181 |
182 | MBC_RAM_START..=MBC_RAM_END => {
183 | self.mbc.ram_write(address - MBC_RAM_START, value)
184 | },
185 |
186 | WRAM_LOWER_BANK_START..=WRAM_LOWER_BANK_END => {
187 | self.wram.write(address - WRAM_LOWER_BANK_START, value)
188 | },
189 | // CGB WRAM is so big that upper bank addresses might not fit into a u16,
190 | // so we'll do this directly with a usize
191 | WRAM_UPPER_BANK_START..=WRAM_UPPER_BANK_END => {
192 | self.wram.bytes[self.upper_wram_bank * WRAM_BANK_SIZE
193 | + (address - WRAM_UPPER_BANK_START) as usize] = value
194 | },
195 | ECHO_RAM_START..=ECHO_RAM_END => self.write(
196 | ints,
197 | gpu,
198 | address - (ECHO_RAM_START - WRAM_LOWER_BANK_START),
199 | value,
200 | ),
201 |
202 | OAM_START..=OAM_END => gpu.raw_write(address, value, ints),
203 |
204 | // TETRIS writes here.. due to a bug
205 | UNUSABLE_MEMORY_START..=UNUSABLE_MEMORY_END => {},
206 |
207 | LINK_CABLE_SB | LINK_CABLE_SC => {
208 | self.serial_cable.write(address, value)
209 | },
210 |
211 | APU_START..=APU_END => self.apu.write(address, value),
212 |
213 | LCD_DATA_START..=LCD_DATA_END => {
214 | gpu.raw_write(address, value, ints)
215 | },
216 | CGB_DMA_START..=CGB_DMA_END => gpu.raw_write(address, value, ints),
217 | CGB_PALETTE_DATA_START..=CGB_PALETTE_DATA_END => {
218 | self.palette_ram.raw_write(address, value)
219 | },
220 | HRAM_START..=HRAM_END => {
221 | self.hram.write(address - HRAM_START, value)
222 | },
223 |
224 | 0xFF00 => self.joypad.write(value),
225 |
226 | // Timers
227 | 0xFF04 => self.timer_divider = 0,
228 | // NOTE: This goes to 0 when written to, not to value
229 | 0xFF05 => self.timer_counter = 0,
230 | 0xFF06 => self.timer_modulo = value,
231 | 0xFF07 => self.timer_control = value,
232 |
233 | 0xFF4D => self.speed_switch.write_switch_byte(value),
234 |
235 | // VRAM bank select
236 | 0xFF4F => self.vram.bank_write(value),
237 |
238 | // Upper WRAM bank select
239 | 0xFF70 => {
240 | if !self.cgb_features {
241 | return;
242 | }
243 | let mut desired_bank = value & 0x07;
244 | if desired_bank == 0 {
245 | desired_bank = 1;
246 | }
247 | self.upper_wram_bank = desired_bank as usize;
248 | },
249 |
250 | // TETRIS also writes here, Sameboy doesn't seem to care
251 | 0xFF7F => {},
252 |
253 | INTERRUPT_ENABLE_ADDRESS => ints.enable_write(value),
254 | INTERRUPT_FLAG_ADDRESS => ints.flag_write(value),
255 |
256 | _ => log!(
257 | "Unsupported memory write at {:#06x} (value: {:#04x})",
258 | address,
259 | value
260 | ),
261 | }
262 | }
263 |
264 | #[inline(always)]
265 | pub fn read_16(&self, ints: &Interrupts, gpu: &Gpu, address: u16) -> u16 {
266 | combine_u8!(
267 | self.read(ints, gpu, address + 1),
268 | self.read(ints, gpu, address)
269 | )
270 | }
271 | #[inline(always)]
272 | pub fn write_16(
273 | &mut self,
274 | ints: &mut Interrupts,
275 | gpu: &mut Gpu,
276 | address: u16,
277 | value: u16,
278 | ) {
279 | let (b1, b2) = split_u16!(value);
280 | self.write(ints, gpu, address, b1);
281 | self.write(ints, gpu, address + 1, b2);
282 | }
283 |
284 | pub fn from_info(
285 | cart_info: Cartridge,
286 | rom: Rom,
287 | target: &EmulationTarget,
288 | ) -> Memory {
289 | let cgb_features = target.has_cgb_features();
290 | Memory {
291 | cgb_features,
292 | mbc: mbc_from_info(cart_info, rom),
293 | vram: VRam::new(cgb_features),
294 | wram: Ram::new(WRAM_BANK_SIZE * 8),
295 | upper_wram_bank: 1,
296 | hram: Ram::new(HRAM_SIZE),
297 | palette_ram: PaletteRam::new(&target),
298 | serial_cable: SerialCable::new(),
299 | timer_divider_increase: 0,
300 | timer_divider: 0,
301 | timer_counter_increase: 0,
302 | timer_counter: 0,
303 | timer_control: 0b00000010,
304 | timer_modulo: 0,
305 | joypad: Joypad::new(),
306 | apu: APU::new(),
307 | speed_switch: CgbSpeedSwitch::new(cgb_features),
308 | }
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/core/src/memory/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod battery_backed_ram;
2 | pub mod cgb_speed_switch;
3 | pub mod mbcs;
4 | pub mod memory;
5 | pub mod ram;
6 | pub mod rom;
7 | pub mod vram;
8 |
--------------------------------------------------------------------------------
/core/src/memory/ram.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(feature = "std"))]
2 | use alloc::{vec, vec::Vec};
3 |
4 | pub struct Ram {
5 | pub bytes: Vec,
6 | pub size: usize,
7 | }
8 |
9 | impl Ram {
10 | #[inline(always)]
11 | pub fn read(&self, address: u16) -> u8 {
12 | self.bytes[address as usize]
13 | }
14 |
15 | #[inline(always)]
16 | pub fn write(&mut self, address: u16, value: u8) {
17 | self.bytes[address as usize] = value;
18 | }
19 |
20 | pub fn new(size: usize) -> Ram {
21 | Ram::with_filled_value(size, 0)
22 | }
23 |
24 | pub fn with_filled_value(size: usize, default_value: u8) -> Ram {
25 | Ram {
26 | bytes: vec![default_value; size],
27 | size,
28 | }
29 | }
30 |
31 | pub fn from_bytes(bytes: Vec, expected_size: usize) -> Ram {
32 | if bytes.len() != expected_size {
33 | panic!("Save file was not the expected length")
34 | }
35 |
36 | Ram {
37 | bytes,
38 | size: expected_size,
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/core/src/memory/rom.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "std")]
2 | use std::{fs::File, io::Read};
3 |
4 | #[cfg(not(feature = "std"))]
5 | use alloc::{string::String, vec::Vec};
6 |
7 | #[derive(Clone)]
8 | pub struct Rom {
9 | pub bytes: Vec,
10 | pub path: String,
11 | }
12 |
13 | impl Rom {
14 | #[inline(always)]
15 | pub fn read(&self, address: u16) -> u8 {
16 | self.bytes[address as usize]
17 | }
18 |
19 | #[cfg(feature = "std")]
20 | pub fn from_file(path: &str) -> Rom {
21 | let mut buffer = vec![];
22 | let mut file = File::open(path).expect("Invalid ROM path");
23 | file.read_to_end(&mut buffer)
24 | .expect("Unable to read ROM file");
25 |
26 | Rom {
27 | bytes: buffer,
28 | path: path.to_string(),
29 | }
30 | }
31 |
32 | pub fn from_bytes(bytes: Vec) -> Rom {
33 | Rom {
34 | bytes,
35 | path: String::default(),
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/src/memory/vram.rs:
--------------------------------------------------------------------------------
1 | use super::ram::Ram;
2 | use crate::colour::bg_map_attributes::BgMapAttributeTable;
3 | use crate::constants::*;
4 |
5 | pub struct VRam {
6 | cgb_features: bool,
7 | memory: Ram,
8 | pub bank: u16,
9 | pub bg_map_attributes: BgMapAttributeTable,
10 | }
11 |
12 | impl VRam {
13 | pub fn read_arbitrary_bank(&self, bank: u16, address: u16) -> u8 {
14 | let relative_address = address - VRAM_START;
15 | self.memory
16 | .read(bank * VRAM_BANK_SIZE as u16 + relative_address)
17 | }
18 |
19 | pub fn raw_read(&self, address: u16) -> u8 {
20 | if self.bank == 1 && address > VRAM_BG_MAP_START {
21 | return self.bg_map_attributes.read(address - VRAM_BG_MAP_START);
22 | }
23 |
24 | let relative_address = address - VRAM_START;
25 | self.memory
26 | .read(self.bank * VRAM_BANK_SIZE as u16 + relative_address)
27 | }
28 |
29 | pub fn raw_write(&mut self, address: u16, value: u8) {
30 | if self.bank == 1 && address > VRAM_BG_MAP_START {
31 | // Attribute table
32 | self.bg_map_attributes
33 | .write(address - VRAM_BG_MAP_START, value);
34 | return;
35 | }
36 |
37 | let relative_address = address - VRAM_START;
38 | self.memory
39 | .write(self.bank * VRAM_BANK_SIZE as u16 + relative_address, value)
40 | }
41 |
42 | pub fn bank_write(&mut self, value: u8) {
43 | if !self.cgb_features {
44 | return;
45 | }
46 | self.bank = value as u16 & 0x01;
47 | }
48 |
49 | pub fn new(cgb_features: bool) -> VRam {
50 | VRam {
51 | cgb_features,
52 | memory: Ram::new(VRAM_BANK_SIZE * 2),
53 | bank: 0,
54 | bg_map_attributes: BgMapAttributeTable::new(),
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/core/src/registers.rs:
--------------------------------------------------------------------------------
1 | use crate::cpu::EmulationTarget;
2 | use crate::gpu::Gpu;
3 | use crate::interrupts::*;
4 | use crate::memory::memory::Memory;
5 | use crate::{combine_u8, set_bit, split_u16};
6 |
7 | #[cfg(not(feature = "std"))]
8 | use alloc::{format, string::String};
9 |
10 | pub struct Registers {
11 | pub a: u8,
12 | pub b: u8,
13 | pub c: u8,
14 | pub d: u8,
15 | pub e: u8,
16 | pub f: u8,
17 | pub h: u8,
18 | pub l: u8,
19 |
20 | pub sp: u16,
21 | pub pc: u16,
22 | }
23 | impl Registers {
24 | fn set_flag(&mut self, flag_index: u8, bit: u8) {
25 | set_bit!(self.f, 4 + flag_index, bit);
26 | }
27 | pub fn set_carry_flag(&mut self, bit: u8) {
28 | self.set_flag(0, bit)
29 | }
30 | pub fn set_half_carry_flag(&mut self, bit: u8) {
31 | self.set_flag(1, bit)
32 | }
33 | pub fn set_operation_flag(&mut self, bit: u8) {
34 | self.set_flag(2, bit)
35 | }
36 | pub fn set_zero_flag(&mut self, bit: u8) {
37 | self.set_flag(3, bit)
38 | }
39 |
40 | pub fn set_flags(
41 | &mut self,
42 | zero: u8,
43 | operation: u8,
44 | half_carry: u8,
45 | carry: u8,
46 | ) {
47 | self.set_carry_flag(carry);
48 | self.set_half_carry_flag(half_carry);
49 | self.set_operation_flag(operation);
50 | self.set_zero_flag(zero);
51 | }
52 |
53 | fn get_flag(&self, flag_index: u8) -> u8 {
54 | (self.f >> (4 + flag_index)) & 0x1
55 | }
56 | pub fn get_carry_flag(&self) -> u8 {
57 | self.get_flag(0)
58 | }
59 | pub fn get_half_carry_flag(&self) -> u8 {
60 | self.get_flag(1)
61 | }
62 | pub fn get_operation_flag(&self) -> u8 {
63 | self.get_flag(2)
64 | }
65 | pub fn get_zero_flag(&self) -> u8 {
66 | self.get_flag(3)
67 | }
68 |
69 | pub fn get_af(&self) -> u16 {
70 | combine_u8!(self.a, self.f)
71 | }
72 | pub fn set_af(&mut self, value: u16) {
73 | let (b1, b2) = split_u16!(value);
74 | self.a = b2;
75 | self.f = b1 & 0xF0;
76 | }
77 |
78 | pub fn get_bc(&self) -> u16 {
79 | combine_u8!(self.b, self.c)
80 | }
81 | pub fn set_bc(&mut self, value: u16) {
82 | let (b1, b2) = split_u16!(value);
83 | self.b = b2;
84 | self.c = b1;
85 | }
86 |
87 | pub fn get_de(&self) -> u16 {
88 | combine_u8!(self.d, self.e)
89 | }
90 | pub fn set_de(&mut self, value: u16) {
91 | let (b1, b2) = split_u16!(value);
92 | self.d = b2;
93 | self.e = b1;
94 | }
95 |
96 | pub fn get_hl(&self) -> u16 {
97 | combine_u8!(self.h, self.l)
98 | }
99 | pub fn set_hl(&mut self, value: u16) {
100 | let (b1, b2) = split_u16!(value);
101 | self.h = b2;
102 | self.l = b1;
103 | }
104 |
105 | // These are left to right from the "GoldenCrystal Gameboy Z80 CPU Opcodes" PDF
106 | // The "sp" flag indicates whether 0b11 refers to the SP or AF
107 | #[inline(always)]
108 | fn set_combined_register_base(
109 | &mut self,
110 | register: u8,
111 | value: u16,
112 | sp: bool,
113 | ) {
114 | match register {
115 | 0b00 => self.set_bc(value),
116 | 0b01 => self.set_de(value),
117 | 0b10 => self.set_hl(value),
118 | 0b11 => {
119 | if sp {
120 | self.sp = value
121 | } else {
122 | self.set_af(value)
123 | }
124 | },
125 | _ => panic!("Invalid combined register set"),
126 | }
127 | }
128 | #[inline(always)]
129 | fn get_combined_register_base(&self, register: u8, sp: bool) -> u16 {
130 | match register {
131 | 0b00 => self.get_bc(),
132 | 0b01 => self.get_de(),
133 | 0b10 => self.get_hl(),
134 | 0b11 => {
135 | if sp {
136 | self.sp
137 | } else {
138 | self.get_af()
139 | }
140 | },
141 | _ => panic!("Invalid combined register get"),
142 | }
143 | }
144 |
145 | #[inline(always)]
146 | pub fn get_combined_register(&self, register: u8) -> u16 {
147 | self.get_combined_register_base(register, true)
148 | }
149 | #[inline(always)]
150 | pub fn set_combined_register(&mut self, register: u8, value: u16) {
151 | self.set_combined_register_base(register, value, true)
152 | }
153 | #[inline(always)]
154 | pub fn get_combined_register_alt(&self, register: u8) -> u16 {
155 | self.get_combined_register_base(register, false)
156 | }
157 | #[inline(always)]
158 | pub fn set_combined_register_alt(&mut self, register: u8, value: u16) {
159 | self.set_combined_register_base(register, value, false)
160 | }
161 |
162 | #[inline(always)]
163 | pub fn set_singular_register(
164 | &mut self,
165 | register: u8,
166 | value: u8,
167 | mem: &mut Memory,
168 | ints: &mut Interrupts,
169 | gpu: &mut Gpu,
170 | ) {
171 | match register {
172 | 0b000 => self.b = value,
173 | 0b001 => self.c = value,
174 | 0b010 => self.d = value,
175 | 0b011 => self.e = value,
176 | 0b100 => self.h = value,
177 | 0b101 => self.l = value,
178 | 0b110 => mem.write(ints, gpu, self.get_hl(), value),
179 | 0b111 => self.a = value,
180 | _ => panic!("Invalid singular register set"),
181 | }
182 | }
183 |
184 | #[inline(always)]
185 | pub fn get_singular_register(
186 | &self,
187 | register: u8,
188 | mem: &Memory,
189 | ints: &Interrupts,
190 | gpu: &Gpu,
191 | ) -> u8 {
192 | match register {
193 | 0b000 => self.b,
194 | 0b001 => self.c,
195 | 0b010 => self.d,
196 | 0b011 => self.e,
197 | 0b100 => self.h,
198 | 0b101 => self.l,
199 | 0b110 => mem.read(ints, gpu, self.get_hl()),
200 | 0b111 => self.a,
201 | _ => panic!("Invalid singular register get"),
202 | }
203 | }
204 |
205 | pub fn debug_dump(&self) -> String {
206 | format!(
207 | "AF: {:#06x} | BC: {:#06x} | DE: {:#06x} | HL: {:#06x}",
208 | self.get_af(),
209 | self.get_bc(),
210 | self.get_de(),
211 | self.get_hl()
212 | )
213 | }
214 |
215 | pub fn new(emulation_target: &EmulationTarget) -> Registers {
216 | // NOTE: These values are what's in the registers after the boot rom,
217 | // since we don't run that.
218 | // This is how games detect that they can use GameBoy Color features.
219 | let bootup_a_value = match emulation_target {
220 | EmulationTarget::Dmg | EmulationTarget::CgbDmgMode => 0x01,
221 | EmulationTarget::CgbCgbMode | EmulationTarget::GbaCgbMode => 0x11,
222 | };
223 | // This is exclusively used to detect running on the GameBoy Advance.
224 | let bootup_b_value = match emulation_target {
225 | EmulationTarget::GbaCgbMode => 0x01,
226 | _ => 0x00,
227 | };
228 | Registers {
229 | a: bootup_a_value,
230 | b: bootup_b_value,
231 | c: 0x13,
232 | d: 0x00,
233 | e: 0xD8,
234 | f: 0xB0,
235 | h: 0x01,
236 | l: 0x4D,
237 | sp: 0xFFFE,
238 | pc: 0x100,
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/core/src/serial_cable.rs:
--------------------------------------------------------------------------------
1 | // Gameboy Link Cable
2 | // Mostly a stub, although some parts have to be emulated *somewhat* accurately
3 | // to emulate fussy games like Alleyway
4 | use crate::constants::*;
5 | use crate::interrupts::{InterruptReason, Interrupts};
6 |
7 | // Unusual serial code inspired by
8 | // https://github.com/rvaccarim/FrozenBoy/blob/master/FrozenBoyCore/Serial/SerialLink.cs
9 |
10 | pub struct SerialCable {
11 | transfer_data_byte: u8,
12 | transfer_control_byte: u8,
13 |
14 | counter: usize,
15 | transfer_in_progress: bool,
16 | }
17 |
18 | impl SerialCable {
19 | pub fn read(&self, address: u16) -> u8 {
20 | match address {
21 | // When there's no gameboy on the other end, this apparently
22 | // just always reads 0xFF
23 | LINK_CABLE_SB => self.transfer_data_byte,
24 | LINK_CABLE_SC => self.transfer_control_byte | 0b1111_1110,
25 | _ => unreachable!(),
26 | }
27 | }
28 |
29 | pub fn write(&mut self, address: u16, value: u8) {
30 | match address {
31 | LINK_CABLE_SB => self.transfer_data_byte = value,
32 | LINK_CABLE_SC => {
33 | self.transfer_control_byte = value;
34 |
35 | if value == 0x81 {
36 | self.transfer_in_progress = true;
37 | self.counter = 0;
38 | }
39 | },
40 | _ => unreachable!(),
41 | }
42 | }
43 |
44 | pub fn step(&mut self, ints: &mut Interrupts, cycles: usize) {
45 | if !self.transfer_in_progress {
46 | return;
47 | }
48 |
49 | self.counter += cycles;
50 |
51 | if self.counter >= 512 {
52 | self.transfer_in_progress = false;
53 | self.transfer_data_byte = 0xFF;
54 | ints.raise_interrupt(InterruptReason::Serial);
55 | }
56 | }
57 |
58 | pub fn new() -> SerialCable {
59 | SerialCable {
60 | transfer_data_byte: 0,
61 | transfer_control_byte: 0,
62 |
63 | counter: 0,
64 | transfer_in_progress: false,
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/core/src/sound/apu.rs:
--------------------------------------------------------------------------------
1 | use super::channel1::APUChannel1;
2 | use super::channel2::APUChannel2;
3 | use super::channel3::APUChannel3;
4 | use super::channel4::APUChannel4;
5 | use super::registers::*;
6 | use crate::constants::*;
7 |
8 | pub trait APUChannel {
9 | fn step(&mut self);
10 | fn sample(&self) -> f32;
11 | fn read(&self, address: u16) -> u8;
12 | fn write(&mut self, address: u16, value: u8);
13 | }
14 |
15 | // Audio processing unit
16 | // NOTE: Max APU frequency seems to be 131072 Hz
17 | pub struct APU {
18 | pub stereo_left_volume: f32,
19 | pub stereo_right_volume: f32,
20 |
21 | pub stereo_panning: StereoPanning,
22 |
23 | pub sound_on_register: u8,
24 |
25 | pub channel1: APUChannel1,
26 | pub channel2: APUChannel2,
27 | pub channel3: APUChannel3,
28 | pub channel4: APUChannel4,
29 |
30 | pub sample_counter: usize,
31 | // This could be a Vec that we check len() against, but we can save the
32 | // allocation because we know the size it's always going to be.
33 | pub buffer: [i16; SOUND_BUFFER_SIZE],
34 | pub buffer_idx: usize,
35 | pub buffer_full: bool,
36 | }
37 |
38 | impl APU {
39 | pub fn step(&mut self) {
40 | self.channel1.step();
41 | self.channel2.step();
42 | self.channel3.step();
43 | self.channel4.step();
44 |
45 | self.sample_counter += 1;
46 |
47 | if self.sample_counter == APU_SAMPLE_CLOCKS {
48 | self.sample_counter = 0;
49 | self.sample();
50 | }
51 | }
52 |
53 | pub fn sample(&mut self) {
54 | let mut left_sample = 0.;
55 | let mut right_sample = 0.;
56 |
57 | // TODO: Maybe we could generate these with a macro?
58 | let chan1 = self.channel1.sample();
59 | if self.stereo_panning.channel1_left {
60 | left_sample += chan1;
61 | }
62 | if self.stereo_panning.channel1_right {
63 | right_sample += chan1;
64 | }
65 |
66 | let chan2 = self.channel2.sample();
67 | if self.stereo_panning.channel2_left {
68 | left_sample += chan2;
69 | }
70 | if self.stereo_panning.channel2_right {
71 | right_sample += chan2;
72 | }
73 |
74 | let chan3 = self.channel3.sample();
75 | if self.stereo_panning.channel3_left {
76 | left_sample += chan3;
77 | }
78 | if self.stereo_panning.channel3_right {
79 | right_sample += chan3;
80 | }
81 |
82 | let chan4 = self.channel4.sample();
83 | if self.stereo_panning.channel4_left {
84 | left_sample += chan4;
85 | }
86 | if self.stereo_panning.channel4_right {
87 | right_sample += chan4;
88 | }
89 |
90 | // Average the 4 channels
91 | left_sample /= 4.;
92 | right_sample /= 4.;
93 |
94 | // Adjust for soft-panning
95 | left_sample *= self.stereo_left_volume;
96 | right_sample *= self.stereo_right_volume;
97 |
98 | let left_sample_int = (left_sample * 30_000.) as i16;
99 | let right_sample_int = (right_sample * 30_000.) as i16;
100 |
101 | self.buffer[self.buffer_idx] = left_sample_int;
102 | self.buffer_idx += 1;
103 | self.buffer[self.buffer_idx] = right_sample_int;
104 | self.buffer_idx += 1;
105 |
106 | if self.buffer_idx == SOUND_BUFFER_SIZE {
107 | self.buffer_idx = 0;
108 | self.buffer_full = true;
109 | }
110 | }
111 |
112 | #[allow(unused_variables)]
113 | #[allow(unreachable_code)]
114 | pub fn read(&self, address: u16) -> u8 {
115 | #[cfg(not(feature = "sound"))]
116 | return 0;
117 |
118 | match address {
119 | 0xFF24 => self.serialise_nr50(),
120 | 0xFF25 => u8::from(self.stereo_panning.clone()),
121 | 0xFF26 => self.sound_on_register,
122 |
123 | 0xFF10..=0xFF14 => self.channel1.read(address),
124 | 0xFF16..=0xFF19 => self.channel2.read(address),
125 | 0xFF1A..=0xFF1E => self.channel3.read(address),
126 | 0xFF20..=0xFF23 => self.channel4.read(address),
127 |
128 | WAVE_RAM_START..=WAVE_RAM_END => self.channel3.read(address),
129 | _ => 0, //panic!("Unknown read {:#06x} in APU", address)
130 | }
131 | }
132 |
133 | #[allow(unused_variables)]
134 | #[allow(unreachable_code)]
135 | pub fn write(&mut self, address: u16, value: u8) {
136 | #[cfg(not(feature = "sound"))]
137 | return;
138 |
139 | match address {
140 | 0xFF24 => self.deserialise_nr50(value),
141 | 0xFF25 => self.stereo_panning = StereoPanning::from(value),
142 | 0xFF26 => self.sound_on_register = value,
143 |
144 | 0xFF10..=0xFF14 => self.channel1.write(address, value),
145 | 0xFF16..=0xFF19 => self.channel2.write(address, value),
146 | 0xFF1A..=0xFF1E => self.channel3.write(address, value),
147 | 0xFF20..=0xFF23 => self.channel4.write(address, value),
148 |
149 | WAVE_RAM_START..=WAVE_RAM_END => {
150 | self.channel3.write(address, value)
151 | },
152 | _ => {}, //log!("Unknown write {:#06x} (value: {:#04}) in APU", address, value)
153 | }
154 | }
155 |
156 | // NOTE: These functions don't take into account the
157 | // Vin output flags. That feature is unused in all
158 | // commercial Gameboy games, so we ignore it.
159 | fn deserialise_nr50(&mut self, nr50: u8) {
160 | let right_vol = nr50 & 0b111;
161 | let left_vol = (nr50 & 0b111_0_000) >> 4;
162 |
163 | self.stereo_left_volume = (left_vol as f32) / 7.;
164 | self.stereo_right_volume = (right_vol as f32) / 7.;
165 | }
166 | fn serialise_nr50(&self) -> u8 {
167 | // These might turn out 1 level too low because of float flooring
168 | // TODO: Test this
169 | let right_vol = (self.stereo_right_volume * 7.) as u8;
170 | let left_vol = (self.stereo_left_volume * 7.) as u8;
171 |
172 | (left_vol << 4) & right_vol
173 | }
174 |
175 | pub fn new() -> APU {
176 | APU {
177 | // These might be meant to start 0, not sure
178 | stereo_left_volume: 1.,
179 | stereo_right_volume: 1.,
180 | stereo_panning: StereoPanning::from(0),
181 | sound_on_register: 0,
182 |
183 | channel1: APUChannel1::new(),
184 | channel2: APUChannel2::new(),
185 | channel3: APUChannel3::new(),
186 | channel4: APUChannel4::new(),
187 |
188 | sample_counter: 0,
189 | buffer: [0; SOUND_BUFFER_SIZE],
190 | buffer_idx: 0,
191 | buffer_full: false,
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/core/src/sound/channel1.rs:
--------------------------------------------------------------------------------
1 | use super::apu::APUChannel;
2 | use super::length_function::LengthFunction;
3 | use super::volume_envelope::VolumeEnvelope;
4 |
5 | const WAVEFORM_TABLE: [u8; 4] =
6 | [0b00000001, 0b00000011, 0b00001111, 0b11111100];
7 |
8 | // Doing something every 32k cycles is roughly a 128Hz clock.
9 | const SWEEP_CLOCKS: usize = 32_768;
10 |
11 | #[derive(PartialEq)]
12 | enum SweepDirection {
13 | Up,
14 | Down,
15 | }
16 |
17 | // TODO: Refactor to share code with APUChannel2
18 | // They are extremely similar to one another minus the sweep register.
19 | pub struct APUChannel1 {
20 | enabled: bool,
21 | frequency: usize,
22 | frequency_timer: usize,
23 | wave_duty: usize,
24 | wave_duty_position: usize,
25 | volume_envelope: VolumeEnvelope,
26 | length_function: LengthFunction,
27 | shadow_frequency: usize,
28 | shadow_frequency_shift: usize,
29 | sweep_enabled: bool,
30 | sweep_direction: SweepDirection,
31 | sweep_period: usize,
32 | sweep_timer: usize,
33 | sweep_frame_sequencer: usize,
34 | }
35 |
36 | impl APUChannel1 {
37 | pub fn new() -> APUChannel1 {
38 | APUChannel1 {
39 | enabled: false,
40 | frequency: 0,
41 | frequency_timer: 1,
42 | wave_duty: 2,
43 | wave_duty_position: 0,
44 | volume_envelope: VolumeEnvelope::new(),
45 | length_function: LengthFunction::new(),
46 | shadow_frequency: 0,
47 | shadow_frequency_shift: 0,
48 | sweep_enabled: false,
49 | sweep_direction: SweepDirection::Down,
50 | sweep_period: 0,
51 | sweep_timer: 1,
52 | sweep_frame_sequencer: 0,
53 | }
54 | }
55 |
56 | // This is called when a game writes a 1 in bit 7 of the NR24 register.
57 | // That means the game is issuing a "restart sound" command
58 | fn restart_triggered(&mut self) {
59 | self.volume_envelope.restart_triggered();
60 | self.length_function.restart_triggered();
61 | self.length_function.channel_enabled = true;
62 | self.enabled = true;
63 |
64 | self.shadow_frequency = self.frequency;
65 | self.sweep_timer = if self.sweep_period == 0 {
66 | 8
67 | } else {
68 | self.sweep_period
69 | };
70 | if self.sweep_period > 0 || self.shadow_frequency_shift > 0 {
71 | self.sweep_enabled = true;
72 | }
73 | if self.shadow_frequency_shift > 0 {
74 | self.calculate_sweep_frequency();
75 | }
76 | // TODO: Restarting a tone channel resets its frequency_timer to
77 | // (2048 - frequency) * 4... I think.
78 | }
79 |
80 | fn calculate_sweep_frequency(&mut self) -> usize {
81 | let mut new_frequency =
82 | self.shadow_frequency >> self.shadow_frequency_shift;
83 |
84 | if self.sweep_direction == SweepDirection::Down {
85 | new_frequency = self.shadow_frequency - new_frequency;
86 | } else {
87 | new_frequency = self.shadow_frequency + new_frequency;
88 | }
89 |
90 | if new_frequency > 2047 {
91 | self.enabled = false;
92 | // TODO: Do we nee to cap it here? I'm pretty sure this wasn't a 64-bit
93 | // value on Gameboy.
94 | }
95 |
96 | new_frequency
97 | }
98 |
99 | fn sweep_clock(&mut self) {
100 | if self.sweep_timer > 0 {
101 | self.sweep_timer -= 1;
102 |
103 | if self.sweep_timer == 0 {
104 | self.sweep_timer = if self.sweep_period == 0 {
105 | 8
106 | } else {
107 | self.sweep_period
108 | };
109 |
110 | if self.sweep_enabled && self.sweep_period != 0 {
111 | let new_frequency = self.calculate_sweep_frequency();
112 |
113 | if new_frequency <= 2047 && self.shadow_frequency_shift > 0
114 | {
115 | // println!("Setting a new frequency!");
116 | self.frequency = new_frequency;
117 | self.shadow_frequency = new_frequency;
118 |
119 | self.calculate_sweep_frequency();
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
127 | impl APUChannel for APUChannel1 {
128 | fn step(&mut self) {
129 | // TODO: I think the Frame Sequencer timers should still be ticking even
130 | // if this channel is not enabled. The Frame Sequencer exists outside
131 | // the channel.
132 | if !self.length_function.channel_enabled {
133 | return;
134 | }
135 |
136 | self.frequency_timer -= 1;
137 |
138 | if self.frequency_timer == 0 {
139 | self.frequency_timer = (2048 - self.frequency) * 4;
140 |
141 | // Wrapping pointer into the bits of the WAVEFORM_TABLE value
142 | self.wave_duty_position += 1;
143 | if self.wave_duty_position == 8 {
144 | self.wave_duty_position = 0
145 | }
146 | }
147 |
148 | self.sweep_frame_sequencer += 1;
149 | if self.sweep_frame_sequencer == SWEEP_CLOCKS {
150 | self.sweep_frame_sequencer = 0;
151 | self.sweep_clock();
152 | }
153 |
154 | self.volume_envelope.step();
155 | self.length_function.step();
156 | }
157 |
158 | fn read(&self, address: u16) -> u8 {
159 | match address {
160 | _ => 0, //panic!("Unimplemented APU Channel 2 read {:#06x}", address)
161 | }
162 | }
163 |
164 | fn write(&mut self, address: u16, value: u8) {
165 | match address {
166 | 0xFF10 => {
167 | // NOTE: This bit is upside down according to Pan Docs. Slightly odd
168 | // hardware quirk.
169 | let sweep_down = (value & 0b0000_1000) > 0;
170 | self.sweep_direction = if sweep_down {
171 | SweepDirection::Down
172 | } else {
173 | SweepDirection::Up
174 | };
175 |
176 | let sweep_shift = (value & 0b0000_0111) as usize;
177 | self.shadow_frequency_shift = sweep_shift;
178 |
179 | let sweep_period = (value & 0b0111_0000) >> 4;
180 | self.sweep_period = sweep_period as usize;
181 | },
182 | 0xFF11 => {
183 | let wave_duty = (value & 0b1100_0000) >> 6;
184 | let length = value & 0b0011_1111;
185 | self.wave_duty = wave_duty as usize;
186 | // TODO: Is there a way we change this into a generic register_write
187 | // function for LengthFunction?
188 | self.length_function.data = length as usize;
189 | },
190 | 0xFF12 => self.volume_envelope.register_write(value),
191 | 0xFF13 => {
192 | // This register sets the bottom 8 bits of the 11-bit
193 | // frequency register.
194 | self.frequency =
195 | (self.frequency & 0b111_0000_0000) | value as usize;
196 | },
197 | 0xFF14 => {
198 | // Among other things, this register sets the top 3 bits
199 | // of the 11-bit frequency register.
200 | let frequency_bits = value & 0b0000_0111;
201 | self.frequency = (self.frequency & 0b000_1111_1111)
202 | | ((frequency_bits as usize) << 8);
203 |
204 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0;
205 |
206 | if (value & 0b1000_0000) > 0 {
207 | self.restart_triggered();
208 | }
209 | },
210 | _ => unreachable!(),
211 | }
212 | }
213 |
214 | fn sample(&self) -> f32 {
215 | if !self.length_function.channel_enabled {
216 | return 0.;
217 | }
218 | if !self.enabled {
219 | return 0.;
220 | }
221 |
222 | let wave_pattern = WAVEFORM_TABLE[self.wave_duty];
223 | let amplitude_bit = (wave_pattern & (1 << self.wave_duty_position))
224 | >> self.wave_duty_position;
225 |
226 | let dac_input = amplitude_bit as usize * self.volume_envelope.volume;
227 | // The DAC in the Gameboy outputs between -1.0 and 1.0
228 | (dac_input as f32 / 7.5) - 1.0
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/core/src/sound/channel2.rs:
--------------------------------------------------------------------------------
1 | use super::apu::APUChannel;
2 | use super::length_function::LengthFunction;
3 | use super::volume_envelope::VolumeEnvelope;
4 |
5 | const WAVEFORM_TABLE: [u8; 4] =
6 | [0b00000001, 0b00000011, 0b00001111, 0b11111100];
7 |
8 | pub struct APUChannel2 {
9 | frequency: usize,
10 | frequency_timer: usize,
11 | wave_duty: usize,
12 | wave_duty_position: usize,
13 | volume_envelope: VolumeEnvelope,
14 | length_function: LengthFunction,
15 | }
16 |
17 | impl APUChannel2 {
18 | pub fn new() -> APUChannel2 {
19 | APUChannel2 {
20 | frequency: 0,
21 | frequency_timer: 1,
22 | wave_duty: 2,
23 | wave_duty_position: 0,
24 | volume_envelope: VolumeEnvelope::new(),
25 | length_function: LengthFunction::new(),
26 | }
27 | }
28 |
29 | // This is called when a game writes a 1 in bit 7 of the NR24 register.
30 | // That means the game is issuing a "restart sound" command
31 | fn restart_triggered(&mut self) {
32 | self.volume_envelope.restart_triggered();
33 | self.length_function.restart_triggered();
34 | self.length_function.channel_enabled = true;
35 | // TODO: Restarting a tone channel resets its frequency_timer to
36 | // (2048 - frequency) * 4... I think.
37 | }
38 | }
39 |
40 | impl APUChannel for APUChannel2 {
41 | fn step(&mut self) {
42 | // TODO: I think the Frame Sequencer timers should still be ticking even
43 | // if this channel is not enabled. The Frame Sequencer exists outside
44 | // the channel.
45 | if !self.length_function.channel_enabled {
46 | return;
47 | }
48 |
49 | self.frequency_timer -= 1;
50 |
51 | if self.frequency_timer == 0 {
52 | self.frequency_timer = (2048 - self.frequency) * 4;
53 |
54 | // Wrapping pointer into the bits of the WAVEFORM_TABLE value
55 | self.wave_duty_position += 1;
56 | if self.wave_duty_position == 8 {
57 | self.wave_duty_position = 0
58 | }
59 | }
60 |
61 | self.volume_envelope.step();
62 | self.length_function.step();
63 | }
64 |
65 | fn read(&self, address: u16) -> u8 {
66 | match address {
67 | _ => 0, //panic!("Unimplemented APU Channel 2 read {:#06x}", address)
68 | }
69 | }
70 |
71 | fn write(&mut self, address: u16, value: u8) {
72 | match address {
73 | 0xFF16 => {
74 | let wave_duty = (value & 0b1100_0000) >> 6;
75 | let length = value & 0b0011_1111;
76 | self.wave_duty = wave_duty as usize;
77 | // TODO: Is there a way we change this into a generic register_write
78 | // function for LengthFunction?
79 | self.length_function.data = length as usize;
80 | },
81 | 0xFF17 => self.volume_envelope.register_write(value),
82 | 0xFF18 => {
83 | // This register sets the bottom 8 bits of the 11-bit
84 | // frequency register.
85 | self.frequency =
86 | (self.frequency & 0b111_0000_0000) | value as usize;
87 | },
88 | 0xFF19 => {
89 | // Among other things, this register sets the top 3 bits
90 | // of the 11-bit frequency register.
91 | let frequency_bits = value & 0b0000_0111;
92 | self.frequency = (self.frequency & 0b000_1111_1111)
93 | | ((frequency_bits as usize) << 8);
94 |
95 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0;
96 |
97 | if (value & 0b1000_0000) > 0 {
98 | self.restart_triggered();
99 | }
100 | },
101 | _ => unreachable!(),
102 | }
103 | }
104 |
105 | fn sample(&self) -> f32 {
106 | if !self.length_function.channel_enabled {
107 | return 0.;
108 | }
109 |
110 | let wave_pattern = WAVEFORM_TABLE[self.wave_duty];
111 | let amplitude_bit = (wave_pattern & (1 << self.wave_duty_position))
112 | >> self.wave_duty_position;
113 |
114 | let dac_input = amplitude_bit as usize * self.volume_envelope.volume;
115 | // The DAC in the Gameboy outputs between -1.0 and 1.0
116 | (dac_input as f32 / 7.5) - 1.0
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/core/src/sound/channel3.rs:
--------------------------------------------------------------------------------
1 | use super::apu::APUChannel;
2 | use super::length_function::LengthFunction;
3 | use crate::constants::*;
4 | use crate::memory::ram::Ram;
5 |
6 | pub struct APUChannel3 {
7 | frequency: usize,
8 | frequency_timer: usize,
9 | // TODO: Is this actually the same bool as the enable within length_function?
10 | master_enable: bool,
11 | length_function: LengthFunction,
12 | wave_ram: Ram,
13 | wave_ram_ptr: usize,
14 | volume_shift: u8,
15 | }
16 |
17 | impl APUChannel3 {
18 | pub fn new() -> APUChannel3 {
19 | APUChannel3 {
20 | frequency: 0,
21 | frequency_timer: 1,
22 | master_enable: false,
23 | length_function: LengthFunction::new(),
24 | wave_ram: Ram::new(WAVE_RAM_SIZE),
25 | wave_ram_ptr: 0,
26 | volume_shift: 0,
27 | }
28 | }
29 |
30 | fn restart_triggered(&mut self) {
31 | self.length_function.restart_triggered();
32 | self.length_function.channel_enabled = true;
33 | self.wave_ram_ptr = 0;
34 | }
35 | }
36 |
37 | impl APUChannel for APUChannel3 {
38 | fn step(&mut self) {
39 | if !self.length_function.channel_enabled {
40 | return;
41 | }
42 |
43 | self.frequency_timer -= 1;
44 |
45 | if self.frequency_timer == 0 {
46 | self.frequency_timer = (2048 - self.frequency) * 2;
47 |
48 | // NOTE: This points to a *nibble* of Wave RAM, not a byte.
49 | self.wave_ram_ptr += 1;
50 | if self.wave_ram_ptr == 32 {
51 | self.wave_ram_ptr = 0;
52 | }
53 | }
54 |
55 | self.length_function.step();
56 | }
57 |
58 | fn read(&self, address: u16) -> u8 {
59 | match address {
60 | 0xFF1A => (self.master_enable as u8) >> 7,
61 | WAVE_RAM_START..=WAVE_RAM_END => {
62 | self.wave_ram.read(address - WAVE_RAM_START)
63 | },
64 | _ => 0, //panic!("Unimplemented APU Channel 3 read {:#06x}", address)
65 | }
66 | }
67 |
68 | fn write(&mut self, address: u16, value: u8) {
69 | match address {
70 | 0xFF1A => {
71 | self.master_enable = (value & 0b1000_0000) > 0;
72 | },
73 | 0xFF1B => {
74 | self.length_function.data = value as usize;
75 | },
76 | 0xFF1C => {
77 | self.volume_shift = (value & 0b0110_0000) >> 5;
78 | },
79 | 0xFF1D => {
80 | self.frequency =
81 | (self.frequency & 0b111_0000_0000) | value as usize;
82 | },
83 | 0xFF1E => {
84 | let frequency_bits = value & 0b0000_0111;
85 | self.frequency = (self.frequency & 0b000_1111_1111)
86 | | ((frequency_bits as usize) << 8);
87 |
88 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0;
89 |
90 | if (value & 0b1000_0000) > 0 {
91 | self.restart_triggered();
92 | }
93 | },
94 | WAVE_RAM_START..=WAVE_RAM_END => {
95 | self.wave_ram.write(address - WAVE_RAM_START, value)
96 | },
97 | _ => unreachable!(),
98 | }
99 | }
100 |
101 | fn sample(&self) -> f32 {
102 | if !self.length_function.channel_enabled {
103 | return 0.;
104 | }
105 |
106 | // This implementation is a bit guessed for now :)
107 | // Documentation on Channel 3 seems a little bit thin
108 | let wave_byte = self.wave_ram.bytes[self.wave_ram_ptr / 2];
109 | let mut wave_nibble = if self.wave_ram_ptr % 2 == 0 {
110 | wave_byte & 0x0F
111 | } else {
112 | wave_byte >> 4
113 | };
114 |
115 | wave_nibble = wave_nibble
116 | >> match self.volume_shift {
117 | 0 => 4,
118 | 1 => 0,
119 | 2 => 1,
120 | 3 => 2,
121 | _ => unreachable!(),
122 | };
123 |
124 | (wave_nibble as f32 / 7.5) - 1.0
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/core/src/sound/channel4.rs:
--------------------------------------------------------------------------------
1 | use super::apu::APUChannel;
2 | use super::length_function::LengthFunction;
3 | use super::volume_envelope::VolumeEnvelope;
4 |
5 | pub struct APUChannel4 {
6 | // TODO: Size these better. Maybe u32 rather than usize?
7 | // Not super important at all, but just to be sure.
8 | frequency_timer: usize,
9 | length_function: LengthFunction,
10 | volume_envelope: VolumeEnvelope,
11 | // Linear Feedback Shift Register
12 | // NOTE: The LFSR is 15-bits wide on the Gameboy. We'll use a 16-bit type
13 | // to represent it.
14 | lfsr: u16,
15 | // TODO: Is this a u8? Does shifting large-ish numbers overflow?
16 | divisor_shift: usize,
17 | half_width_mode: bool,
18 | divisor_code: usize,
19 | }
20 |
21 | impl APUChannel4 {
22 | pub fn new() -> APUChannel4 {
23 | APUChannel4 {
24 | frequency_timer: 1,
25 | length_function: LengthFunction::new(),
26 | volume_envelope: VolumeEnvelope::new(),
27 | lfsr: 0,
28 | divisor_shift: 0,
29 | half_width_mode: false,
30 | divisor_code: 0,
31 | }
32 | }
33 |
34 | fn restart_triggered(&mut self) {
35 | self.length_function.restart_triggered();
36 | self.volume_envelope.restart_triggered();
37 | self.lfsr = 0b0111_1111_1111_1111;
38 | }
39 |
40 | fn get_divisor(&self) -> usize {
41 | match self.divisor_code {
42 | 0 => 8,
43 | 1 => 16,
44 | 2 => 32,
45 | 3 => 48,
46 | 4 => 64,
47 | 5 => 80,
48 | 6 => 96,
49 | 7 => 112,
50 | _ => unreachable!(),
51 | }
52 | }
53 | }
54 |
55 | impl APUChannel for APUChannel4 {
56 | fn step(&mut self) {
57 | if !self.length_function.channel_enabled {
58 | return;
59 | }
60 |
61 | self.frequency_timer -= 1;
62 |
63 | if self.frequency_timer == 0 {
64 | self.frequency_timer = self.get_divisor() << self.divisor_shift;
65 |
66 | // Pseudo-random white noise generation
67 | let xor = (self.lfsr & 0b01) ^ ((self.lfsr & 0b10) >> 1);
68 | self.lfsr = (self.lfsr >> 1) | (xor << 14);
69 |
70 | if self.half_width_mode {
71 | self.lfsr = (self.lfsr & 0b0011_1111) | (xor << 6);
72 | }
73 | }
74 |
75 | self.volume_envelope.step();
76 | self.length_function.step();
77 | }
78 |
79 | fn read(&self, address: u16) -> u8 {
80 | match address {
81 | _ => 0, //panic!("Unimplemented APU Channel 4 read {:#06x}", address)
82 | }
83 | }
84 |
85 | fn write(&mut self, address: u16, value: u8) {
86 | match address {
87 | 0xFF20 => {
88 | let length = value & 0b0011_1111;
89 | self.length_function.data = length as usize;
90 | },
91 | 0xFF21 => self.volume_envelope.register_write(value),
92 | 0xFF22 => {
93 | // Polynomial register
94 | self.divisor_shift = ((value & 0b1111_0000) >> 4) as usize;
95 | self.half_width_mode = (value & 0b0000_1000) > 0;
96 | self.divisor_code = (value & 0b0000_0111) as usize;
97 | },
98 | 0xFF23 => {
99 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0;
100 |
101 | if (value & 0b1000_0000) > 0 {
102 | self.restart_triggered();
103 | }
104 | },
105 | _ => unreachable!(),
106 | }
107 | }
108 |
109 | fn sample(&self) -> f32 {
110 | if !self.length_function.channel_enabled {
111 | return 0.;
112 | }
113 |
114 | let lfsr_bit = !(self.lfsr) & 1;
115 |
116 | let dac_input = lfsr_bit as usize * self.volume_envelope.volume;
117 | (dac_input as f32 / 7.5) - 1.0
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/core/src/sound/length_function.rs:
--------------------------------------------------------------------------------
1 | // 256Hz
2 | const LENGTH_CLOCKS: usize = 16_392;
3 |
4 | pub struct LengthFunction {
5 | pub channel_enabled: bool,
6 | pub data: usize,
7 | pub timer_enabled: bool,
8 | timer: usize,
9 | clock_timer: usize,
10 | }
11 |
12 | impl LengthFunction {
13 | pub fn step(&mut self) {
14 | self.clock_timer += 1;
15 | if self.clock_timer == LENGTH_CLOCKS {
16 | self.clock_timer = 0;
17 | self.clock();
18 | }
19 | }
20 |
21 | pub fn restart_triggered(&mut self) {
22 | // TODO: This behaviour isn't quite right
23 | // https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Trigger_Event
24 | self.timer = 64;
25 | self.timer = self.timer.saturating_sub(self.data);
26 | }
27 |
28 | // Called at 256Hz
29 | fn clock(&mut self) {
30 | if self.timer > 0 {
31 | self.timer -= 1;
32 | }
33 |
34 | if self.timer == 0 {
35 | if self.timer_enabled {
36 | // TODO: Without saturating_sub, this causes panics after moving to
37 | // 48KHz audio when flapping with the rabbit ears in Mario Land 2
38 | self.timer = 64;
39 | self.timer = self.timer.saturating_sub(self.data);
40 | self.channel_enabled = false;
41 | }
42 | }
43 | }
44 |
45 | pub fn new() -> LengthFunction {
46 | LengthFunction {
47 | channel_enabled: true,
48 | timer: 0,
49 | data: 0,
50 | timer_enabled: false,
51 | clock_timer: 0,
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/sound/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod apu;
2 | pub mod channel1;
3 | pub mod channel2;
4 | pub mod channel3;
5 | pub mod channel4;
6 | pub mod length_function;
7 | pub mod registers;
8 | pub mod volume_envelope;
9 |
--------------------------------------------------------------------------------
/core/src/sound/registers.rs:
--------------------------------------------------------------------------------
1 | #[derive(Clone)]
2 | pub struct StereoPanning {
3 | pub channel1_left: bool,
4 | pub channel1_right: bool,
5 | pub channel2_left: bool,
6 | pub channel2_right: bool,
7 | pub channel3_left: bool,
8 | pub channel3_right: bool,
9 | pub channel4_left: bool,
10 | pub channel4_right: bool,
11 | }
12 |
13 | impl From for StereoPanning {
14 | fn from(n: u8) -> StereoPanning {
15 | StereoPanning {
16 | channel4_left: (n & 0b1000_0000) > 0,
17 | channel3_left: (n & 0b0100_0000) > 0,
18 | channel2_left: (n & 0b0010_0000) > 0,
19 | channel1_left: (n & 0b0001_0000) > 0,
20 | channel4_right: (n & 0b0000_1000) > 0,
21 | channel3_right: (n & 0b0000_0100) > 0,
22 | channel2_right: (n & 0b0000_0010) > 0,
23 | channel1_right: (n & 0b0000_0001) > 0,
24 | }
25 | }
26 | }
27 |
28 | impl From for u8 {
29 | fn from(stereo: StereoPanning) -> u8 {
30 | (stereo.channel1_right as u8)
31 | | ((stereo.channel2_right as u8) << 1)
32 | | ((stereo.channel3_right as u8) << 2)
33 | | ((stereo.channel4_right as u8) << 3)
34 | | ((stereo.channel1_left as u8) << 4)
35 | | ((stereo.channel2_left as u8) << 5)
36 | | ((stereo.channel3_left as u8) << 6)
37 | | ((stereo.channel4_left as u8) << 7)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/core/src/sound/volume_envelope.rs:
--------------------------------------------------------------------------------
1 | #[derive(PartialEq)]
2 | enum EnvelopeDirection {
3 | Up,
4 | Down,
5 | }
6 |
7 | // Doing something every 65k cycles is roughly a 64Hz clock.
8 | const ENV_CLOCKS: usize = 65_536;
9 |
10 | pub struct VolumeEnvelope {
11 | initial_volume: usize,
12 | direction: EnvelopeDirection,
13 | sweep_period: usize,
14 | period_timer: usize,
15 | volume_timer: usize,
16 | pub volume: usize,
17 | }
18 |
19 | impl VolumeEnvelope {
20 | pub fn step(&mut self) {
21 | self.volume_timer += 1;
22 | if self.volume_timer == ENV_CLOCKS {
23 | self.volume_timer = 0;
24 | self.clock();
25 | }
26 | }
27 |
28 | pub fn restart_triggered(&mut self) {
29 | self.period_timer = self.sweep_period;
30 | self.volume = self.initial_volume;
31 | }
32 |
33 | pub fn register_write(&mut self, value: u8) {
34 | self.initial_volume = (value as usize & 0b1111_0000) >> 4;
35 | self.direction = if (value & 0b0000_1000) > 0 {
36 | EnvelopeDirection::Up
37 | } else {
38 | EnvelopeDirection::Down
39 | };
40 | self.sweep_period = value as usize & 0b0000_0111;
41 | }
42 |
43 | // Called at 64Hz
44 | fn clock(&mut self) {
45 | if self.sweep_period == 0 {
46 | return;
47 | }
48 |
49 | if self.period_timer > 0 {
50 | self.period_timer -= 1;
51 | }
52 |
53 | if self.period_timer == 0 {
54 | self.period_timer = self.sweep_period;
55 |
56 | if self.direction == EnvelopeDirection::Up && self.volume < 0xF {
57 | self.volume += 1;
58 | }
59 | if self.direction == EnvelopeDirection::Down && self.volume > 0 {
60 | self.volume -= 1;
61 | }
62 | }
63 | }
64 |
65 | pub fn new() -> VolumeEnvelope {
66 | VolumeEnvelope {
67 | initial_volume: 0,
68 | direction: EnvelopeDirection::Down,
69 | sweep_period: 0,
70 | period_timer: 0,
71 | volume: 0,
72 | volume_timer: 0,
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/libretro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gbrs-libretro"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | libretro-rs = { git = "https://github.com/libretro-rs/libretro-rs.git", features = [
8 | "experimental",
9 | ] }
10 | gbrs-core = { path = "../core", features = ["sound"], default-features = false }
11 | spin = { version = "0.9.8", features = ["once", "spin_mutex"] }
12 |
13 | [lib]
14 | crate-type = ["cdylib"]
15 |
--------------------------------------------------------------------------------
/libretro/libgbrs_libretro.info:
--------------------------------------------------------------------------------
1 | # Software Information
2 | display_name = "Nintendo - Game Boy / Color (gbrs)"
3 | authors = "Adam Soutar"
4 | supported_extensions = "gb|gbc"
5 | corename = "gbrs"
6 | display_version = "0.1.0"
7 | categories = "Emulator"
8 | license = "MIT"
9 | permissions = ""
10 |
11 | # Hardware Information
12 | manufacturer = "Nintendo"
13 | systemname = "Game Boy/Game Boy Color"
14 | systemid = "game_boy"
15 |
16 | # Libretro Features
17 | database = "Nintendo - Game Boy|Nintendo - Game Boy Color"
18 | supports_no_game = "false"
19 |
20 | description = "A programmer's hobby project. Decently accurate in DMG games, but not a replacement for a 'proper' emulator."
21 |
--------------------------------------------------------------------------------
/libretro/run.sh:
--------------------------------------------------------------------------------
1 | cargo build &&
2 | RUST_BACKTRACE=1 /Applications/RetroArch.app/Contents/MacOS/RetroArch --verbose -L ./target/debug/libgbrs_libretro.dylib "../roms/$1.gb""
3 |
--------------------------------------------------------------------------------
/libretro/src/lib.rs:
--------------------------------------------------------------------------------
1 | use gbrs_core::config::Config;
2 | use gbrs_core::constants::*;
3 | use gbrs_core::cpu::Cpu;
4 | use gbrs_core::memory::rom::Rom;
5 | use libretro_rs::c_utf8::{c_utf8, CUtf8};
6 | use libretro_rs::ffi::retro_log_level::*;
7 | use libretro_rs::retro::env::{Init, UnloadGame};
8 | use libretro_rs::retro::pixel::{Format, XRGB8888};
9 | use libretro_rs::retro::*;
10 | use libretro_rs::{ext, libretro_core};
11 | use spin::{mutex::SpinMutex, Once};
12 |
13 | struct LibretroCore {
14 | gameboy: Cpu,
15 | last_cpu_config: Config,
16 | rendering_mode: SoftwareRenderEnabled,
17 | frame_buffer: [XRGB8888; SCREEN_WIDTH * SCREEN_HEIGHT],
18 | pixel_format: Format,
19 | }
20 |
21 | static LOGGER: Once> = Once::new();
22 |
23 | impl<'a> Core<'a> for LibretroCore {
24 | type Init = ();
25 |
26 | fn get_system_info() -> SystemInfo {
27 | SystemInfo::new(
28 | c_utf8!("gbrs"),
29 | c_utf8!(env!("CARGO_PKG_VERSION")),
30 | ext!["gb", "gbc"],
31 | )
32 | }
33 |
34 | fn init(env: &mut impl Init) -> Self::Init {
35 | LOGGER.call_once(|| SpinMutex::new(env.get_log_interface().unwrap()));
36 |
37 | gbrs_core::callbacks::set_callbacks(gbrs_core::callbacks::Callbacks {
38 | log: |log_str| {
39 | let null_terminated = &format!("{}\0", log_str)[..];
40 | let retro_str = CUtf8::from_str(null_terminated).unwrap();
41 | if let Some(logger) = LOGGER.get() {
42 | logger.lock().log(RETRO_LOG_INFO, retro_str);
43 | }
44 | },
45 | save: |_game_name, _rom_path, _save_data| {},
46 | load: |_game_name, _rom_path, expected_size| vec![0; expected_size],
47 | })
48 | }
49 |
50 | fn get_system_av_info(
51 | &self,
52 | _env: &mut impl env::GetAvInfo,
53 | ) -> SystemAVInfo {
54 | SystemAVInfo::new(
55 | GameGeometry::fixed(SCREEN_WIDTH as u16, SCREEN_HEIGHT as u16),
56 | SystemTiming::new(
57 | DEFAULT_FRAME_RATE as f64,
58 | SOUND_SAMPLE_RATE as f64,
59 | ),
60 | )
61 | }
62 |
63 | fn run(
64 | &mut self,
65 | _env: &mut impl env::Run,
66 | runtime: &mut impl Callbacks,
67 | ) -> InputsPolled {
68 | let gb = &mut self.gameboy;
69 |
70 | for i in 0..SCREEN_BUFFER_SIZE {
71 | let mut pixel: u32 = 0;
72 | let r = gb.gpu.finished_frame[i].red;
73 | let g = gb.gpu.finished_frame[i].green;
74 | let b = gb.gpu.finished_frame[i].blue;
75 | pixel |= b as u32;
76 | pixel |= (g as u32) << 8;
77 | pixel |= (r as u32) << 16;
78 | self.frame_buffer[i] = XRGB8888::new_with_raw_value(pixel);
79 | }
80 |
81 | let frame = Frame::new(
82 | &self.frame_buffer,
83 | SCREEN_WIDTH as u32,
84 | SCREEN_HEIGHT as u32,
85 | );
86 | runtime.upload_video_frame(
87 | &self.rendering_mode,
88 | &self.pixel_format,
89 | &frame,
90 | );
91 | runtime.upload_audio_frame(&gb.mem.apu.buffer);
92 |
93 | let inputs_polled = runtime.poll_inputs();
94 | let port = DevicePort::new(0);
95 | gb.mem.joypad.a_pressed =
96 | runtime.is_joypad_button_pressed(port, JoypadButton::A);
97 | gb.mem.joypad.b_pressed =
98 | runtime.is_joypad_button_pressed(port, JoypadButton::B);
99 | gb.mem.joypad.start_pressed =
100 | runtime.is_joypad_button_pressed(port, JoypadButton::Start);
101 | gb.mem.joypad.select_pressed =
102 | runtime.is_joypad_button_pressed(port, JoypadButton::Select);
103 | gb.mem.joypad.left_pressed =
104 | runtime.is_joypad_button_pressed(port, JoypadButton::Left);
105 | gb.mem.joypad.right_pressed =
106 | runtime.is_joypad_button_pressed(port, JoypadButton::Right);
107 | gb.mem.joypad.up_pressed =
108 | runtime.is_joypad_button_pressed(port, JoypadButton::Up);
109 | gb.mem.joypad.down_pressed =
110 | runtime.is_joypad_button_pressed(port, JoypadButton::Down);
111 |
112 | while !gb.mem.apu.buffer_full {
113 | gb.step();
114 | }
115 | gb.mem.apu.buffer_full = false;
116 |
117 | inputs_polled
118 | }
119 |
120 | fn load_game(
121 | game: &GameInfo,
122 | args: LoadGameExtraArgs<'a, '_, E, Self::Init>,
123 | ) -> Result {
124 | let LoadGameExtraArgs {
125 | env,
126 | pixel_format,
127 | rendering_mode,
128 | ..
129 | } = args;
130 | let pixel_format = env.set_pixel_format_xrgb8888(pixel_format)?;
131 | let data: &[u8] = game.as_data().ok_or(CoreError::new())?.data();
132 | let config = Config {
133 | sound_buffer_size: SOUND_BUFFER_SIZE,
134 | sound_sample_rate: SOUND_SAMPLE_RATE,
135 | rom: Rom::from_bytes(data.to_vec()),
136 | };
137 | Ok(Self {
138 | rendering_mode,
139 | pixel_format,
140 | gameboy: Cpu::from_config(config.clone()),
141 | last_cpu_config: config,
142 | frame_buffer: [XRGB8888::DEFAULT; SCREEN_WIDTH * SCREEN_HEIGHT],
143 | })
144 | }
145 |
146 | fn reset(&mut self, _env: &mut impl env::Reset) {
147 | self.gameboy = Cpu::from_config(self.last_cpu_config.clone());
148 | }
149 |
150 | fn unload_game(self, _env: &mut impl UnloadGame) -> Self::Init {
151 | ()
152 | }
153 | }
154 |
155 | libretro_core!(crate::LibretroCore);
156 |
--------------------------------------------------------------------------------
/profiling/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "profiling"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
6 | [profile.release]
7 | panic = "abort"
8 | opt-level = 3
9 | lto = false
10 | debug = true
11 | split-debuginfo = "packed"
12 |
13 | [dependencies]
14 | gbrs-core = { path = "../core", default-features = false, features = ["std"] }
15 |
--------------------------------------------------------------------------------
/profiling/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::time::SystemTime;
3 |
4 | use gbrs_core::{
5 | config::Config,
6 | constants::{SOUND_BUFFER_SIZE, SOUND_SAMPLE_RATE},
7 | cpu::Cpu,
8 | memory::rom::Rom,
9 | };
10 |
11 | const RUNS: u128 = 5000;
12 |
13 | fn main() {
14 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument");
15 | let mut processor = Cpu::from_config(Config {
16 | rom: Rom::from_file(&rom_path),
17 | sound_buffer_size: SOUND_BUFFER_SIZE,
18 | sound_sample_rate: SOUND_SAMPLE_RATE,
19 | });
20 |
21 | // Just run the CPU forever so we can profile hot areas of emulation.
22 | let mut harness_total = 0;
23 | for _ in 0..RUNS {
24 | let now = SystemTime::now();
25 |
26 | processor.step_one_frame();
27 |
28 | let time = now.elapsed().unwrap().as_micros();
29 | harness_total += time;
30 | }
31 |
32 | println!(
33 | "Average execution time across {} runs: {} microseconds",
34 | RUNS,
35 | harness_total / RUNS
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/roms/DMG-ACID2-LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Matt Currie
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 |
--------------------------------------------------------------------------------
/roms/dmg-acid2.gb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/roms/dmg-acid2.gb
--------------------------------------------------------------------------------
/sdl-gui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gbrs-sdl-gui"
3 | version = "0.2.0"
4 | authors = ["Adam Soutar "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | gbrs-core = { path = "../core" }
9 | sdl2 = { version = "0.37.0", features = ["bundled"] }
10 |
--------------------------------------------------------------------------------
/sdl-gui/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | #[cfg(target_os = "macos")]
3 | println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path");
4 |
5 | #[cfg(target_os = "linux")]
6 | println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
7 | }
8 |
--------------------------------------------------------------------------------
/sdl-gui/src/gui.rs:
--------------------------------------------------------------------------------
1 | use gbrs_core::constants::*;
2 | use gbrs_core::cpu::Cpu;
3 |
4 | use sdl2::audio::{AudioQueue, AudioSpecDesired};
5 | use sdl2::event::Event;
6 | use sdl2::keyboard::Scancode;
7 | use sdl2::pixels::Color;
8 | use sdl2::rect::Rect;
9 |
10 | // NOTE: The SDL port does not currently perform non-integer scaling.
11 | // Please choose a multiple of 160x144
12 | const WINDOW_WIDTH: u32 = 800;
13 | const WINDOW_HEIGHT: u32 = 720;
14 |
15 | pub fn run_gui(mut gameboy: Cpu) {
16 | let sdl_context = sdl2::init().unwrap();
17 | let video_subsystem = sdl_context.video().unwrap();
18 |
19 | let window_title = format!("{} - gbrs (SDL)", gameboy.cart_info.title);
20 | let window = video_subsystem
21 | .window(&window_title[..], WINDOW_WIDTH, WINDOW_HEIGHT)
22 | .position_centered()
23 | .build()
24 | .unwrap();
25 |
26 | let square_width = WINDOW_WIDTH as usize / SCREEN_WIDTH;
27 | let square_height = WINDOW_HEIGHT as usize / SCREEN_HEIGHT;
28 |
29 | let mut canvas = window
30 | .into_canvas()
31 | // TODO: This option fixes visual tearing, but it messes up our sound
32 | // timing code, and the speed of the emulator is thrown way off :(
33 | // .present_vsync()
34 | .build()
35 | .unwrap();
36 |
37 | canvas.set_draw_color(Color::RGB(255, 255, 255));
38 | canvas.clear();
39 | canvas.present();
40 | let mut event_pump = sdl_context.event_pump().unwrap();
41 |
42 | let audio_subsystem = sdl_context.audio().unwrap();
43 | let desired_spec = AudioSpecDesired {
44 | freq: Some(SOUND_SAMPLE_RATE as i32),
45 | channels: Some(2),
46 | samples: Some(SOUND_BUFFER_SIZE as u16),
47 | };
48 |
49 | let audio_queue: AudioQueue =
50 | audio_subsystem.open_queue(None, &desired_spec).unwrap();
51 |
52 | assert_eq!(
53 | audio_queue.spec().samples,
54 | SOUND_BUFFER_SIZE as u16,
55 | "Audio device does not support gbrs' sound buffer size"
56 | );
57 |
58 | gameboy.step_until_full_audio_buffer();
59 | // gameboy.mem.apu.buffer_full = true;
60 |
61 | 'running: loop {
62 | for event in event_pump.poll_iter() {
63 | if matches!(event, Event::Quit { .. }) {
64 | break 'running;
65 | }
66 | }
67 |
68 | // Draw the screen
69 | for x in 0..SCREEN_WIDTH {
70 | for y in 0..SCREEN_HEIGHT {
71 | let i = (y * 160 + x) as usize;
72 | let colour = &gameboy.gpu.finished_frame[i];
73 | canvas.set_draw_color(Color::RGB(
74 | colour.red,
75 | colour.green,
76 | colour.blue,
77 | ));
78 | canvas
79 | .fill_rect(Rect::new(
80 | (x * square_width) as i32,
81 | (y * square_height) as i32,
82 | square_width as u32,
83 | square_height as u32,
84 | ))
85 | .unwrap();
86 | }
87 | }
88 | canvas.present();
89 |
90 | gameboy.mem.joypad.start_pressed = event_pump
91 | .keyboard_state()
92 | .is_scancode_pressed(Scancode::Return);
93 | gameboy.mem.joypad.select_pressed = event_pump
94 | .keyboard_state()
95 | .is_scancode_pressed(Scancode::Backspace);
96 | gameboy.mem.joypad.a_pressed =
97 | event_pump.keyboard_state().is_scancode_pressed(Scancode::X);
98 | gameboy.mem.joypad.b_pressed =
99 | event_pump.keyboard_state().is_scancode_pressed(Scancode::Z);
100 | gameboy.mem.joypad.left_pressed = event_pump
101 | .keyboard_state()
102 | .is_scancode_pressed(Scancode::Left);
103 | gameboy.mem.joypad.right_pressed = event_pump
104 | .keyboard_state()
105 | .is_scancode_pressed(Scancode::Right);
106 | gameboy.mem.joypad.up_pressed = event_pump
107 | .keyboard_state()
108 | .is_scancode_pressed(Scancode::Up);
109 | gameboy.mem.joypad.down_pressed = event_pump
110 | .keyboard_state()
111 | .is_scancode_pressed(Scancode::Down);
112 |
113 | gameboy.step_until_full_audio_buffer();
114 |
115 | let pre = audio_queue.size();
116 | audio_queue.queue_audio(&gameboy.mem.apu.buffer).unwrap();
117 | audio_queue.resume();
118 | let diff = audio_queue.size() - pre;
119 |
120 | while audio_queue.size() > diff {
121 | // NOTE: You can comment this if statement out if you're having
122 | // speed or sound issues. It is an attempt to help out slower
123 | // machines, but you may not need it if your machine is fast
124 | // enough.
125 | if !gameboy.mem.apu.buffer_full {
126 | gameboy.step();
127 | }
128 | std::hint::spin_loop();
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/sdl-gui/src/main.rs:
--------------------------------------------------------------------------------
1 | pub mod gui;
2 |
3 | use std::env;
4 |
5 | use gbrs_core::config::Config;
6 | use gbrs_core::cpu::Cpu;
7 | use gbrs_core::memory::rom::Rom;
8 | use gui::run_gui;
9 |
10 | // TODO: Get these from an SDL audio device
11 | const SOUND_BUFFER_SIZE: usize = 1024;
12 | const SOUND_SAMPLE_RATE: usize = 48000;
13 |
14 | fn main() {
15 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument");
16 | let processor = Cpu::from_config(Config {
17 | sound_buffer_size: SOUND_BUFFER_SIZE,
18 | sound_sample_rate: SOUND_SAMPLE_RATE,
19 | rom: Rom::from_file(&rom_path),
20 | });
21 | run_gui(processor);
22 | }
23 |
--------------------------------------------------------------------------------
/sfml-gui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gbrs-sfml-gui"
3 | version = "0.2.0"
4 | authors = ["Adam Soutar "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | gbrs-core = { path = "../core" }
9 | sfml = "0.24.0"
10 | spin = { version = "0.9.8", features = ["spin_mutex"] }
11 |
--------------------------------------------------------------------------------
/sfml-gui/src/control.rs:
--------------------------------------------------------------------------------
1 | use gbrs_core::cpu::Cpu;
2 |
3 | use sfml::window::joystick::*;
4 | use sfml::window::*;
5 |
6 | // TODO: Mappings for:
7 | // - Xbox 360
8 | // - Xbox One
9 | // - DualShock 5
10 | // - The above on Windows
11 | #[allow(dead_code)]
12 | mod ps4 {
13 | // These are ascertained through experimentation with a wired DualShock 4
14 | // on macOS.
15 | use sfml::window::joystick::Axis;
16 |
17 | pub const X: u32 = 1;
18 | pub const SQUARE: u32 = 0;
19 | pub const TRIANGLE: u32 = 3;
20 | pub const CIRCLE: u32 = 2;
21 |
22 | pub const START: u32 = 9;
23 | pub const SHARE: u32 = 8;
24 | pub const TOUCHPAD: u32 = 13;
25 |
26 | pub const LEFT_STICK_X: Axis = Axis::X;
27 | pub const LEFT_STICK_Y: Axis = Axis::Y;
28 | pub const RIGHT_STICK_X: Axis = Axis::Z;
29 | pub const RIGHT_STICK_Y: Axis = Axis::R;
30 | pub const DPAD_X: Axis = Axis::PovX;
31 | pub const DPAD_Y: Axis = Axis::PovY;
32 |
33 | // DualShock 4 axes go from -100 to +100
34 | pub const DEADZONE: f32 = 25.;
35 | }
36 |
37 | pub fn update_joypad_state(gameboy: &mut Cpu) {
38 | // TODO: Raise the joypad interrupt
39 | gameboy.mem.joypad.a_pressed =
40 | key(Key::X) || joy(ps4::X) || joy(ps4::CIRCLE);
41 |
42 | gameboy.mem.joypad.b_pressed =
43 | key(Key::Z) || joy(ps4::SQUARE) || joy(ps4::TRIANGLE);
44 |
45 | gameboy.mem.joypad.start_pressed = key(Key::Enter) || joy(ps4::START);
46 |
47 | gameboy.mem.joypad.select_pressed =
48 | key(Key::Backspace) || joy(ps4::TOUCHPAD) || joy(ps4::SHARE);
49 |
50 | gameboy.mem.joypad.up_pressed = key(Key::Up)
51 | || axis(ps4::LEFT_STICK_Y, false)
52 | || axis(ps4::DPAD_Y, true);
53 |
54 | gameboy.mem.joypad.down_pressed = key(Key::Down)
55 | || axis(ps4::LEFT_STICK_Y, true)
56 | || axis(ps4::DPAD_Y, false);
57 |
58 | gameboy.mem.joypad.left_pressed = key(Key::Left)
59 | || axis(ps4::LEFT_STICK_X, false)
60 | || axis(ps4::DPAD_X, false);
61 |
62 | gameboy.mem.joypad.right_pressed = key(Key::Right)
63 | || axis(ps4::LEFT_STICK_X, true)
64 | || axis(ps4::DPAD_X, true);
65 | }
66 |
67 | fn key(key: Key) -> bool {
68 | Key::is_pressed(key)
69 | }
70 | fn joy(button: u32) -> bool {
71 | for i in 0..joystick::COUNT {
72 | if joystick::is_button_pressed(i, button) {
73 | return true;
74 | }
75 | }
76 |
77 | false
78 | }
79 | fn axis(target: Axis, positive: bool) -> bool {
80 | for i in 0..joystick::COUNT {
81 | let mut val = joystick::axis_position(i, target);
82 | if !positive {
83 | val = -val
84 | }
85 |
86 | if val > 100. - ps4::DEADZONE {
87 | return true;
88 | }
89 | }
90 |
91 | false
92 | }
93 |
--------------------------------------------------------------------------------
/sfml-gui/src/gui.rs:
--------------------------------------------------------------------------------
1 | use crate::control::*;
2 |
3 | use gbrs_core::constants::*;
4 | use gbrs_core::cpu::Cpu;
5 |
6 | use sfml::audio::{Sound, SoundBuffer, SoundStatus};
7 | use sfml::graphics::*;
8 | use sfml::system::*;
9 | use sfml::window::*;
10 | use spin::mutex::SpinMutex;
11 |
12 | pub const STEP_BY_STEP: bool = false;
13 | // NOTE: This debug option is only supported on macOS. See note below
14 | pub const DRAW_FPS: bool = false;
15 |
16 | static SOUND_BACKING_STORE: SpinMutex<[i16; SOUND_BUFFER_SIZE]> =
17 | SpinMutex::new([0; SOUND_BUFFER_SIZE]);
18 |
19 | pub fn run_gui(mut gameboy: Cpu) {
20 | let sw = SCREEN_WIDTH as u32;
21 | let sh = SCREEN_HEIGHT as u32;
22 | let window_width: u32 = 640;
23 | let window_height: u32 = 512;
24 |
25 | let style = Style::RESIZE | Style::TITLEBAR | Style::CLOSE;
26 | let mut window = RenderWindow::new(
27 | (window_width, window_height),
28 | &format!("{} - gbrs (SFML)", gameboy.cart_info.title)[..],
29 | style,
30 | &ContextSettings::default(),
31 | )
32 | .unwrap();
33 | // window.set_framerate_limit(gameboy.frame_rate as u32);
34 |
35 | let mut screen_texture = Texture::new().unwrap();
36 | screen_texture
37 | .create(sw, sh)
38 | .expect("Failed to create screen texture");
39 |
40 | // Scale the 160x144 image to the appropriate resolution
41 | let sprite_scale = Vector2f::new(
42 | window_width as f32 / sw as f32,
43 | window_height as f32 / sh as f32,
44 | );
45 |
46 | let mut clock = Clock::start().unwrap();
47 |
48 | let font;
49 | let mut text = None;
50 | if DRAW_FPS {
51 | // NOTE: DRAW_FPS only works on macOS at the moment due to hardcoded
52 | // font paths. I don't want to include a font in the gbrs repo just
53 | // for this debug feature.
54 | font = Font::from_file("/System/Library/Fonts/Menlo.ttc").unwrap();
55 | text = Some(Text::new("", &font, 32));
56 | // Make it stick out instead of white on a black+white screen
57 | text.as_mut().unwrap().set_fill_color(Color::BLUE);
58 | }
59 |
60 | // Get the initial frame & buffer of audio
61 | gameboy.step_until_full_audio_buffer();
62 |
63 | loop {
64 | let secs = clock.restart().as_seconds();
65 |
66 | while let Some(ev) = window.poll_event() {
67 | if ev == Event::Closed {
68 | window.close();
69 | return;
70 | }
71 | }
72 |
73 | update_joypad_state(&mut gameboy);
74 | // gameboy.step_until_full_audio_buffer();
75 |
76 | // Draw the previous frame
77 | screen_texture.update_from_pixels(
78 | &gameboy.gpu.get_rgba_frame(),
79 | sw,
80 | sh,
81 | 0,
82 | 0,
83 | );
84 | let mut screen_sprite = Sprite::with_texture(&screen_texture);
85 | screen_sprite.set_scale(sprite_scale);
86 |
87 | window.clear(Color::BLACK);
88 | window.draw(&screen_sprite);
89 | if DRAW_FPS {
90 | text.as_mut()
91 | .unwrap()
92 | .set_string(&format!("{} FPS", (1. / secs) as usize)[..]);
93 | window.draw(text.as_ref().unwrap());
94 | }
95 | window.display();
96 |
97 | // Play the audio while creating the next frame and sound buffer
98 | // This way we're not idling, we're actively computing the next event.
99 | // let sound_buffer = SoundBuffer::from_samples(&gameboy.mem.apu.buffer, 2, SOUND_SAMPLE_RATE as u32).unwrap();
100 | // let mut sound = Sound::with_buffer(&sound_buffer);
101 | // sound.play();
102 |
103 | let mut sound_backing_store = SOUND_BACKING_STORE.lock();
104 | *sound_backing_store = gameboy.mem.apu.buffer;
105 | let sound_buffer = SoundBuffer::from_samples(
106 | &*sound_backing_store,
107 | 2,
108 | SOUND_SAMPLE_RATE as u32,
109 | )
110 | .unwrap();
111 | let mut sound = Sound::with_buffer(&sound_buffer);
112 |
113 | // sound.set_volume(0.);
114 | sound.play();
115 | while sound.status() == SoundStatus::PLAYING {
116 | if !gameboy.mem.apu.buffer_full {
117 | gameboy.step();
118 | } else {
119 | // We're finished with this frame. Let's just wait for audio
120 | // to sync up.
121 | std::hint::spin_loop();
122 | }
123 | }
124 |
125 | // Just in-case we're running too slow, let's catch up.
126 | // This may be when you get a small audio pop. It happens more often
127 | // on slower machines.
128 | while !gameboy.mem.apu.buffer_full {
129 | gameboy.step();
130 | }
131 | gameboy.mem.apu.buffer_full = false;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/sfml-gui/src/main.rs:
--------------------------------------------------------------------------------
1 | pub mod control;
2 | pub mod gui;
3 |
4 | use std::env;
5 |
6 | use gbrs_core::config::Config;
7 | use gbrs_core::cpu::Cpu;
8 | use gbrs_core::memory::rom::Rom;
9 | use gui::run_gui;
10 |
11 | // TODO: Get these from an SFML audio device
12 | const SOUND_BUFFER_SIZE: usize = 1024;
13 | const SOUND_SAMPLE_RATE: usize = 48000;
14 |
15 | fn main() {
16 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument");
17 | let processor = Cpu::from_config(Config {
18 | sound_buffer_size: SOUND_BUFFER_SIZE,
19 | sound_sample_rate: SOUND_SAMPLE_RATE,
20 | rom: Rom::from_file(&rom_path),
21 | });
22 | run_gui(processor);
23 | }
24 |
--------------------------------------------------------------------------------
/wasm-gui/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "base64"
7 | version = "0.20.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
10 |
11 | [[package]]
12 | name = "bumpalo"
13 | version = "3.11.1"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
16 |
17 | [[package]]
18 | name = "cfg-if"
19 | version = "1.0.0"
20 | source = "registry+https://github.com/rust-lang/crates.io-index"
21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
22 |
23 | [[package]]
24 | name = "console_error_panic_hook"
25 | version = "0.1.7"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
28 | dependencies = [
29 | "cfg-if",
30 | "wasm-bindgen",
31 | ]
32 |
33 | [[package]]
34 | name = "gbrs-core"
35 | version = "0.2.0"
36 | dependencies = [
37 | "lazy_static",
38 | "smallvec",
39 | ]
40 |
41 | [[package]]
42 | name = "gbrs-wasm-gui"
43 | version = "0.1.0"
44 | dependencies = [
45 | "base64",
46 | "console_error_panic_hook",
47 | "gbrs-core",
48 | "wasm-bindgen",
49 | "web-sys",
50 | ]
51 |
52 | [[package]]
53 | name = "js-sys"
54 | version = "0.3.60"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
57 | dependencies = [
58 | "wasm-bindgen",
59 | ]
60 |
61 | [[package]]
62 | name = "lazy_static"
63 | version = "1.4.0"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
66 |
67 | [[package]]
68 | name = "log"
69 | version = "0.4.17"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
72 | dependencies = [
73 | "cfg-if",
74 | ]
75 |
76 | [[package]]
77 | name = "once_cell"
78 | version = "1.16.0"
79 | source = "registry+https://github.com/rust-lang/crates.io-index"
80 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
81 |
82 | [[package]]
83 | name = "proc-macro2"
84 | version = "1.0.49"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
87 | dependencies = [
88 | "unicode-ident",
89 | ]
90 |
91 | [[package]]
92 | name = "quote"
93 | version = "1.0.23"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
96 | dependencies = [
97 | "proc-macro2",
98 | ]
99 |
100 | [[package]]
101 | name = "smallvec"
102 | version = "1.10.0"
103 | source = "registry+https://github.com/rust-lang/crates.io-index"
104 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
105 |
106 | [[package]]
107 | name = "syn"
108 | version = "1.0.107"
109 | source = "registry+https://github.com/rust-lang/crates.io-index"
110 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
111 | dependencies = [
112 | "proc-macro2",
113 | "quote",
114 | "unicode-ident",
115 | ]
116 |
117 | [[package]]
118 | name = "unicode-ident"
119 | version = "1.0.6"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
122 |
123 | [[package]]
124 | name = "wasm-bindgen"
125 | version = "0.2.83"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
128 | dependencies = [
129 | "cfg-if",
130 | "wasm-bindgen-macro",
131 | ]
132 |
133 | [[package]]
134 | name = "wasm-bindgen-backend"
135 | version = "0.2.83"
136 | source = "registry+https://github.com/rust-lang/crates.io-index"
137 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
138 | dependencies = [
139 | "bumpalo",
140 | "log",
141 | "once_cell",
142 | "proc-macro2",
143 | "quote",
144 | "syn",
145 | "wasm-bindgen-shared",
146 | ]
147 |
148 | [[package]]
149 | name = "wasm-bindgen-macro"
150 | version = "0.2.83"
151 | source = "registry+https://github.com/rust-lang/crates.io-index"
152 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
153 | dependencies = [
154 | "quote",
155 | "wasm-bindgen-macro-support",
156 | ]
157 |
158 | [[package]]
159 | name = "wasm-bindgen-macro-support"
160 | version = "0.2.83"
161 | source = "registry+https://github.com/rust-lang/crates.io-index"
162 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
163 | dependencies = [
164 | "proc-macro2",
165 | "quote",
166 | "syn",
167 | "wasm-bindgen-backend",
168 | "wasm-bindgen-shared",
169 | ]
170 |
171 | [[package]]
172 | name = "wasm-bindgen-shared"
173 | version = "0.2.83"
174 | source = "registry+https://github.com/rust-lang/crates.io-index"
175 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
176 |
177 | [[package]]
178 | name = "web-sys"
179 | version = "0.3.60"
180 | source = "registry+https://github.com/rust-lang/crates.io-index"
181 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
182 | dependencies = [
183 | "js-sys",
184 | "wasm-bindgen",
185 | ]
186 |
--------------------------------------------------------------------------------
/wasm-gui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gbrs-wasm-gui"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | gbrs-core = { path = "../core" }
11 | wasm-bindgen = "0.2"
12 | web-sys = { version = "0.3.77", features = ["console", "Window", "Storage"] }
13 | console_error_panic_hook = "0.1.7"
14 | base64 = "0.22.1"
--------------------------------------------------------------------------------
/wasm-gui/buildAndServe.sh:
--------------------------------------------------------------------------------
1 | wasm-pack build --release --target web && \
2 | python3 -m http.server
--------------------------------------------------------------------------------
/wasm-gui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | gbrs
5 |
6 |
20 |
21 |
22 |
23 |
24 |
25 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/wasm-gui/src/lib.rs:
--------------------------------------------------------------------------------
1 | use gbrs_core::config::Config;
2 | use gbrs_core::constants;
3 | use gbrs_core::cpu::Cpu;
4 | use gbrs_core::memory::rom::Rom;
5 | use gbrs_core::{callbacks, callbacks::Callbacks, constants::*};
6 | use wasm_bindgen::prelude::*;
7 | use web_sys::{console, window, Storage};
8 |
9 | static mut CPU: Option = None;
10 |
11 | fn local_storage() -> Storage {
12 | window().unwrap().local_storage().unwrap().unwrap()
13 | }
14 |
15 | #[wasm_bindgen]
16 | pub fn create_gameboy() {
17 | console_error_panic_hook::set_once();
18 |
19 | unsafe {
20 | callbacks::set_callbacks(Callbacks {
21 | log: |log_str| console::log_1(&log_str.into()),
22 | save: |game_name, _rom_path, save_data| {
23 | let data_string = base64::encode(save_data);
24 | local_storage()
25 | .set_item(game_name, &data_string)
26 | .expect("Failed to save in localStorage");
27 | },
28 | load: |game_name, _rom_path, expected_size| {
29 | let optional_data_string = local_storage()
30 | .get_item(game_name)
31 | .expect("Failed to read save in localStorage");
32 |
33 | if let Some(data_string) = optional_data_string {
34 | // This game already has save data in this browser
35 | let loaded_data = base64::decode(data_string).unwrap();
36 | if loaded_data.len() == expected_size {
37 | return loaded_data;
38 | }
39 | }
40 | // Else we've not run this game before
41 | vec![0; expected_size as usize]
42 | },
43 | });
44 |
45 | CPU = Some(Cpu::from_config(Config {
46 | sound_buffer_size: constants::SOUND_BUFFER_SIZE,
47 | sound_sample_rate: constants::SOUND_SAMPLE_RATE,
48 | rom: Rom::from_bytes(
49 | include_bytes!("../../roms/dmg-acid2.gb").to_vec(),
50 | ),
51 | }));
52 | }
53 | }
54 |
55 | #[wasm_bindgen]
56 | pub fn step_one_frame() {
57 | unsafe {
58 | CPU.as_mut().unwrap().step_one_frame();
59 | }
60 | }
61 |
62 | #[wasm_bindgen]
63 | pub fn get_finished_frame() -> Vec {
64 | let frame = unsafe { CPU.as_mut().unwrap().gpu.finished_frame };
65 | // TODO: Re-use a buffer instead
66 | let mut int_frame = Vec::with_capacity(SCREEN_BUFFER_SIZE);
67 |
68 | for i in 0..SCREEN_BUFFER_SIZE {
69 | int_frame.push(format!(
70 | "rgb({},{},{})",
71 | frame[i].red, frame[i].green, frame[i].blue
72 | ));
73 | }
74 |
75 | int_frame
76 | }
77 |
78 | #[wasm_bindgen]
79 | pub fn set_control_state(
80 | a: bool,
81 | b: bool,
82 | up: bool,
83 | down: bool,
84 | left: bool,
85 | right: bool,
86 | start: bool,
87 | select: bool,
88 | ) {
89 | unsafe {
90 | let cpu = CPU.as_mut().unwrap();
91 | cpu.mem.joypad.a_pressed = a;
92 | cpu.mem.joypad.b_pressed = b;
93 | cpu.mem.joypad.up_pressed = up;
94 | cpu.mem.joypad.down_pressed = down;
95 | cpu.mem.joypad.left_pressed = left;
96 | cpu.mem.joypad.right_pressed = right;
97 | cpu.mem.joypad.start_pressed = start;
98 | cpu.mem.joypad.select_pressed = select;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------