├── .gitignore
├── LICENSE
├── README.md
├── app
├── SwiftBoy.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Swift Boy Release.xcscheme
│ │ └── Swift Boy.xcscheme
└── SwiftBoy
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── AppIcon1024x1024.png
│ │ ├── AppIcon20x20.png
│ │ ├── AppIcon20x20@2x-1.png
│ │ ├── AppIcon20x20@2x.png
│ │ ├── AppIcon20x20@3x.png
│ │ ├── AppIcon29x29.png
│ │ ├── AppIcon29x29@2x-1.png
│ │ ├── AppIcon29x29@2x.png
│ │ ├── AppIcon29x29@3x.png
│ │ ├── AppIcon40x40.png
│ │ ├── AppIcon40x40@2x-1.png
│ │ ├── AppIcon40x40@2x.png
│ │ ├── AppIcon40x40@3x.png
│ │ ├── AppIcon60x60@2x.png
│ │ ├── AppIcon60x60@3x.png
│ │ ├── AppIcon76x76.png
│ │ ├── AppIcon76x76@2x.png
│ │ ├── AppIcon83.5x83.5@2x.png
│ │ └── Contents.json
│ ├── Contents.json
│ └── LaunchScreenColor.colorset
│ │ └── Contents.json
│ ├── Base.lproj
│ └── Main.storyboard
│ ├── Emulator
│ ├── APU.swift
│ ├── CPU.swift
│ ├── Cartridge.swift
│ ├── Clock.swift
│ ├── Common.swift
│ ├── FileSystem.swift
│ ├── GameLibraryManager.swift
│ ├── Instructions.swift
│ ├── Joypad.swift
│ ├── MMU.swift
│ ├── PPU.swift
│ ├── Timer.swift
│ └── UI.swift
│ ├── Info.plist
│ ├── ROMs
│ └── cpu_instrs.gb
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── assets
├── icloud-drive-portrait.png
├── icon-rounded.png
├── icon.png
├── icon.sketch
├── icon.svg
├── import-menu-landscape.png
├── import-menu-portrait.gif
├── import-menu-portrait.mov
├── import-menu-portrait.png
├── super-marioland-gameplay-landscape.gif
├── super-marioland-gameplay-landscape.mov
├── super-marioland-gameplay-landscape.png
├── super-marioland-gameplay-portrait.gif
├── super-marioland-gameplay-portrait.mov
├── super-marioland-gameplay-portrait.png
├── super-marioland-main-landscape.png
├── super-marioland-main-portrait.png
├── tetris-gameplay-landscape.png
├── tetris-gameplay-portrait.gif
├── tetris-gameplay-portrait.mov
├── tetris-main-landscape.png
├── tetris-main-portrait.png
├── tetris-music-landscape.png
└── tetris-music-portrait.png
├── blog-posts
└── part-one.md
└── gb-cpu-code-generator
├── format.js
├── generate.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/swift
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift
4 |
5 | ### Swift ###
6 | # Xcode
7 | #
8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
9 |
10 | ## User settings
11 | xcuserdata/
12 |
13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
14 | *.xcscmblueprint
15 | *.xccheckout
16 |
17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
18 | build/
19 | DerivedData/
20 | *.moved-aside
21 | *.pbxuser
22 | !default.pbxuser
23 | *.mode1v3
24 | !default.mode1v3
25 | *.mode2v3
26 | !default.mode2v3
27 | *.perspectivev3
28 | !default.perspectivev3
29 |
30 | ## Obj-C/Swift specific
31 | *.hmap
32 |
33 | ## App packaging
34 | *.ipa
35 | *.dSYM.zip
36 | *.dSYM
37 |
38 | ## Playgrounds
39 | timeline.xctimeline
40 | playground.xcworkspace
41 |
42 | # Swift Package Manager
43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
44 | # Packages/
45 | # Package.pins
46 | # Package.resolved
47 | # *.xcodeproj
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | # We recommend against adding the Pods directory to your .gitignore. However
56 | # you should judge for yourself, the pros and cons are mentioned at:
57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
58 | # Pods/
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
64 | # Carthage/Checkouts
65 |
66 | Carthage/Build/
67 |
68 | # Accio dependency management
69 | Dependencies/
70 | .accio/
71 |
72 | # fastlane
73 | # It is recommended to not store the screenshots in the git repo.
74 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
75 | # For more information about the recommended setup visit:
76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
77 |
78 | fastlane/report.xml
79 | fastlane/Preview.html
80 | fastlane/screenshots/**/*.png
81 | fastlane/test_output
82 |
83 | # Code Injection
84 | # After new code Injection tools there's a generated folder /iOSInjectionProject
85 | # https://github.com/johnno1962/injectionforxcode
86 |
87 | iOSInjectionProject/
88 |
89 | # End of https://www.toptal.com/developers/gitignore/api/swift
90 |
91 | # Custom additions
92 |
93 | node_modules/
94 | .expo/*
95 | npm-debug.*
96 | *.jks
97 | *.p8
98 | *.p12
99 | *.key
100 | *.mobileprovision
101 | *.orig.*
102 | web-build/
103 | /gb-cpu-code-generator/generated.swift
104 | /gb-cpu-code-generator/formatted.swift
105 | *.DS_Store
106 | deadeus.gb
107 | super-mario-land.gb
108 | tetris.gb
109 |
110 | # End of custom additions
111 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Boris Berak
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Swift Boy
6 |
7 | A Game Boy emulator for iOS 📱
8 |
9 | ## Getting Started
10 |
11 | 1. Clone the repo: `git clone https://github.com/bberak/swift-boy.git`
12 | 2. Open `app/SwiftBoy.xcodeproc` with XCode
13 | 3. Select `Swift Boy Release` scheme and target device
14 | 4. Build, run and enjoy some nostalgic gaming 🎉
15 |
16 | ## Importing Games
17 |
18 | You can import your own legally-obtained Game Boy ROMs (.gb files) by clicking the title above the *LCD* screen and clicking the big **IMPORT GAME** button at the bottom of the modal 👍.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## Links and Resources
27 |
28 | - [Game Boy CPU Instructions Database](https://gist.github.com/bberak/ca001281bb8431d2706afd31401e802b)
29 | - [Blargg's Test ROMs](https://github.com/retrio/gb-test-roms)
30 | - [Mooneye Test Suite](https://github.com/Gekkio/mooneye-test-suite)
31 | - [Game Boy Complete Technical Reference](https://github.com/Gekkio/gb-ctr)
32 | - [Pan Docs](https://gbdev.io/pandocs/)
33 |
34 | ## TODOs
35 |
36 | - Better MBC support (currently only partially supports MBC0 and MBC1)
37 | - The sound synthesizer is not quite right yet
38 | - Handle transparent pixels and sprite priority
39 | - A bunch of other stuff
40 |
41 | ## License
42 |
43 | MIT License
44 |
45 | Copyright (c) 2022 Boris Berak
46 |
47 | Permission is hereby granted, free of charge, to any person obtaining a copy
48 | of this software and associated documentation files (the "Software"), to deal
49 | in the Software without restriction, including without limitation the rights
50 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
51 | copies of the Software, and to permit persons to whom the Software is
52 | furnished to do so, subject to the following conditions:
53 |
54 | The above copyright notice and this permission notice shall be included in all
55 | copies or substantial portions of the Software.
56 |
57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
58 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
59 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
60 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
61 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
62 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
63 | SOFTWARE.
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A11768D127E55F2600DC4751 /* AudioKit in Frameworks */ = {isa = PBXBuildFile; productRef = A11768D027E55F2600DC4751 /* AudioKit */; };
11 | A11768D427E55F4100DC4751 /* SoundpipeAudioKit in Frameworks */ = {isa = PBXBuildFile; productRef = A11768D327E55F4100DC4751 /* SoundpipeAudioKit */; };
12 | A12B15FA2774851C00418291 /* Joypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12B15F92774851C00418291 /* Joypad.swift */; };
13 | A13BADBA284613F7001B9FF6 /* FileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13BADB9284613F7001B9FF6 /* FileSystem.swift */; };
14 | A13BADBC284636EE001B9FF6 /* UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13BADBB284636EE001B9FF6 /* UI.swift */; };
15 | A1779F0728669826005E6C8C /* GameLibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1779F0628669826005E6C8C /* GameLibraryManager.swift */; };
16 | A1785E70283BA1D3006CD7AC /* cpu_instrs.gb in Resources */ = {isa = PBXBuildFile; fileRef = A1785E6A283BA1D3006CD7AC /* cpu_instrs.gb */; };
17 | A1B4011226EB5937002F4512 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4011126EB5937002F4512 /* AppDelegate.swift */; };
18 | A1B4011426EB5937002F4512 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4011326EB5937002F4512 /* SceneDelegate.swift */; };
19 | A1B4011626EB5937002F4512 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4011526EB5937002F4512 /* ViewController.swift */; };
20 | A1B4011926EB5937002F4512 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B4011726EB5937002F4512 /* Main.storyboard */; };
21 | A1B4011B26EB593A002F4512 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B4011A26EB593A002F4512 /* Assets.xcassets */; };
22 | A1B4014726EB5DB3002F4512 /* Clock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4013F26EB5DB3002F4512 /* Clock.swift */; };
23 | A1B4014826EB5DB3002F4512 /* Cartridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014026EB5DB3002F4512 /* Cartridge.swift */; };
24 | A1B4014926EB5DB3002F4512 /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014126EB5DB3002F4512 /* Instructions.swift */; };
25 | A1B4014B26EB5DB3002F4512 /* PPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014326EB5DB3002F4512 /* PPU.swift */; };
26 | A1B4014C26EB5DB3002F4512 /* MMU.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014426EB5DB3002F4512 /* MMU.swift */; };
27 | A1B4014D26EB5DB3002F4512 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014526EB5DB3002F4512 /* Common.swift */; };
28 | A1B4014E26EB5DB3002F4512 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B4014626EB5DB3002F4512 /* CPU.swift */; };
29 | A1DCD2D52709696B001CF043 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DCD2D42709696B001CF043 /* Timer.swift */; };
30 | A1F3754D2789881C00EDEC23 /* APU.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F3754C2789881C00EDEC23 /* APU.swift */; };
31 | /* End PBXBuildFile section */
32 |
33 | /* Begin PBXFileReference section */
34 | A12B15F92774851C00418291 /* Joypad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Joypad.swift; sourceTree = ""; };
35 | A13BADB9284613F7001B9FF6 /* FileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystem.swift; sourceTree = ""; };
36 | A13BADBB284636EE001B9FF6 /* UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UI.swift; sourceTree = ""; };
37 | A1779F0628669826005E6C8C /* GameLibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameLibraryManager.swift; sourceTree = ""; };
38 | A1785E6A283BA1D3006CD7AC /* cpu_instrs.gb */ = {isa = PBXFileReference; lastKnownFileType = file; path = cpu_instrs.gb; sourceTree = ""; };
39 | A1B4010E26EB5937002F4512 /* SwiftBoy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBoy.app; sourceTree = BUILT_PRODUCTS_DIR; };
40 | A1B4011126EB5937002F4512 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
41 | A1B4011326EB5937002F4512 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
42 | A1B4011526EB5937002F4512 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
43 | A1B4011826EB5937002F4512 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
44 | A1B4011A26EB593A002F4512 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
45 | A1B4011F26EB593A002F4512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
46 | A1B4013F26EB5DB3002F4512 /* Clock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clock.swift; sourceTree = ""; };
47 | A1B4014026EB5DB3002F4512 /* Cartridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cartridge.swift; sourceTree = ""; };
48 | A1B4014126EB5DB3002F4512 /* Instructions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = ""; };
49 | A1B4014326EB5DB3002F4512 /* PPU.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPU.swift; sourceTree = ""; };
50 | A1B4014426EB5DB3002F4512 /* MMU.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMU.swift; sourceTree = ""; };
51 | A1B4014526EB5DB3002F4512 /* Common.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; };
52 | A1B4014626EB5DB3002F4512 /* CPU.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; };
53 | A1DCD2D42709696B001CF043 /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = ""; };
54 | A1F3754C2789881C00EDEC23 /* APU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APU.swift; sourceTree = ""; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | A1B4010B26EB5937002F4512 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | A11768D427E55F4100DC4751 /* SoundpipeAudioKit in Frameworks */,
63 | A11768D127E55F2600DC4751 /* AudioKit in Frameworks */,
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXFrameworksBuildPhase section */
68 |
69 | /* Begin PBXGroup section */
70 | A1B4010526EB5937002F4512 = {
71 | isa = PBXGroup;
72 | children = (
73 | A1B4011026EB5937002F4512 /* SwiftBoy */,
74 | A1B4010F26EB5937002F4512 /* Products */,
75 | );
76 | sourceTree = "";
77 | };
78 | A1B4010F26EB5937002F4512 /* Products */ = {
79 | isa = PBXGroup;
80 | children = (
81 | A1B4010E26EB5937002F4512 /* SwiftBoy.app */,
82 | );
83 | name = Products;
84 | sourceTree = "";
85 | };
86 | A1B4011026EB5937002F4512 /* SwiftBoy */ = {
87 | isa = PBXGroup;
88 | children = (
89 | A1B4014F26EB5DCE002F4512 /* ROMs */,
90 | A1B4013E26EB5DB3002F4512 /* Emulator */,
91 | A1B4011126EB5937002F4512 /* AppDelegate.swift */,
92 | A1B4011326EB5937002F4512 /* SceneDelegate.swift */,
93 | A1B4011526EB5937002F4512 /* ViewController.swift */,
94 | A1B4011726EB5937002F4512 /* Main.storyboard */,
95 | A1B4011A26EB593A002F4512 /* Assets.xcassets */,
96 | A1B4011F26EB593A002F4512 /* Info.plist */,
97 | );
98 | path = SwiftBoy;
99 | sourceTree = "";
100 | };
101 | A1B4013E26EB5DB3002F4512 /* Emulator */ = {
102 | isa = PBXGroup;
103 | children = (
104 | A1B4013F26EB5DB3002F4512 /* Clock.swift */,
105 | A1B4014026EB5DB3002F4512 /* Cartridge.swift */,
106 | A1B4014126EB5DB3002F4512 /* Instructions.swift */,
107 | A1B4014326EB5DB3002F4512 /* PPU.swift */,
108 | A1B4014426EB5DB3002F4512 /* MMU.swift */,
109 | A1B4014526EB5DB3002F4512 /* Common.swift */,
110 | A1B4014626EB5DB3002F4512 /* CPU.swift */,
111 | A1DCD2D42709696B001CF043 /* Timer.swift */,
112 | A12B15F92774851C00418291 /* Joypad.swift */,
113 | A1F3754C2789881C00EDEC23 /* APU.swift */,
114 | A13BADB9284613F7001B9FF6 /* FileSystem.swift */,
115 | A13BADBB284636EE001B9FF6 /* UI.swift */,
116 | A1779F0628669826005E6C8C /* GameLibraryManager.swift */,
117 | );
118 | path = Emulator;
119 | sourceTree = "";
120 | };
121 | A1B4014F26EB5DCE002F4512 /* ROMs */ = {
122 | isa = PBXGroup;
123 | children = (
124 | A1785E6A283BA1D3006CD7AC /* cpu_instrs.gb */,
125 | );
126 | path = ROMs;
127 | sourceTree = "";
128 | };
129 | /* End PBXGroup section */
130 |
131 | /* Begin PBXNativeTarget section */
132 | A1B4010D26EB5937002F4512 /* SwiftBoy */ = {
133 | isa = PBXNativeTarget;
134 | buildConfigurationList = A1B4012226EB593A002F4512 /* Build configuration list for PBXNativeTarget "SwiftBoy" */;
135 | buildPhases = (
136 | A1B4010A26EB5937002F4512 /* Sources */,
137 | A1B4010B26EB5937002F4512 /* Frameworks */,
138 | A1B4010C26EB5937002F4512 /* Resources */,
139 | );
140 | buildRules = (
141 | );
142 | dependencies = (
143 | );
144 | name = SwiftBoy;
145 | packageProductDependencies = (
146 | A11768D027E55F2600DC4751 /* AudioKit */,
147 | A11768D327E55F4100DC4751 /* SoundpipeAudioKit */,
148 | );
149 | productName = "Swift Boy";
150 | productReference = A1B4010E26EB5937002F4512 /* SwiftBoy.app */;
151 | productType = "com.apple.product-type.application";
152 | };
153 | /* End PBXNativeTarget section */
154 |
155 | /* Begin PBXProject section */
156 | A1B4010626EB5937002F4512 /* Project object */ = {
157 | isa = PBXProject;
158 | attributes = {
159 | LastSwiftUpdateCheck = 1130;
160 | LastUpgradeCheck = 1310;
161 | ORGANIZATIONNAME = "Boris Berak";
162 | TargetAttributes = {
163 | A1B4010D26EB5937002F4512 = {
164 | CreatedOnToolsVersion = 11.3;
165 | };
166 | };
167 | };
168 | buildConfigurationList = A1B4010926EB5937002F4512 /* Build configuration list for PBXProject "SwiftBoy" */;
169 | compatibilityVersion = "Xcode 9.3";
170 | developmentRegion = en;
171 | hasScannedForEncodings = 0;
172 | knownRegions = (
173 | en,
174 | Base,
175 | );
176 | mainGroup = A1B4010526EB5937002F4512;
177 | packageReferences = (
178 | A11768CF27E55F2600DC4751 /* XCRemoteSwiftPackageReference "AudioKit" */,
179 | A11768D227E55F4100DC4751 /* XCRemoteSwiftPackageReference "SoundpipeAudioKit" */,
180 | );
181 | productRefGroup = A1B4010F26EB5937002F4512 /* Products */;
182 | projectDirPath = "";
183 | projectRoot = "";
184 | targets = (
185 | A1B4010D26EB5937002F4512 /* SwiftBoy */,
186 | );
187 | };
188 | /* End PBXProject section */
189 |
190 | /* Begin PBXResourcesBuildPhase section */
191 | A1B4010C26EB5937002F4512 /* Resources */ = {
192 | isa = PBXResourcesBuildPhase;
193 | buildActionMask = 2147483647;
194 | files = (
195 | A1B4011B26EB593A002F4512 /* Assets.xcassets in Resources */,
196 | A1785E70283BA1D3006CD7AC /* cpu_instrs.gb in Resources */,
197 | A1B4011926EB5937002F4512 /* Main.storyboard in Resources */,
198 | );
199 | runOnlyForDeploymentPostprocessing = 0;
200 | };
201 | /* End PBXResourcesBuildPhase section */
202 |
203 | /* Begin PBXSourcesBuildPhase section */
204 | A1B4010A26EB5937002F4512 /* Sources */ = {
205 | isa = PBXSourcesBuildPhase;
206 | buildActionMask = 2147483647;
207 | files = (
208 | A1B4011626EB5937002F4512 /* ViewController.swift in Sources */,
209 | A12B15FA2774851C00418291 /* Joypad.swift in Sources */,
210 | A1B4014B26EB5DB3002F4512 /* PPU.swift in Sources */,
211 | A1B4011226EB5937002F4512 /* AppDelegate.swift in Sources */,
212 | A1B4014E26EB5DB3002F4512 /* CPU.swift in Sources */,
213 | A13BADBA284613F7001B9FF6 /* FileSystem.swift in Sources */,
214 | A1B4014726EB5DB3002F4512 /* Clock.swift in Sources */,
215 | A1779F0728669826005E6C8C /* GameLibraryManager.swift in Sources */,
216 | A1B4014D26EB5DB3002F4512 /* Common.swift in Sources */,
217 | A13BADBC284636EE001B9FF6 /* UI.swift in Sources */,
218 | A1F3754D2789881C00EDEC23 /* APU.swift in Sources */,
219 | A1B4011426EB5937002F4512 /* SceneDelegate.swift in Sources */,
220 | A1B4014926EB5DB3002F4512 /* Instructions.swift in Sources */,
221 | A1B4014C26EB5DB3002F4512 /* MMU.swift in Sources */,
222 | A1DCD2D52709696B001CF043 /* Timer.swift in Sources */,
223 | A1B4014826EB5DB3002F4512 /* Cartridge.swift in Sources */,
224 | );
225 | runOnlyForDeploymentPostprocessing = 0;
226 | };
227 | /* End PBXSourcesBuildPhase section */
228 |
229 | /* Begin PBXVariantGroup section */
230 | A1B4011726EB5937002F4512 /* Main.storyboard */ = {
231 | isa = PBXVariantGroup;
232 | children = (
233 | A1B4011826EB5937002F4512 /* Base */,
234 | );
235 | name = Main.storyboard;
236 | sourceTree = "";
237 | };
238 | /* End PBXVariantGroup section */
239 |
240 | /* Begin XCBuildConfiguration section */
241 | A1B4012026EB593A002F4512 /* Debug */ = {
242 | isa = XCBuildConfiguration;
243 | buildSettings = {
244 | ALWAYS_SEARCH_USER_PATHS = NO;
245 | CLANG_ANALYZER_NONNULL = YES;
246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
248 | CLANG_CXX_LIBRARY = "libc++";
249 | CLANG_ENABLE_MODULES = YES;
250 | CLANG_ENABLE_OBJC_ARC = YES;
251 | CLANG_ENABLE_OBJC_WEAK = YES;
252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
253 | CLANG_WARN_BOOL_CONVERSION = YES;
254 | CLANG_WARN_COMMA = YES;
255 | CLANG_WARN_CONSTANT_CONVERSION = YES;
256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
259 | CLANG_WARN_EMPTY_BODY = YES;
260 | CLANG_WARN_ENUM_CONVERSION = YES;
261 | CLANG_WARN_INFINITE_RECURSION = YES;
262 | CLANG_WARN_INT_CONVERSION = YES;
263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
267 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
268 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
269 | CLANG_WARN_STRICT_PROTOTYPES = YES;
270 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
271 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
272 | CLANG_WARN_UNREACHABLE_CODE = YES;
273 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
274 | CODE_SIGN_IDENTITY = "Apple Development";
275 | COPY_PHASE_STRIP = NO;
276 | DEBUG_INFORMATION_FORMAT = dwarf;
277 | ENABLE_HARDENED_RUNTIME = NO;
278 | ENABLE_STRICT_OBJC_MSGSEND = YES;
279 | ENABLE_TESTABILITY = YES;
280 | GCC_C_LANGUAGE_STANDARD = gnu11;
281 | GCC_DYNAMIC_NO_PIC = NO;
282 | GCC_NO_COMMON_BLOCKS = YES;
283 | GCC_OPTIMIZATION_LEVEL = 0;
284 | GCC_PREPROCESSOR_DEFINITIONS = (
285 | "DEBUG=1",
286 | "$(inherited)",
287 | );
288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
290 | GCC_WARN_UNDECLARED_SELECTOR = YES;
291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
292 | GCC_WARN_UNUSED_FUNCTION = YES;
293 | GCC_WARN_UNUSED_VARIABLE = YES;
294 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
295 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
296 | MTL_FAST_MATH = YES;
297 | ONLY_ACTIVE_ARCH = YES;
298 | OTHER_CODE_SIGN_FLAGS = "";
299 | SDKROOT = iphoneos;
300 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
301 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
302 | };
303 | name = Debug;
304 | };
305 | A1B4012126EB593A002F4512 /* Release */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ALWAYS_SEARCH_USER_PATHS = NO;
309 | CLANG_ANALYZER_NONNULL = YES;
310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
312 | CLANG_CXX_LIBRARY = "libc++";
313 | CLANG_ENABLE_MODULES = YES;
314 | CLANG_ENABLE_OBJC_ARC = YES;
315 | CLANG_ENABLE_OBJC_WEAK = YES;
316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
317 | CLANG_WARN_BOOL_CONVERSION = YES;
318 | CLANG_WARN_COMMA = YES;
319 | CLANG_WARN_CONSTANT_CONVERSION = YES;
320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
323 | CLANG_WARN_EMPTY_BODY = YES;
324 | CLANG_WARN_ENUM_CONVERSION = YES;
325 | CLANG_WARN_INFINITE_RECURSION = YES;
326 | CLANG_WARN_INT_CONVERSION = YES;
327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
331 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
332 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
333 | CLANG_WARN_STRICT_PROTOTYPES = YES;
334 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
335 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
336 | CLANG_WARN_UNREACHABLE_CODE = YES;
337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
338 | CODE_SIGN_IDENTITY = "Apple Development";
339 | COPY_PHASE_STRIP = NO;
340 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
341 | ENABLE_HARDENED_RUNTIME = NO;
342 | ENABLE_NS_ASSERTIONS = NO;
343 | ENABLE_STRICT_OBJC_MSGSEND = YES;
344 | GCC_C_LANGUAGE_STANDARD = gnu11;
345 | GCC_NO_COMMON_BLOCKS = YES;
346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
348 | GCC_WARN_UNDECLARED_SELECTOR = YES;
349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
350 | GCC_WARN_UNUSED_FUNCTION = YES;
351 | GCC_WARN_UNUSED_VARIABLE = YES;
352 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
353 | MTL_ENABLE_DEBUG_INFO = NO;
354 | MTL_FAST_MATH = YES;
355 | OTHER_CODE_SIGN_FLAGS = "";
356 | SDKROOT = iphoneos;
357 | SWIFT_COMPILATION_MODE = wholemodule;
358 | SWIFT_OPTIMIZATION_LEVEL = "-O";
359 | VALIDATE_PRODUCT = YES;
360 | };
361 | name = Release;
362 | };
363 | A1B4012326EB593A002F4512 /* Debug */ = {
364 | isa = XCBuildConfiguration;
365 | buildSettings = {
366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
367 | CODE_SIGN_IDENTITY = "Apple Development";
368 | CODE_SIGN_STYLE = Automatic;
369 | DEVELOPMENT_TEAM = QN6STV2X25;
370 | ENABLE_HARDENED_RUNTIME = NO;
371 | INFOPLIST_FILE = SwiftBoy/Info.plist;
372 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
373 | LD_RUNPATH_SEARCH_PATHS = (
374 | "$(inherited)",
375 | "@executable_path/Frameworks",
376 | );
377 | OTHER_CODE_SIGN_FLAGS = "";
378 | PRODUCT_BUNDLE_IDENTIFIER = com.borisBerak.SwiftBoy;
379 | PRODUCT_NAME = "$(TARGET_NAME)";
380 | PROVISIONING_PROFILE_SPECIFIER = "";
381 | SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off;
382 | SWIFT_VERSION = 5.0;
383 | TARGETED_DEVICE_FAMILY = "1,2";
384 | };
385 | name = Debug;
386 | };
387 | A1B4012426EB593A002F4512 /* Release */ = {
388 | isa = XCBuildConfiguration;
389 | buildSettings = {
390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
391 | CODE_SIGN_IDENTITY = "Apple Development";
392 | CODE_SIGN_STYLE = Automatic;
393 | DEVELOPMENT_TEAM = QN6STV2X25;
394 | ENABLE_HARDENED_RUNTIME = NO;
395 | INFOPLIST_FILE = SwiftBoy/Info.plist;
396 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
397 | LD_RUNPATH_SEARCH_PATHS = (
398 | "$(inherited)",
399 | "@executable_path/Frameworks",
400 | );
401 | OTHER_CODE_SIGN_FLAGS = "";
402 | PRODUCT_BUNDLE_IDENTIFIER = com.borisBerak.SwiftBoy;
403 | PRODUCT_NAME = "$(TARGET_NAME)";
404 | PROVISIONING_PROFILE_SPECIFIER = "";
405 | SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off;
406 | SWIFT_VERSION = 5.0;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | };
409 | name = Release;
410 | };
411 | /* End XCBuildConfiguration section */
412 |
413 | /* Begin XCConfigurationList section */
414 | A1B4010926EB5937002F4512 /* Build configuration list for PBXProject "SwiftBoy" */ = {
415 | isa = XCConfigurationList;
416 | buildConfigurations = (
417 | A1B4012026EB593A002F4512 /* Debug */,
418 | A1B4012126EB593A002F4512 /* Release */,
419 | );
420 | defaultConfigurationIsVisible = 0;
421 | defaultConfigurationName = Release;
422 | };
423 | A1B4012226EB593A002F4512 /* Build configuration list for PBXNativeTarget "SwiftBoy" */ = {
424 | isa = XCConfigurationList;
425 | buildConfigurations = (
426 | A1B4012326EB593A002F4512 /* Debug */,
427 | A1B4012426EB593A002F4512 /* Release */,
428 | );
429 | defaultConfigurationIsVisible = 0;
430 | defaultConfigurationName = Release;
431 | };
432 | /* End XCConfigurationList section */
433 |
434 | /* Begin XCRemoteSwiftPackageReference section */
435 | A11768CF27E55F2600DC4751 /* XCRemoteSwiftPackageReference "AudioKit" */ = {
436 | isa = XCRemoteSwiftPackageReference;
437 | repositoryURL = "https://github.com/AudioKit/AudioKit.git";
438 | requirement = {
439 | kind = upToNextMajorVersion;
440 | minimumVersion = 5.0.0;
441 | };
442 | };
443 | A11768D227E55F4100DC4751 /* XCRemoteSwiftPackageReference "SoundpipeAudioKit" */ = {
444 | isa = XCRemoteSwiftPackageReference;
445 | repositoryURL = "https://github.com/AudioKit/SoundpipeAudioKit.git";
446 | requirement = {
447 | kind = upToNextMajorVersion;
448 | minimumVersion = 5.0.0;
449 | };
450 | };
451 | /* End XCRemoteSwiftPackageReference section */
452 |
453 | /* Begin XCSwiftPackageProductDependency section */
454 | A11768D027E55F2600DC4751 /* AudioKit */ = {
455 | isa = XCSwiftPackageProductDependency;
456 | package = A11768CF27E55F2600DC4751 /* XCRemoteSwiftPackageReference "AudioKit" */;
457 | productName = AudioKit;
458 | };
459 | A11768D327E55F4100DC4751 /* SoundpipeAudioKit */ = {
460 | isa = XCSwiftPackageProductDependency;
461 | package = A11768D227E55F4100DC4751 /* XCRemoteSwiftPackageReference "SoundpipeAudioKit" */;
462 | productName = SoundpipeAudioKit;
463 | };
464 | /* End XCSwiftPackageProductDependency section */
465 | };
466 | rootObject = A1B4010626EB5937002F4512 /* Project object */;
467 | }
468 |
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "audiokit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/AudioKit/AudioKit.git",
7 | "state" : {
8 | "revision" : "cb50a6878535735fd1f10c8b8e5e96864630836c",
9 | "version" : "5.5.4"
10 | }
11 | },
12 | {
13 | "identity" : "audiokitex",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/AudioKit/AudioKitEX",
16 | "state" : {
17 | "revision" : "22183bcf7e8c6baf15e59c1126a4c541e044cec5",
18 | "version" : "5.5.2"
19 | }
20 | },
21 | {
22 | "identity" : "kissfft",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/AudioKit/KissFFT",
25 | "state" : {
26 | "revision" : "dd0636e151724b8ba2e0908eba4d99a6ff24d00c",
27 | "version" : "1.0.0"
28 | }
29 | },
30 | {
31 | "identity" : "soundpipeaudiokit",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/AudioKit/SoundpipeAudioKit.git",
34 | "state" : {
35 | "revision" : "c9349accd3c9e102a35962cf40c998eacb6108d5",
36 | "version" : "5.5.3"
37 | }
38 | }
39 | ],
40 | "version" : 2
41 | }
42 |
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/xcshareddata/xcschemes/Swift Boy Release.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/SwiftBoy.xcodeproj/xcshareddata/xcschemes/Swift Boy.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/SwiftBoy/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
6 | // Override point for customization after application launch.
7 | return true
8 | }
9 |
10 | // MARK: UISceneSession Lifecycle
11 |
12 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
13 | // Called when a new scene session is being created.
14 | // Use this method to select a configuration to create the new scene with.
15 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
16 | }
17 |
18 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
19 | // Called when the user discards a scene session.
20 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
21 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x-1.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@3x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x-1.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x-1.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon76x76.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x.png
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AppIcon20x20@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "AppIcon20x20@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "AppIcon29x29@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "AppIcon29x29@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "AppIcon40x40@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "AppIcon40x40@3x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "AppIcon60x60@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "AppIcon60x60@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "AppIcon20x20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "AppIcon20x20@2x-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "AppIcon29x29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "AppIcon29x29@2x-1.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "AppIcon40x40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "AppIcon40x40@2x-1.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "AppIcon76x76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "AppIcon76x76@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "AppIcon83.5x83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "AppIcon1024x1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Assets.xcassets/LaunchScreenColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.000",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/APU.swift:
--------------------------------------------------------------------------------
1 | // TODO: Figure out a good default for master volume 🤔
2 | // TODO: A weird noise can be heard as soon as the GB is turned on
3 | // TODO: Tetris main menu and Type-B music is not sounding correct
4 | // TODO: Still need to implement the noise voice properly
5 |
6 | import Foundation
7 | import AudioKit
8 | import SoundpipeAudioKit
9 |
10 | typealias AKAmplitudeEnvelope = SoundpipeAudioKit.AmplitudeEnvelope
11 |
12 | func bitsToFrequency(bits: UInt16) -> Float {
13 | return 131072 / (2048 - Float(bits))
14 | }
15 |
16 | func frequencyToBits(frequency: Float) -> UInt16 {
17 | if frequency == 0 {
18 | return 0
19 | }
20 |
21 | return UInt16(2048 - (131072 / frequency))
22 | }
23 |
24 | func convertToSweepTime(byte: UInt8) -> Float {
25 | switch (byte) {
26 | case 0b000: return 0
27 | case 0b001: return 0.0078
28 | case 0b010: return 0.0156
29 | case 0b011: return 0.0234
30 | case 0b100: return 0.0313
31 | case 0b101: return 0.0391
32 | case 0b110: return 0.0469
33 | case 0b111: return 0.0547
34 | default: return -1
35 | }
36 | }
37 |
38 | func convertToPulseWidth(byte: UInt8) -> Float {
39 | switch (byte) {
40 | case 0b00000000: return 0.125
41 | case 0b01000000: return 0.25
42 | case 0b10000000: return 0.5
43 | case 0b11000000: return 0.75
44 | default: return -1
45 | }
46 | }
47 |
48 | protocol OscillatorNode: Node {
49 | var frequency: Float { get set }
50 | var amplitude: Float { get set }
51 | func rampFrequency(to: Float, duration: Float)
52 | func rampAmplitude(to: Float, duration: Float)
53 | }
54 |
55 | protocol Envelope: AnyObject {
56 | var voice: Voice? { get set }
57 | func update(seconds: Float) -> Void
58 | func onTriggered() -> Void
59 | }
60 |
61 | class Voice {
62 | let oscillator: OscillatorNode
63 | let envelopes: [Envelope]
64 |
65 | var dacEnabled: Bool = true
66 | var enabled: Bool = true
67 | var amplitude: Float = 0
68 | var frequency: Float = 0
69 |
70 | var triggered: Bool = false {
71 | didSet {
72 | onTriggered()
73 | }
74 | }
75 |
76 | init(oscillator: OscillatorNode, envelopes: [Envelope]) {
77 | self.oscillator = oscillator
78 | self.oscillator.start()
79 | self.oscillator.amplitude = 0
80 | self.oscillator.frequency = 0
81 | self.envelopes = envelopes
82 | self.envelopes.forEach { $0.voice = self }
83 | }
84 |
85 | func onTriggered() {
86 | enabled = true
87 | envelopes.forEach { $0.onTriggered() }
88 | }
89 |
90 | func update(seconds: Float) -> Bool {
91 | envelopes.forEach { $0.update(seconds: seconds) }
92 |
93 | oscillator.rampFrequency(to: frequency, duration: 0.01)
94 |
95 | if !dacEnabled {
96 | oscillator.rampAmplitude(to: 0, duration: 0.01)
97 | } else if !enabled {
98 | oscillator.rampAmplitude(to: 0, duration: 0.01)
99 | } else {
100 | oscillator.rampAmplitude(to: amplitude, duration: 0.01)
101 | }
102 |
103 | return dacEnabled && enabled
104 | }
105 | }
106 |
107 | class Synthesizer {
108 | let engine: AudioEngine
109 | let channelsLeft: [AKAmplitudeEnvelope]
110 | let channelsRight: [AKAmplitudeEnvelope]
111 | let mixerLeft: Mixer
112 | let mixerRight: Mixer
113 | let mixerMain: Mixer
114 |
115 | public var volume: Float {
116 | get {
117 | self.mixerMain.volume
118 | }
119 | set {
120 | self.mixerMain.volume = newValue
121 | }
122 | }
123 |
124 | var enabled = false {
125 | didSet {
126 | if enabled && !engine.avEngine.isRunning {
127 | try? self.engine.start()
128 | }
129 |
130 | if !enabled && engine.avEngine.isRunning {
131 | self.engine.pause()
132 | }
133 | }
134 | }
135 |
136 | init(volume: Float = 0.5, voices: [Voice]) {
137 | self.channelsLeft = voices.map { AKAmplitudeEnvelope($0.oscillator) }
138 | self.channelsLeft.forEach { $0.openGate() }
139 | self.channelsRight = voices.map { AKAmplitudeEnvelope($0.oscillator) }
140 | self.channelsRight.forEach { $0.openGate() }
141 | self.mixerLeft = Mixer(self.channelsLeft)
142 | self.mixerLeft.pan = -1
143 | self.mixerRight = Mixer(self.channelsRight)
144 | self.mixerRight.pan = 1
145 | self.mixerMain = Mixer(self.mixerLeft, self.mixerRight)
146 | self.mixerMain.volume = volume
147 | self.engine = AudioEngine()
148 | self.engine.output = self.mixerMain
149 | }
150 |
151 | func setLeftChannelVolume(_ val: Float) {
152 | self.mixerLeft.volume = val
153 | }
154 |
155 | func setRightChannelVolume(_ val: Float) {
156 | self.mixerRight.volume = val
157 | }
158 |
159 | func enableChannels(index: Int, left: Bool, right: Bool) {
160 | let channelLeft = self.channelsLeft[index]
161 | let channelRight = self.channelsRight[index]
162 |
163 | left ? channelLeft.openGate() : channelLeft.closeGate()
164 | right ? channelRight.openGate() : channelRight.closeGate()
165 | }
166 | }
167 |
168 | class LengthEnvelope: Envelope {
169 | var voice: Voice?
170 | var maxDuration: Float = 0
171 | var enabled = false
172 | var duration: Float = 0
173 |
174 | init(voice: Voice? = nil) {
175 | self.voice = voice
176 | }
177 |
178 | func update(seconds: Float) {
179 | if !enabled {
180 | return
181 | }
182 |
183 | if duration > 0 {
184 | duration -= seconds
185 |
186 | if duration <= 0 {
187 | voice?.enabled = false
188 | }
189 | }
190 | }
191 |
192 | func onTriggered() {
193 | if duration <= 0 {
194 | duration = maxDuration
195 | }
196 | }
197 | }
198 |
199 | class AmplitudeEnvelope: Envelope {
200 | private var elapsedTime: Float = 0
201 |
202 | var voice: Voice?
203 | var startStep: Int = 0
204 | var increasing = false
205 | var stepDuration: Float = 0
206 |
207 | init(_ voice: Voice? = nil) {
208 | self.voice = voice
209 | }
210 |
211 | func update(seconds: Float) {
212 | if stepDuration == 0 {
213 | return
214 | }
215 |
216 | elapsedTime += seconds
217 |
218 | let deltaSteps = Int(elapsedTime / stepDuration) * (increasing ? 1 : -1)
219 | let currentStep = (startStep + deltaSteps).clamp(min: 0, max: 0x0F)
220 |
221 | voice?.amplitude = Float(currentStep) / 0x0F
222 | }
223 |
224 | func onTriggered() {
225 | elapsedTime = 0
226 | voice?.amplitude = Float(startStep) / 0x0F
227 | }
228 | }
229 |
230 | class FrequencySweepEnvelope: Envelope {
231 | private var elapsedTime: Float = 0
232 | private var adjustedFrequency: Float = 0 {
233 | didSet {
234 | voice?.frequency = adjustedFrequency
235 | }
236 | }
237 |
238 | var voice: Voice?
239 | var startFrequency: Float = 0
240 | var sweepIncreasing = false
241 | var sweepShifts: UInt8 = 0
242 | var sweepTime: Float = 0
243 |
244 | init(_ voice: Voice? = nil) {
245 | self.voice = voice
246 | }
247 |
248 | func update(seconds: Float) {
249 | if sweepTime == 0 {
250 | return
251 | }
252 |
253 | if sweepShifts == 0 {
254 | return
255 | }
256 |
257 | elapsedTime += seconds
258 |
259 | let sweeps = Int(elapsedTime / sweepTime)
260 | let totalShifts = sweeps > 0 ? sweeps * Int(sweepShifts) : 0
261 | let shiftedValue = sweepIncreasing ? frequencyToBits(frequency: startFrequency) << totalShifts : frequencyToBits(frequency: startFrequency) >> totalShifts
262 |
263 | if shiftedValue == 0 {
264 | return
265 | }
266 |
267 | if shiftedValue > 2047 {
268 | return
269 | }
270 |
271 | adjustedFrequency = bitsToFrequency(bits: shiftedValue)
272 | }
273 |
274 | func onTriggered() {
275 | elapsedTime = 0
276 | adjustedFrequency = startFrequency
277 | }
278 | }
279 |
280 | extension PWMOscillator: OscillatorNode {
281 | func rampFrequency(to: Float, duration: Float) {
282 | $frequency.ramp(to: to, duration: duration)
283 | }
284 |
285 | func rampAmplitude(to: Float, duration: Float) {
286 | $amplitude.ramp(to: to, duration: duration)
287 | }
288 | }
289 |
290 | class Pulse: Voice {
291 | let lengthEnvelope = LengthEnvelope()
292 | let amplitudeEnvelope = AmplitudeEnvelope()
293 |
294 | var pulseWidth: Float = 0 {
295 | didSet {
296 | if pulseWidth != oldValue {
297 | if let pwm = oscillator as? PWMOscillator {
298 | pwm.pulseWidth = pulseWidth
299 | }
300 | }
301 | }
302 | }
303 |
304 | init(maxDuration: Float) {
305 | super.init(oscillator: PWMOscillator(), envelopes: [self.lengthEnvelope, self.amplitudeEnvelope])
306 |
307 | self.lengthEnvelope.maxDuration = maxDuration
308 | }
309 | }
310 |
311 | class PulseWithSweep: Voice {
312 | let lengthEnvelope = LengthEnvelope()
313 | let amplitudeEnvelope = AmplitudeEnvelope()
314 | let frequencySweepEnvelope = FrequencySweepEnvelope()
315 |
316 | var pulseWidth: Float = 0 {
317 | didSet {
318 | if pulseWidth != oldValue {
319 | if let pwm = oscillator as? PWMOscillator {
320 | pwm.pulseWidth = pulseWidth
321 | }
322 | }
323 | }
324 | }
325 |
326 | init(maxDuration: Float) {
327 | super.init(oscillator: PWMOscillator(), envelopes: [self.lengthEnvelope, self.amplitudeEnvelope, self.frequencySweepEnvelope])
328 |
329 | self.lengthEnvelope.maxDuration = maxDuration
330 | }
331 | }
332 |
333 | extension DynamicOscillator: OscillatorNode {
334 | func rampFrequency(to: Float, duration: Float) {
335 | $frequency.ramp(to: to, duration: duration)
336 | }
337 |
338 | func rampAmplitude(to: Float, duration: Float) {
339 | $amplitude.ramp(to: to, duration: duration)
340 | }
341 | }
342 |
343 | class CustomWave: Voice {
344 | let lengthEnvelope = LengthEnvelope()
345 |
346 | var data = [Float]() {
347 | didSet {
348 | if data != oldValue {
349 | if let dyn = oscillator as? DynamicOscillator {
350 | dyn.setWaveform(Table(data))
351 | }
352 | }
353 | }
354 | }
355 |
356 | init(maxDuration: Float) {
357 | super.init(oscillator: DynamicOscillator(waveform: Table(.sine)), envelopes: [self.lengthEnvelope])
358 |
359 | self.lengthEnvelope.maxDuration = maxDuration
360 | }
361 | }
362 |
363 | extension WhiteNoise: OscillatorNode {
364 | var frequency: Float {
365 | get { 0 }
366 | set { }
367 | }
368 |
369 | func rampFrequency(to: Float, duration: Float) { }
370 |
371 | func rampAmplitude(to: Float, duration: Float) {
372 | $amplitude.ramp(to: to, duration: duration)
373 | }
374 | }
375 |
376 | class Noise: Voice {
377 | let lengthEnvelope = LengthEnvelope()
378 | let amplitudeEnvelope = AmplitudeEnvelope()
379 |
380 | init(maxDuration: Float) {
381 | super.init(oscillator: WhiteNoise(), envelopes: [self.lengthEnvelope, self.amplitudeEnvelope])
382 |
383 | self.lengthEnvelope.maxDuration = maxDuration
384 | }
385 | }
386 |
387 | public class APU {
388 | private let mmu: MMU
389 | private let master: Synthesizer
390 | private let pulseA: PulseWithSweep
391 | private let pulseB: Pulse
392 | private let customWave: CustomWave
393 | private let noise: Noise
394 | private var waveformDataMemo = Memo<[Float]>()
395 |
396 | init(_ mmu: MMU) {
397 | self.mmu = mmu
398 | self.pulseA = PulseWithSweep(maxDuration: 0.25)
399 | self.pulseB = Pulse(maxDuration: 0.25)
400 | self.customWave = CustomWave(maxDuration: 1.0)
401 | self.noise = Noise(maxDuration: 0.25)
402 | self.master = Synthesizer(voices: [self.pulseA, self.pulseB, self.customWave, self.noise])
403 | self.master.volume = 0.125
404 |
405 | self.wireUpPulseA()
406 | self.wireUpPulseB()
407 | self.wireUpCustomWave()
408 | self.wireUpNoise()
409 | self.wireUpMaster()
410 | }
411 |
412 | func wireUpPulseA() {
413 | self.mmu.nr10.subscribe { nr10 in
414 | let sweepShifts = nr10 & 0b00000111
415 | let sweepIncreasing = nr10.bit(3)
416 | let sweepTime = convertToSweepTime(byte: (nr10 & 0b01110000) >> 4)
417 |
418 | self.pulseA.frequencySweepEnvelope.sweepShifts = sweepShifts
419 | self.pulseA.frequencySweepEnvelope.sweepIncreasing = sweepIncreasing
420 | self.pulseA.frequencySweepEnvelope.sweepTime = sweepTime
421 | }
422 |
423 | self.mmu.nr11.subscribe { nr11 in
424 | let pulseWidth = convertToPulseWidth(byte: nr11 & 0b11000000)
425 | let lengthEnvelopDuration = (64 - Float(nr11 & 0b00111111)) * (1 / 256)
426 |
427 | self.pulseA.pulseWidth = pulseWidth
428 | self.pulseA.lengthEnvelope.duration = lengthEnvelopDuration
429 | }
430 |
431 | self.mmu.nr12.subscribe { nr12 in
432 | let amplitudeEnvelopeStartStep = Int((nr12 & 0b11110000) >> 4)
433 | let amplitudeEnvelopeStepDuration = Float(nr12 & 0b00000111) * (1 / 64)
434 | let amplitudeEnvelopeIncreasing = nr12.bit(3)
435 | let dacEnabled = (nr12 & 0b11111000) != 0
436 |
437 | self.pulseA.dacEnabled = dacEnabled
438 | self.pulseA.amplitudeEnvelope.startStep = amplitudeEnvelopeStartStep
439 | self.pulseA.amplitudeEnvelope.stepDuration = amplitudeEnvelopeStepDuration
440 | self.pulseA.amplitudeEnvelope.increasing = amplitudeEnvelopeIncreasing
441 | }
442 |
443 | self.mmu.nr13.subscribe { nr13 in
444 | let bits = frequencyToBits(frequency: self.pulseA.frequencySweepEnvelope.startFrequency)
445 | let lsb = UInt16(nr13)
446 | let msb = bits & 0xFF00
447 | let frequency = bitsToFrequency(bits: lsb + msb)
448 |
449 | self.pulseA.frequencySweepEnvelope.startFrequency = frequency
450 | }
451 |
452 | self.mmu.nr14.subscribe { nr14 in
453 | let bits = frequencyToBits(frequency: self.pulseA.frequencySweepEnvelope.startFrequency)
454 | let lsb = bits & 0x00FF
455 | let msb = UInt16(nr14 & 0b00000111) << 8
456 | let frequency = bitsToFrequency(bits: lsb + msb)
457 | let lengthEnvelopEnabled = nr14.bit(6)
458 | let triggered = nr14.bit(7)
459 |
460 | self.pulseA.frequencySweepEnvelope.startFrequency = frequency
461 | self.pulseA.triggered = triggered
462 | self.pulseA.lengthEnvelope.enabled = lengthEnvelopEnabled
463 | }
464 | }
465 |
466 | func wireUpPulseB() {
467 | self.mmu.nr21.subscribe { nr21 in
468 | let pulseWidth = convertToPulseWidth(byte: nr21 & 0b11000000)
469 | let lengthEnvelopeDuration = (64 - Float(nr21 & 0b00111111)) * (1 / 256)
470 |
471 | self.pulseB.pulseWidth = pulseWidth
472 | self.pulseB.lengthEnvelope.duration = lengthEnvelopeDuration
473 | }
474 |
475 | self.mmu.nr22.subscribe { nr22 in
476 | let amplitudeEnvelopeStartStep = Int((nr22 & 0b11110000) >> 4)
477 | let amplitudeEnvelopeStepDuration = Float(nr22 & 0b00000111) * (1 / 64)
478 | let amplitudeEnvelopeIncreasing = nr22.bit(3)
479 | let dacEnabled = (nr22 & 0b11111000) != 0
480 |
481 | self.pulseB.dacEnabled = dacEnabled
482 | self.pulseB.amplitudeEnvelope.startStep = amplitudeEnvelopeStartStep
483 | self.pulseB.amplitudeEnvelope.stepDuration = amplitudeEnvelopeStepDuration
484 | self.pulseB.amplitudeEnvelope.increasing = amplitudeEnvelopeIncreasing
485 | }
486 |
487 | self.mmu.nr23.subscribe { nr23 in
488 | let bits = frequencyToBits(frequency: self.pulseB.frequency)
489 | let lsb = UInt16(nr23)
490 | let msb = bits & 0xFF00
491 | let frequency = bitsToFrequency(bits: lsb + msb)
492 |
493 | self.pulseB.frequency = frequency
494 | }
495 |
496 | self.mmu.nr24.subscribe { nr24 in
497 | let bits = frequencyToBits(frequency: self.pulseB.frequency)
498 | let lsb = bits & 0x00FF
499 | let msb = UInt16(nr24 & 0b00000111) << 8
500 | let frequency = bitsToFrequency(bits: lsb + msb)
501 | let lengthEnvelopeEnabled = nr24.bit(6)
502 | let triggered = nr24.bit(7)
503 |
504 | self.pulseB.triggered = triggered
505 | self.pulseB.frequency = frequency
506 | self.pulseB.lengthEnvelope.enabled = lengthEnvelopeEnabled
507 | }
508 | }
509 |
510 | func wireUpCustomWave() {
511 | self.mmu.nr30.subscribe { nr30 in
512 | let dacEnabled = nr30.bit(7)
513 |
514 | self.customWave.dacEnabled = dacEnabled
515 | }
516 |
517 | self.mmu.nr31.subscribe { nr31 in
518 | let lengthEnvelopeDuration = (256 - Float(nr31)) * (1 / 256)
519 |
520 | self.customWave.lengthEnvelope.duration = lengthEnvelopeDuration
521 | }
522 |
523 | self.mmu.nr33.subscribe { nr33 in
524 | let bits = frequencyToBits(frequency: self.pulseB.frequency)
525 | let lsb = UInt16(nr33)
526 | let msb = bits & 0xFF00
527 | let frequency = bitsToFrequency(bits: lsb + msb)
528 |
529 | self.customWave.frequency = frequency
530 | }
531 |
532 | self.mmu.nr34.subscribe { nr34 in
533 | let bits = frequencyToBits(frequency: self.pulseB.frequency)
534 | let lsb = bits & 0x00FF
535 | let msb = UInt16(nr34 & 0b00000111) << 8
536 | let frequency = bitsToFrequency(bits: lsb + msb)
537 | let lengthEnvelopEnabled = nr34.bit(6)
538 | let triggered = nr34.bit(7)
539 |
540 | self.customWave.triggered = triggered
541 | self.customWave.frequency = frequency
542 | self.customWave.lengthEnvelope.enabled = lengthEnvelopEnabled
543 | }
544 | }
545 |
546 | func wireUpNoise() {
547 | self.mmu.nr41.subscribe { nr41 in
548 | let lengthEnvelopeDuration = (64 - Float(nr41 & 0b00111111)) * (1 / 256)
549 |
550 | self.noise.lengthEnvelope.duration = lengthEnvelopeDuration
551 | }
552 |
553 | self.mmu.nr42.subscribe { nr42 in
554 | let amplitudeEnvelopeStartStep = Int((nr42 & 0b11110000) >> 4)
555 | let amplitudeEnvelopeStepDuration = Float(nr42 & 0b00000111) * 1 / 64
556 | let amplitudeEnvelopeIncreasing = nr42.bit(3)
557 | let dacEnabled = (nr42 & 0b11111000) != 0
558 |
559 | self.noise.dacEnabled = dacEnabled
560 | self.noise.amplitudeEnvelope.startStep = amplitudeEnvelopeStartStep
561 | self.noise.amplitudeEnvelope.stepDuration = amplitudeEnvelopeStepDuration
562 | self.noise.amplitudeEnvelope.increasing = amplitudeEnvelopeIncreasing
563 | }
564 |
565 | self.mmu.nr43.subscribe { nr43 in
566 | let temp = nr43 & 0b00000111
567 | let r = temp == 0 ? Float(0.5) : Float(temp)
568 | let s = Float(nr43 & 0b11100000)
569 | let frequency = Float(524288) / r / powf(2, s + 1.0)
570 |
571 | self.noise.frequency = frequency
572 | }
573 |
574 | self.mmu.nr44.subscribe { nr44 in
575 | let lengthEnvelopeEnabled = nr44.bit(6)
576 | let triggered = nr44.bit(7)
577 |
578 | self.noise.triggered = triggered
579 | self.noise.lengthEnvelope.enabled = lengthEnvelopeEnabled
580 | }
581 | }
582 |
583 | func wireUpMaster() {
584 | // Set left and right channel volumes
585 | self.mmu.nr50.subscribe { nr50 in
586 | self.master.setLeftChannelVolume(Float((nr50 & 0b01110000) >> 4) / 7.0)
587 | self.master.setRightChannelVolume(Float(nr50 & 0b00000111) / 7.0)
588 | }
589 |
590 | // Enabled left or right channel output for each voice
591 | self.mmu.nr51.subscribe { nr51 in
592 | self.master.enableChannels(index: 0, left: nr51.bit(4), right: nr51.bit(0))
593 | self.master.enableChannels(index: 1, left: nr51.bit(5), right: nr51.bit(1))
594 | self.master.enableChannels(index: 2, left: nr51.bit(6), right: nr51.bit(2))
595 | self.master.enableChannels(index: 3, left: nr51.bit(7), right: nr51.bit(3))
596 | }
597 |
598 | // Master sound output
599 | self.mmu.nr52.subscribe { nr52 in
600 | self.master.enabled = nr52.bit(7)
601 | }
602 | }
603 |
604 | func updateWaveformSamples() {
605 | let nr32 = self.mmu.nr32.read()
606 | let outputLevel = (nr32 & 0b01100000) >> 5
607 | let waveformData = self.waveformDataMemo.get(deps: [self.mmu.waveformRam.version, outputLevel]) {
608 | let buffer = self.mmu.waveformRam.buffer
609 | return buffer.flatMap({ [Float($0.nibble(1) >> outputLevel) / 0b1111, Float($0.nibble(0) >> outputLevel) / 0b1111 ] }).map({ $0 * 2 - 1 })
610 | }
611 |
612 | self.customWave.data = waveformData
613 | self.customWave.amplitude = 1
614 | }
615 |
616 | public func run(seconds: Float) throws {
617 | if !self.master.enabled {
618 | return
619 | }
620 |
621 | // Update custom waveform samples
622 | self.updateWaveformSamples()
623 |
624 | // Update all voices
625 | var nr52 = self.mmu.nr52.read()
626 |
627 | nr52[0] = self.pulseA.update(seconds: seconds)
628 | nr52[1] = self.pulseB.update(seconds: seconds)
629 | nr52[2] = self.customWave.update(seconds: seconds)
630 | nr52[3] = self.noise.update(seconds: seconds)
631 |
632 | // Snippet to determine which voices are producing sound
633 | // print(nr52[0] && self.pulseA.amplitude > 0, nr52[1] && self.pulseB.amplitude > 0, nr52[2] && self.customWave.amplitude > 0, nr52[3] && self.noise.amplitude > 0)
634 |
635 | // Write nr52 back into RAM
636 | self.mmu.nr52.write(nr52, publish: false)
637 | }
638 |
639 | public func reset() {
640 | waveformDataMemo.invalidate()
641 | }
642 | }
643 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/CPU.swift:
--------------------------------------------------------------------------------
1 | // TODO: Only execute a cmd if you have accumulated enough cycles?
2 | // TODO: if cmd.cycles > cycles {
3 | // TODO: queue.insert(cmd, at: 0)
4 | // TODO: return
5 | // TODO: }
6 |
7 | struct Instruction {
8 | internal var toCommand: (CPU) throws -> Command
9 |
10 | init(toCommand: @escaping (CPU) throws -> Command) {
11 | self.toCommand = toCommand
12 | }
13 |
14 | static func atomic(cycles: UInt16, command: @escaping (CPU) throws -> Void) -> Instruction {
15 | return Instruction { cpu in
16 | return Command(cycles: cycles) {
17 | try command(cpu)
18 | return nil
19 | }
20 | }
21 | }
22 | }
23 |
24 | class Flags {
25 | internal var zero = false
26 | internal var subtract = false
27 | internal var halfCarry = false
28 | internal var carry = false
29 |
30 | func clear() {
31 | zero = false
32 | subtract = false
33 | halfCarry = false
34 | carry = false
35 | }
36 |
37 | func set(byte: UInt8) {
38 | zero = (byte & 0b10000000) != 0
39 | subtract = (byte & 0b01000000) != 0
40 | halfCarry = (byte & 0b00100000) != 0
41 | carry = (byte & 0b00010000) != 0
42 | }
43 |
44 | func toUInt8() -> UInt8 {
45 | var result: UInt8 = 0;
46 | let arr = [zero, subtract, halfCarry, carry]
47 |
48 | arr.forEach {
49 | if $0 {
50 | result+=1
51 | }
52 |
53 | result<<=1
54 | }
55 |
56 | result<<=3
57 |
58 | return result
59 | }
60 | }
61 |
62 | enum OpCode: Hashable {
63 | case byte(UInt8)
64 | case word(UInt8)
65 | }
66 |
67 | enum CPUError: Error {
68 | case instructionNotFound(OpCode)
69 | case instructionNotImplemented(OpCode)
70 | }
71 |
72 | public struct Interrupts {
73 | static let vBlank = (bit: UInt8(0), address: UInt16(0x0040))
74 | static let lcdStat = (bit: UInt8(1), address: UInt16(0x0048))
75 | static let timer = (bit: UInt8(2), address: UInt16(0x0050))
76 | static let serial = (bit: UInt8(3), address: UInt16(0x0058))
77 | static let joypad = (bit: UInt8(4), address: UInt16(0x0060))
78 | static let prioritized = [Interrupts.vBlank, Interrupts.lcdStat, Interrupts.timer, Interrupts.serial, Interrupts.joypad]
79 | }
80 |
81 | public class CPU {
82 | internal let mmu: MMU
83 | internal let flags: Flags = Flags()
84 | internal var a: UInt8 = 0
85 | internal var b: UInt8 = 0
86 | internal var c: UInt8 = 0
87 | internal var d: UInt8 = 0
88 | internal var e: UInt8 = 0
89 | internal var h: UInt8 = 0
90 | internal var l: UInt8 = 0
91 | internal var sp: UInt16 = 0x0000
92 | internal var pc: UInt16 = 0x0000
93 | internal var ime: Bool = false
94 | private var queue: [Command] = []
95 | private var cycles: Int16 = 0
96 | public var printOpcodes = false
97 | public var enabled = true
98 |
99 | internal var af: UInt16 {
100 | get {
101 | return [flags.toUInt8(), a].toWord()
102 | }
103 |
104 | set {
105 | let bytes = newValue.toBytes()
106 | a = bytes[1]
107 | flags.set(byte: bytes[0])
108 | }
109 | }
110 |
111 | internal var bc: UInt16 {
112 | get {
113 | return [c, b].toWord()
114 | }
115 |
116 | set {
117 | let bytes = newValue.toBytes()
118 | b = bytes[1]
119 | c = bytes[0]
120 | }
121 | }
122 |
123 | internal var de: UInt16 {
124 | get {
125 | return [e, d].toWord()
126 | }
127 |
128 | set {
129 | let bytes = newValue.toBytes()
130 | d = bytes[1]
131 | e = bytes[0]
132 | }
133 | }
134 |
135 | internal var hl: UInt16 {
136 | get {
137 | return [l, h].toWord()
138 | }
139 |
140 | set {
141 | let bytes = newValue.toBytes()
142 | h = bytes[1]
143 | l = bytes[0]
144 | }
145 | }
146 |
147 | public init(_ mmu: MMU) {
148 | self.mmu = mmu
149 |
150 | self.mmu.interruptFlags.subscribe { flags in
151 | if flags > 0 && self.enabled == false {
152 | self.enabled = flags & self.mmu.interruptsEnabled.read() > 0
153 | }
154 | }
155 | }
156 |
157 | func readNextByte() throws -> UInt8 {
158 | let byte = try mmu.readByte(address: pc)
159 | pc = pc &+ 1
160 |
161 | return byte
162 | }
163 |
164 | func readNextWord() throws -> UInt16 {
165 | return [try readNextByte(), try readNextByte()].toWord()
166 | }
167 |
168 | func readNextOpCode() throws -> OpCode {
169 | var value = try readNextByte()
170 |
171 | if value == 0xCB {
172 | value = try readNextByte()
173 |
174 | return OpCode.word(value)
175 | }
176 |
177 | return OpCode.byte(value)
178 | }
179 |
180 | func popByteOffStack() throws -> UInt8 {
181 | let byte = try mmu.readByte(address: sp)
182 | sp = sp &+ 1
183 | return byte
184 | }
185 |
186 | func popWordOffStack() throws -> UInt16 {
187 | let word = try mmu.readWord(address: sp)
188 | sp = sp &+ 2
189 | return word
190 | }
191 |
192 | func pushByteOnStack(byte: UInt8) throws -> Void {
193 | sp = sp &- 1
194 | try mmu.writeByte(address: sp, byte: byte)
195 | }
196 |
197 | func pushWordOnStack(word: UInt16) throws -> Void {
198 | sp = sp &- 2
199 | try mmu.writeWord(address: sp, word: word)
200 | }
201 |
202 | func fetchNextInstruction() throws -> Instruction {
203 | let opCode = try readNextOpCode()
204 | let instruction = instructions[opCode]
205 |
206 | if instruction == nil {
207 | throw CPUError.instructionNotFound(opCode)
208 | }
209 |
210 | if printOpcodes {
211 | print(opCode)
212 | }
213 |
214 | return instruction!
215 | }
216 |
217 | func handleInterrupts() throws {
218 | if ime == false {
219 | return
220 | }
221 |
222 | let enabled = mmu.interruptsEnabled.read()
223 |
224 | if enabled == 0x00 {
225 | return
226 | }
227 |
228 | let flags = mmu.interruptFlags.read()
229 |
230 | if flags == 0x00 {
231 | return
232 | }
233 |
234 | for interrupt in Interrupts.prioritized {
235 | if enabled.bit(interrupt.bit) && flags.bit(interrupt.bit) {
236 | try pushWordOnStack(word: pc)
237 | mmu.interruptFlags.write(flags.reset(interrupt.bit))
238 | ime = false
239 | pc = interrupt.address
240 | cycles = cycles - 5
241 |
242 | return
243 | }
244 | }
245 | }
246 |
247 | public func run(cpuCycles: Int16) throws {
248 | cycles = cycles + (enabled ? cpuCycles : 0)
249 |
250 | while cycles > 0 && enabled {
251 | if queue.count > 0 {
252 | let cmd = queue.removeFirst()
253 | let next = try cmd.run()
254 |
255 | cycles = cycles - Int16(cmd.cycles)
256 |
257 | if next != nil {
258 | queue.insert(next!, at: 0)
259 | }
260 | } else {
261 | try handleInterrupts()
262 | queue.insert(try fetchNextInstruction().toCommand(self), at: 0)
263 | }
264 | }
265 | }
266 |
267 | public func reset() {
268 | cycles = 0
269 | queue.removeAll()
270 | flags.clear()
271 | a = 0
272 | b = 0
273 | c = 0
274 | d = 0
275 | e = 0
276 | h = 0
277 | l = 0
278 | sp = 0
279 | pc = 0
280 | ime = false
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/Cartridge.swift:
--------------------------------------------------------------------------------
1 | // References:
2 | // https://github.com/juchi/gameboy.js/blob/2eeed5eb5fdc497b47584e2719c18fe8aa13c1ea/src/mbc.js#L10
3 | // https://retrocomputing.stackexchange.com/questions/11732/how-does-the-gameboys-memory-bank-switching-work
4 | // https://b13rg.github.io/Gameboy-MBC-Analysis/
5 |
6 | // TODO: Support more MBC types
7 | // TODO: Make sure memory bank switching is working properly
8 | // TODO: Make sure catridge RAM restoration is working properly
9 |
10 | import Foundation
11 |
12 | enum MBCType {
13 | case zero
14 | case one
15 | case one_ram
16 | case one_ram_battery
17 | case five
18 | case five_ram
19 | case five_ram_battery
20 | case five_rumble
21 | case five_rumble_ram
22 | case five_rumble_ram_battery
23 | case unsupported
24 |
25 | init(rawValue: UInt8) {
26 | switch rawValue {
27 | case 0x00: self = .zero
28 | case 0x01: self = .one
29 | case 0x02: self = .one_ram
30 | case 0x03: self = .one_ram_battery
31 | case 0x19: self = .five
32 | case 0x1A: self = .five_ram
33 | case 0x1B: self = .five_ram_battery
34 | case 0x1C: self = .five_rumble
35 | case 0x1D: self = .five_rumble_ram
36 | case 0x1E: self = .five_rumble_ram_battery
37 | default: self = .unsupported
38 | }
39 | }
40 | }
41 |
42 | struct MBC {
43 | let memory: MemoryAccessArray
44 | let extractRam: () -> [UInt8]
45 | }
46 |
47 | func getRamSize(rom: Data) -> Int {
48 | let eightKB = 8192
49 |
50 | switch rom[0x0149] {
51 | case 0x00,0x01:
52 | return eightKB
53 | case 0x02:
54 | return eightKB
55 | case 0x03:
56 | return eightKB * 4
57 | case 0x04:
58 | return eightKB * 16
59 | case 0x05:
60 | return eightKB * 8
61 | default:
62 | return eightKB * 4
63 | }
64 | }
65 |
66 | func mbcZero(rom: Data, ram: Data) -> MBC {
67 | let romBlock = MemoryBlock(range: 0x0000...0x7FFF, buffer: rom.map { $0 }, readOnly: true, enabled: true)
68 | let ramSize = getRamSize(rom: rom)
69 | let ramBlock = MemoryBlock(range: 0xA000...0xBFFF, buffer: ram.extractFrom(0).fillUntil(count: ramSize, with: 0xFF), readOnly: false, enabled: true)
70 |
71 | return MBC(memory: MemoryAccessArray([romBlock, ramBlock])) {
72 | return ramBlock.buffer
73 | }
74 | }
75 |
76 | func mbcOne(rom: Data, ram: Data) -> MBC {
77 | let romBank1 = MemoryBlockBanked(range: 0x0000...0x3FFF, buffer: rom.extractFrom(0x0000), readOnly: true, enabled: true)
78 | let romBank2 = MemoryBlockBanked(range: 0x4000...0x7FFF, buffer: rom.extractFrom(0x4000), readOnly: true, enabled: true)
79 | let ramSize = getRamSize(rom: rom)
80 | let ramBank = MemoryBlockBanked(range: 0xA000...0xBFFF, buffer: ram.extractFrom(0).fillUntil(count: ramSize, with: 0xFF), readOnly: false, enabled: false)
81 | let memory = MemoryAccessArray([romBank1, romBank2, ramBank])
82 |
83 | var ramBankingEnabled = false
84 | var currentRomBank: UInt8 = 1 {
85 | didSet {
86 | romBank2.bankIndex = UInt16(currentRomBank) - 1
87 | }
88 | }
89 |
90 | memory.subscribe({ addr, _ in addr <= 0x1FFF }) { _, byte in
91 | ramBank.enabled = (byte & 0x0F == 0x0A)
92 | }
93 |
94 | memory.subscribe({ addr, _ in addr >= 0x2000 && addr <= 0x3FFF }) { _, byte in
95 | currentRomBank = (currentRomBank & 0x60) | (byte & 0x1F)
96 |
97 | if byte & 0x1F == 0x00 {
98 | currentRomBank += 1
99 | }
100 | }
101 |
102 | memory.subscribe({ addr, _ in addr >= 0x4000 && addr <= 0x5FFF }) { _, byte in
103 | if ramBankingEnabled {
104 | ramBank.bankIndex = UInt16(byte & 0x03)
105 | } else {
106 | currentRomBank = (currentRomBank & 0x1F) | ((byte & 0x03) << 5)
107 | }
108 | }
109 |
110 | memory.subscribe({ addr, _ in addr >= 0x6000 && addr <= 0x7FFF }) { _, byte in
111 | ramBankingEnabled = (byte & 0x01 == 0x01)
112 |
113 | if !ramBankingEnabled {
114 | ramBank.bankIndex = 0
115 | }
116 | }
117 |
118 | return MBC(memory: memory) {
119 | return ramBank.banks.reduce([]) { agg, arr in
120 | return agg + arr
121 | }
122 | }
123 | }
124 |
125 | func mbcFive(rom: Data, ram: Data) -> MBC {
126 | let rom0 = MemoryBlock(range: 0x0000...0x3FFF, buffer: rom.extractFrom(0x0000...0x3FFF), readOnly: true, enabled: true)
127 | let romBank = MemoryBlockBanked(range: 0x4000...0x7FFF, buffer: rom.extractFrom(0x4000), readOnly: true, enabled: true)
128 | let ramSize = getRamSize(rom: rom)
129 | let ramBank = ramSize > 0 ?
130 | MemoryBlockBanked(range: 0xA000...0xBFFF, buffer: ram.extractFrom(0).fillUntil(count: ramSize, with: 0xFF), readOnly: false, enabled: true) :
131 | MemoryBlockBanked(range: 0xA000...0xBFFF, readOnly: false, enabled: true)
132 | let memory = MemoryAccessArray([rom0, romBank, ramBank])
133 |
134 | romBank.bankIndex = 1
135 |
136 | memory.subscribe({ addr, _ in addr <= 0x1FFF }) { _, byte in
137 | ramBank.enabled = (byte & 0x0A) == 0x0A
138 | }
139 |
140 | memory.subscribe({ addr, _ in addr >= 0x2000 && addr <= 0x2FFF }) { _, byte in
141 | var bank = UInt16(0)
142 | var upper = UInt16(0)
143 | var lower = UInt16(0)
144 |
145 | upper = romBank.bankIndex & 0b100000000
146 | lower = UInt16(byte)
147 | bank = upper | lower
148 |
149 | romBank.bankIndex = bank
150 | }
151 |
152 | memory.subscribe({ addr, _ in addr >= 0x3000 && addr <= 0x3FFF }) { _, byte in
153 | var bank = UInt16(0)
154 | var upper = UInt16(0)
155 | var lower = UInt16(0)
156 |
157 | upper = UInt16(byte & 0b00000001) << 8
158 | lower = romBank.bankIndex & 0b11111111
159 | bank = upper | lower
160 |
161 | romBank.bankIndex = bank
162 | }
163 |
164 | memory.subscribe({ addr, _ in addr >= 0x4000 && addr <= 0x5FFF }) { _, byte in
165 | ramBank.bankIndex = UInt16(byte & 0x0F)
166 | }
167 |
168 | return MBC(memory: memory) {
169 | return ramBank.banks.reduce([]) { agg, arr in
170 | return agg + arr
171 | }
172 | }
173 | }
174 |
175 | func mbcUnsupported(rom: Data) -> MBC {
176 | let titleBuffer = (0x0134...0x0143).map { rom[$0] }
177 | let romWithTitleOnly = MemoryBlock(range: 0x0134...0x0143, buffer: titleBuffer, readOnly: true, enabled: true)
178 |
179 | return MBC(memory: MemoryAccessArray([romWithTitleOnly])) {
180 | return []
181 | }
182 | }
183 |
184 | public class Cartridge: MemoryAccessArray, Identifiable {
185 | let type: MBCType
186 | let romPath: URL
187 | let ramPath: URL
188 |
189 | private var extractRam: (() -> [UInt8])?
190 |
191 | public var title: String {
192 | get {
193 | let bytes = (0x0134...0x0143).map { try! self.readByte(address: $0) }
194 | return String(bytes: bytes, encoding: .utf8) ?? romPath.lastPathComponent.replacingOccurrences(of: ".gb", with: "")
195 | }
196 | }
197 |
198 | public var canBeDeleted: Bool {
199 | get {
200 | return !romPath.absoluteString.contains(Bundle.main.bundleURL.absoluteString)
201 | }
202 | }
203 |
204 | public init(romPath: URL, ramPath: URL) {
205 | let rom = FileSystem.readItem(at: romPath)
206 | let ram = FileSystem.readItem(at: ramPath)
207 |
208 | self.type = MBCType(rawValue: rom[0x0147])
209 | self.romPath = romPath
210 | self.ramPath = ramPath
211 |
212 | super.init()
213 |
214 | switch type {
215 | case .zero:
216 | let mbc = mbcZero(rom: rom, ram: ram)
217 | super.copy(other: mbc.memory)
218 | self.extractRam = mbc.extractRam
219 | case .one, .one_ram, .one_ram_battery:
220 | let mbc = mbcOne(rom: rom, ram: ram)
221 | super.copy(other: mbc.memory)
222 | self.extractRam = mbc.extractRam
223 | case .five, .five_ram, .five_ram_battery, .five_rumble, .five_rumble_ram, .five_rumble_ram_battery:
224 | let mbc = mbcFive(rom: rom, ram: ram)
225 | super.copy(other: mbc.memory)
226 | self.extractRam = mbc.extractRam
227 | case .unsupported:
228 | let mbc = mbcUnsupported(rom: rom)
229 | super.copy(other: mbc.memory)
230 | self.extractRam = mbc.extractRam
231 | }
232 | }
233 |
234 | public func saveRam() {
235 | if let extractRam = self.extractRam {
236 | let bytes = extractRam()
237 | FileSystem.writeItem(at: ramPath, data: Data(bytes))
238 | }
239 | }
240 | }
241 |
242 | public extension Array where Element == Cartridge {
243 | func sortedByDeletablilityAndTitle() -> [Cartridge] {
244 | return self
245 | .sorted(by: { a, b in
246 | if a.canBeDeleted && b.canBeDeleted {
247 | return a.title.lowercased() < b.title.lowercased()
248 | } else if a.canBeDeleted {
249 | return false
250 | } else {
251 | return true
252 | }
253 | })
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/Clock.swift:
--------------------------------------------------------------------------------
1 | // TODO: Refactor cycle calculations in the frame() function. Need to be more explicit and maybe encapsulated into their respective components
2 |
3 | import Foundation
4 |
5 | public class Clock {
6 | private let mmu: MMU
7 | private let ppu: PPU
8 | private let cpu: CPU
9 | private let apu: APU
10 | private let timer: Timer
11 | private let fps: Double
12 | private let frameTime: Double
13 | private var syncTasks: [(MMU, CPU, PPU, APU, Timer) -> Void] = []
14 | public var printFrameDuration = false
15 |
16 | public init(_ mmu: MMU, _ ppu: PPU, _ cpu: CPU, _ apu: APU, _ timer: Timer) {
17 | self.mmu = mmu
18 | self.ppu = ppu
19 | self.cpu = cpu
20 | self.apu = apu
21 | self.timer = timer
22 | self.fps = 60
23 | self.frameTime = 1 / fps
24 | }
25 |
26 | public func sync(task: @escaping (MMU, CPU, PPU, APU, Timer) -> Void) {
27 | self.syncTasks.append(task)
28 | }
29 |
30 | public func start(_ current: DispatchTime = .now()) {
31 | var next = current + frameTime
32 |
33 | DispatchQueue.global(qos: .userInteractive).async {
34 | if self.syncTasks.isNotEmpty {
35 | self.syncTasks.forEach { $0(self.mmu, self.cpu, self.ppu, self.apu, self.timer) }
36 | self.syncTasks.removeAll()
37 | }
38 |
39 | try! self.frame()
40 |
41 | let now = DispatchTime.now()
42 |
43 | if now > next {
44 | next = now
45 | }
46 |
47 | DispatchQueue.main.asyncAfter(deadline: next, execute: {
48 | self.start(next)
49 | })
50 | }
51 | }
52 |
53 | // 70224 clock cycles = 1 frame
54 | // 456 clock cycles = 1 scanline
55 | // Clock runs at 4 MHz
56 | public func frame() throws {
57 | var total: Int = 0
58 | let cycles: Int16 = 456 // Or 48 if we want to get more granular
59 | let seconds = Float(cycles) / 4000000
60 |
61 | StopWatch.global.start("frame")
62 |
63 | while total < 70224 {
64 | StopWatch.global.start("cpu")
65 | try cpu.run(cpuCycles: cycles / 4) // 1 MHz
66 | StopWatch.global.stop("cpu")
67 |
68 | StopWatch.global.start("mmu")
69 | try mmu.run(mmuCycles: cycles / 4) // 1 MHZ
70 | StopWatch.global.stop("mmu")
71 |
72 | StopWatch.global.start("ppu")
73 | try ppu.run(ppuCycles: cycles / 2) // 2 MHz
74 | StopWatch.global.stop("ppu")
75 |
76 | StopWatch.global.start("apu")
77 | try apu.run(seconds: seconds)
78 | StopWatch.global.stop("apu")
79 |
80 | StopWatch.global.start("timer")
81 | try timer.run(timerCycles: cycles / 16) // 250 KHz
82 | StopWatch.global.stop("timer")
83 |
84 | total = total + Int(cycles)
85 | }
86 |
87 | StopWatch.global.stop("frame")
88 |
89 | maybe {
90 | StopWatch.global.printAll()
91 | }
92 |
93 | StopWatch.global.resetAll()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/Common.swift:
--------------------------------------------------------------------------------
1 | // TODO: Rename UInt8.swap() function to swapNibbles()
2 |
3 | import Foundation
4 | import SwiftUI
5 |
6 | public extension Data {
7 | func extractFrom(_ range: ClosedRange) -> [UInt8] {
8 | if self.count == 0 {
9 | return []
10 | }
11 |
12 | if range.lowerBound == range.upperBound {
13 | return [self[range.lowerBound]]
14 | }
15 |
16 | return range.map({ self[$0] })
17 | }
18 |
19 | func extractFrom(_ start: Int) -> [UInt8] {
20 | let end = self.count == 0 ? 0 : self.count - 1
21 | return self.extractFrom(start...end)
22 | }
23 | }
24 |
25 | extension Array {
26 | func chunked(into size: Int) -> [[Element]] {
27 | return stride(from: 0, to: count, by: size).map {
28 | Array(self[$0 ..< Swift.min($0 + size, count)])
29 | }
30 | }
31 |
32 | func extractFrom(_ range: ClosedRange) -> [Element] {
33 | if self.count == 0 {
34 | return []
35 | }
36 |
37 | if range.lowerBound == range.upperBound {
38 | return [self[range.lowerBound]]
39 | }
40 |
41 | return range.map({ self[$0] })
42 | }
43 |
44 | func extractFrom(_ start: Int) -> [Element] {
45 | let end = self.count == 0 ? 0 : self.count - 1
46 | return self.extractFrom(start...end)
47 | }
48 |
49 | func fillUntil(count: Int, with value: Element) -> [Element] {
50 | if self.count < count {
51 | return self + [Element](repeating: value, count: count - self.count)
52 | }
53 |
54 | return self
55 | }
56 | }
57 |
58 | public extension Array where Element == UInt8 {
59 | func toWord() -> UInt16 {
60 | let hb = self[1]
61 | let lb = self[0]
62 | return UInt16(hb) << 8 + UInt16(lb)
63 | }
64 | }
65 |
66 | public extension UInt8 {
67 | func toHexString() -> String {
68 | return String(format:"%02X", self)
69 | }
70 |
71 | func bit(_ bit: UInt8) -> Bool {
72 | let mask = UInt8(0x01 << bit)
73 | return (self & mask) == mask
74 | }
75 |
76 | func crumb(_ crumb: UInt8) -> UInt8 {
77 | let mask = UInt8(0b00000011)
78 | return (self >> (crumb * 2)) & mask
79 | }
80 |
81 | func nibble(_ nibble: UInt8) -> UInt8 {
82 | let mask = UInt8(0b00001111)
83 | return (self >> (nibble * 4)) & mask
84 | }
85 |
86 | func reset(_ bit: UInt8) -> UInt8 {
87 | let mask = ~UInt8(0x01 << bit)
88 | return self & mask
89 | }
90 |
91 | func set(_ bit: UInt8) -> UInt8 {
92 | let mask = UInt8(0x01 << bit)
93 | return self | mask
94 | }
95 |
96 | func swap() -> UInt8 {
97 | let hb = self << 4
98 | let lb = self >> 4
99 | return hb + lb
100 | }
101 |
102 | func toInt8() -> Int8 {
103 | return Int8(bitPattern: self)
104 | }
105 |
106 | func isBetween(_ lower: UInt8, _ upper: UInt8) -> Bool {
107 | return self >= lower && self <= upper
108 | }
109 |
110 | subscript(index: UInt8) -> Bool {
111 | get {
112 | return bit(index)
113 | }
114 | set {
115 | if newValue {
116 | self = set(index)
117 | } else {
118 | self = reset(index)
119 | }
120 | }
121 | }
122 | }
123 |
124 | public extension UInt16 {
125 | func toBytes() -> [UInt8] {
126 | return [UInt8(0x00FF & self), UInt8((0xFF00 & self) >> 8)]
127 | }
128 |
129 | func toHexString() -> String {
130 | let bytes = toBytes()
131 | return String(format:"%02X", bytes[1]) + String(format:"%02X", bytes[0])
132 | }
133 |
134 | func bit(_ pos: UInt8) -> Bool {
135 | let mask = UInt16(0x0001 << pos)
136 | return (self & mask) == mask
137 | }
138 |
139 | func isBetween(_ lower: UInt16, _ upper: UInt16) -> Bool {
140 | return self >= lower && self <= upper
141 | }
142 | }
143 |
144 | public extension UInt64 {
145 | func inMs() -> UInt64 {
146 | return self / 1000000
147 | }
148 | }
149 |
150 | public extension Int8 {
151 | func toUInt16() -> UInt16 {
152 | return UInt16(bitPattern: Int16(self))
153 | }
154 | }
155 |
156 | public extension Int16 {
157 | func toUInt16() -> UInt16 {
158 | return UInt16(bitPattern: self)
159 | }
160 |
161 | func isBetween(_ lower: Int, _ upper: Int) -> Bool {
162 | return self >= lower && self <= upper
163 | }
164 | }
165 |
166 | public extension Int {
167 | func clamp(min: Int, max: Int) -> Int {
168 | if (self > max) { return max }
169 | if (self < min) { return min }
170 |
171 | return self
172 | }
173 | }
174 |
175 | public extension Float {
176 | func clamp(min: Float, max: Float) -> Float {
177 | if (self > max) { return max }
178 | if (self < min) { return min }
179 |
180 | return self
181 | }
182 |
183 | func lerp(min: Float, max: Float, targetMin: Float, targetMax: Float) -> Float {
184 | let scale = (targetMax - targetMin) / (max - min)
185 | let normalized = self - min
186 | let value = normalized * scale
187 |
188 | return value.clamp(min: targetMin, max: targetMax)
189 | }
190 | }
191 |
192 | public extension CGFloat {
193 | func clamp(min: CGFloat, max: CGFloat) -> CGFloat {
194 | if (self > max) { return max }
195 | if (self < min) { return min }
196 |
197 | return self
198 | }
199 | }
200 |
201 | public struct ByteOp {
202 | public var value: UInt8
203 | public var halfCarry: Bool
204 | public var carry: Bool
205 | public var subtract: Bool
206 | public var zero: Bool {
207 | return value == 0
208 | }
209 | }
210 |
211 | public func add(_ num1: UInt8, _ num2: UInt8, carry: Bool = false) -> ByteOp {
212 | let cy: UInt8 = carry ? 1 : 0
213 | let value: UInt8 = num1 &+ num2 &+ cy
214 | let halfCarry = (num1 & 0x0F) + (num2 & 0x0F) + cy > 0x0F
215 | let carry = UInt16(num1) + UInt16(num2) + UInt16(cy) > 0xFF
216 |
217 | return ByteOp(value: value, halfCarry: halfCarry, carry: carry, subtract: false)
218 | }
219 |
220 | public func sub(_ num1: UInt8, _ num2: UInt8, carry: Bool = false) -> ByteOp {
221 | let cy: UInt8 = carry ? 1 : 0
222 | let value: UInt8 = num1 &- num2 &- cy
223 | let halfCarry = ((num1 & 0x0F) &- (num2 & 0x0F) &- cy) & 0x10 != 0x00
224 | let carry = UInt16(num1) < UInt16(num2) + UInt16(cy)
225 |
226 | return ByteOp(value: value, halfCarry: halfCarry, carry: carry, subtract: true)
227 | }
228 |
229 | public struct WordOp {
230 | public var value: UInt16
231 | public var halfCarry: Bool
232 | public var carry: Bool
233 | public var subtract: Bool
234 | public var zero: Bool {
235 | return value == 0
236 | }
237 | }
238 |
239 | public func checkCarry(_ num1: UInt16, _ num2: UInt16, carryBit: UInt8) -> Bool {
240 | let mask = UInt16(0xFFFF) >> (15 - carryBit)
241 | return (num1 & mask) + (num2 & mask) > mask
242 | }
243 |
244 | public func add(_ num1: UInt16, _ num2: UInt16, carryBit: UInt8) -> WordOp {
245 | let value: UInt16 = num1 &+ num2
246 | let halfCarry = checkCarry(num1, num2, carryBit: carryBit)
247 | let carry = num1 > 0xFFFF - num2
248 |
249 | return WordOp(value: value, halfCarry: halfCarry, carry: carry, subtract: false)
250 | }
251 |
252 | public struct Command {
253 | public let cycles: UInt16
254 | public let run: () throws -> Command?
255 |
256 | public init(cycles: UInt16, run: @escaping () throws -> Command?) {
257 | self.cycles = cycles;
258 | self.run = run;
259 | }
260 | }
261 |
262 | func maybe(doThis: () -> Void) {
263 | if Int.random(in: 0...100) == 7 {
264 | doThis()
265 | }
266 | }
267 |
268 | func rarely(doThis: () -> Void) {
269 | if Int.random(in: 0...10000) == 7 {
270 | doThis()
271 | }
272 | }
273 |
274 | class StopWatch {
275 | static let global = StopWatch()
276 |
277 | private var timers: [String: DispatchTime] = [:]
278 | private var ledger: [String: UInt64] = [:]
279 |
280 | func start(_ label: String) {
281 | timers[label] = DispatchTime.now();
282 | }
283 |
284 | func stop(_ label: String) {
285 | if let start = timers[label] {
286 | let end = DispatchTime.now();
287 | let duration = end.uptimeNanoseconds - start.uptimeNanoseconds
288 | let current = ledger[label] ?? 0
289 | ledger[label] = current + duration
290 | }
291 | }
292 |
293 | func reset(_ label: String) {
294 | timers[label] = nil
295 | ledger[label] = nil
296 | }
297 |
298 | func check(_ label: String) -> UInt64 {
299 | return ledger[label] ?? 0
300 | }
301 |
302 | func resetAll() {
303 | timers.removeAll(keepingCapacity: false)
304 | ledger.removeAll(keepingCapacity: false)
305 | }
306 |
307 | func printAll() {
308 | let all = ledger.sorted(by: { $0.value > $1.value }).map( { (k,v) in
309 | return "\(k): \(v.inMs())ms"
310 | })
311 |
312 | print(all.joined(separator: ", "))
313 | }
314 | }
315 |
316 | class Memo {
317 | private var value: T?
318 | private var deps: [AnyHashable] = []
319 |
320 | func get(deps: [AnyHashable], _ getter: @escaping () -> T) -> T {
321 | if value == nil || self.deps != deps {
322 | value = getter()
323 | self.deps = deps
324 | }
325 |
326 | return value!
327 | }
328 |
329 | func invalidate() -> Void {
330 | self.deps = [Int32.random(in: 0...1000000000)]
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/FileSystem.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct FileSystem {
4 | static var documentsDirectory: String {
5 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
6 | return paths[0].path
7 | }
8 |
9 | static func listAbsoluteURLs(inDirectory: String, suffix: String = "") -> [URL] {
10 | if let files = try? FileManager.default.contentsOfDirectory(atPath: inDirectory) {
11 | return files
12 | .filter { $0.hasSuffix(suffix) }
13 | .map { URL(fileURLWithPath: "\(inDirectory)/\($0)") }
14 | }
15 |
16 | return []
17 | }
18 |
19 | static func removeItem(at: URL) throws {
20 | try FileManager.default.removeItem(atPath: at.path)
21 | }
22 |
23 | static func readItem(at: URL) -> Data {
24 | do {
25 | let handle = try FileHandle(forReadingFrom: at)
26 | let bytes = handle.readDataToEndOfFile()
27 |
28 | try handle.close()
29 |
30 | return bytes
31 | } catch {
32 | print("Unexpected error: \(error).")
33 | }
34 |
35 | return Data()
36 | }
37 |
38 | static func writeItem(at: URL, data: Data) {
39 | do {
40 | try data.write(to: at)
41 | } catch {
42 | print("Unexpected error: \(error).")
43 | }
44 | }
45 |
46 | static func copyItem(at: URL, to: URL) throws {
47 | try FileManager.default.copyItem(at: at, to: to)
48 | }
49 |
50 | static func moveItem(at: URL, to: URL) throws {
51 | try FileManager.default.moveItem(at: at, to: to)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/GameLibraryManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | class GameLibraryManager: ObservableObject {
4 | @Published private(set) var library: [Cartridge]
5 | @Published private(set) var inserted: Cartridge
6 |
7 | let clock: Clock
8 |
9 | init(_ clock: Clock) {
10 | let bundledGames = FileSystem.listAbsoluteURLs(inDirectory: Bundle.main.bundlePath, suffix: ".gb")
11 | let importedGames = FileSystem.listAbsoluteURLs(inDirectory: FileSystem.documentsDirectory, suffix: ".gb")
12 |
13 | let bundledCarts = bundledGames.map { romPath -> Cartridge in
14 | let ramPath = URL(fileURLWithPath: "\(FileSystem.documentsDirectory)/\(romPath.lastPathComponent).ram")
15 | return Cartridge(romPath: romPath, ramPath: ramPath)
16 | }
17 | let importedCarts = importedGames.map { romPath -> Cartridge in
18 | return Cartridge(romPath: romPath, ramPath: romPath.appendingPathExtension("ram"))
19 | }
20 | let allCarts = bundledCarts + importedCarts
21 |
22 | self.library = allCarts
23 | self.inserted = allCarts.first!
24 | self.clock = clock
25 | self.insertCartridge(self.inserted)
26 | }
27 |
28 | func deleteCartridge(_ discarded: Cartridge) {
29 | if discarded === inserted {
30 | let discardedIndex = library.firstIndex(where: { $0 === discarded})!
31 | let nextIndex = discardedIndex == library.count - 1 ? discardedIndex - 1 : discardedIndex + 1
32 | let next = library[nextIndex]
33 | insertCartridge(next)
34 | }
35 |
36 | library = library.filter { $0 !== discarded }
37 |
38 | try? FileSystem.removeItem(at: discarded.romPath)
39 | try? FileSystem.removeItem(at: discarded.ramPath)
40 | }
41 |
42 | func insertCartridge(_ next: Cartridge) {
43 | let previous = self.inserted
44 |
45 | self.inserted = next
46 |
47 | self.clock.sync { mmu, cpu, ppu, apu, timer in
48 | previous.saveRam()
49 | mmu.insertCartridge(next)
50 | mmu.reset()
51 | cpu.reset()
52 | ppu.reset()
53 | apu.reset()
54 | timer.reset()
55 | }
56 | }
57 |
58 | func importURLs(urls: [URL]) {
59 | library.append(contentsOf: urls.map { src in
60 | let romPath = URL(fileURLWithPath: "\(FileSystem.documentsDirectory)/\(src.lastPathComponent)")
61 | let ramPath = romPath.appendingPathExtension("ram")
62 | try! FileSystem.moveItem(at: src, to: romPath)
63 | return Cartridge(romPath: romPath, ramPath: ramPath)
64 | })
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/Joypad.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | class Buttons: ObservableObject {
5 | @Published var up = false
6 | @Published var down = false
7 | @Published var left = false
8 | @Published var right = false
9 | @Published var a = false
10 | @Published var b = false
11 | @Published var start = false
12 | @Published var select = false
13 | }
14 |
15 | public class Joypad {
16 | private let mmu: MMU
17 |
18 | let buttons: Buttons
19 |
20 | init(_ mmu: MMU) {
21 | self.mmu = mmu
22 | self.buttons = Buttons()
23 |
24 | self.mmu.joypad.subscribe { input in
25 | var result = input
26 |
27 | if !result[4] {
28 | result[0] = !self.buttons.right
29 | result[1] = !self.buttons.left
30 | result[2] = !self.buttons.up
31 | result[3] = !self.buttons.down
32 |
33 | if input > result {
34 | var flags = self.mmu.interruptFlags.read()
35 | flags[Interrupts.joypad.bit] = true
36 | self.mmu.interruptFlags.write(flags)
37 | }
38 | } else if !result[5] {
39 | result[0] = !self.buttons.a
40 | result[1] = !self.buttons.b
41 | result[2] = !self.buttons.select
42 | result[3] = !self.buttons.start
43 |
44 | if input > result {
45 | var flags = self.mmu.interruptFlags.read()
46 | flags[Interrupts.joypad.bit] = true
47 | self.mmu.interruptFlags.write(flags)
48 | }
49 | } else {
50 | result = 0xFF
51 | }
52 |
53 | self.mmu.joypad.write(result, publish: false)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/MMU.swift:
--------------------------------------------------------------------------------
1 | // TODO: Make sure only HRAM is accessible to the CPU during the DMA transfer process
2 | // TODO: Should we still publish the write if the byte was not actually committed? Can happen if MemoryBlock is disabled..
3 |
4 | import Foundation
5 |
6 | let biosProgram: [UInt8] = [
7 | 0x31, 0xFE, 0xFF, 0xAF, 0x21, 0xFF, 0x9F, 0x32, 0xCB, 0x7C, 0x20, 0xFB, 0x21, 0x26, 0xFF, 0x0E,
8 | 0x11, 0x3E, 0x80, 0x32, 0xE2, 0x0C, 0x3E, 0xF3, 0xE2, 0x32, 0x3E, 0x77, 0x77, 0x3E, 0xFC, 0xE0,
9 | 0x47, 0x11, 0x04, 0x01, 0x21, 0x10, 0x80, 0x1A, 0xCD, 0x95, 0x00, 0xCD, 0x96, 0x00, 0x13, 0x7B,
10 | 0xFE, 0x34, 0x20, 0xF3, 0x11, 0xD8, 0x00, 0x06, 0x08, 0x1A, 0x13, 0x22, 0x23, 0x05, 0x20, 0xF9,
11 | 0x3E, 0x19, 0xEA, 0x10, 0x99, 0x21, 0x2F, 0x99, 0x0E, 0x0C, 0x3D, 0x28, 0x08, 0x32, 0x0D, 0x20,
12 | 0xF9, 0x2E, 0x0F, 0x18, 0xF3, 0x67, 0x3E, 0x64, 0x57, 0xE0, 0x42, 0x3E, 0x91, 0xE0, 0x40, 0x04,
13 | 0x1E, 0x02, 0x0E, 0x0C, 0xF0, 0x44, 0xFE, 0x90, 0x20, 0xFA, 0x0D, 0x20, 0xF7, 0x1D, 0x20, 0xF2,
14 | 0x0E, 0x13, 0x24, 0x7C, 0x1E, 0x83, 0xFE, 0x62, 0x28, 0x06, 0x1E, 0xC1, 0xFE, 0x64, 0x20, 0x06,
15 | 0x7B, 0xE2, 0x0C, 0x3E, 0x87, 0xE2, 0xF0, 0x42, 0x90, 0xE0, 0x42, 0x15, 0x20, 0xD2, 0x05, 0x20,
16 | 0x4F, 0x16, 0x20, 0x18, 0xCB, 0x4F, 0x06, 0x04, 0xC5, 0xCB, 0x11, 0x17, 0xC1, 0xCB, 0x11, 0x17,
17 | 0x05, 0x20, 0xF5, 0x22, 0x23, 0x22, 0x23, 0xC9, 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
18 | 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
19 | 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
20 | 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, 0x3C, 0x42, 0xB9, 0xA5, 0xB9, 0xA5, 0x42, 0x3C,
21 | 0x21, 0x04, 0x01, 0x11, 0xA8, 0x00, 0x1A, 0x13, 0xBE, 0x20, 0xFE, 0x23, 0x7D, 0xFE, 0x34, 0x20,
22 | 0xF5, 0x06, 0x19, 0x78, 0x86, 0x23, 0x05, 0x20, 0xFB, 0x86, 0x20, 0xFE, 0x3E, 0x01, 0xE0, 0x50
23 | ]
24 |
25 | enum MemoryAccessError: Error {
26 | case addressOutOfRange
27 | }
28 |
29 | protocol MemoryAccess: AnyObject {
30 | var version: UInt64 { get }
31 | func contains(address: UInt16) -> Bool
32 | func readByte(address: UInt16) throws -> UInt8
33 | func writeByte(address: UInt16, byte: UInt8) throws -> Void
34 | }
35 |
36 | extension MemoryAccess {
37 | func readWord(address: UInt16) throws -> UInt16 {
38 | let bytes = [try readByte(address: address), try readByte(address: address + 1)]
39 | return bytes.toWord()
40 | }
41 |
42 | func writeWord(address: UInt16, word: UInt16) throws -> Void {
43 | let bytes = word.toBytes()
44 | try writeByte(address: address, byte: bytes[0])
45 | try writeByte(address: address + 1, byte: bytes[1])
46 | }
47 | }
48 |
49 | class MemoryBlock: MemoryAccess {
50 | private let range: ClosedRange
51 | private var readOnly: Bool
52 | private(set) var version: UInt64 = 0
53 | var buffer: [UInt8]
54 | var enabled: Bool
55 | var offset: Int = 0
56 |
57 | init(range: ClosedRange, buffer: [UInt8], readOnly: Bool, enabled: Bool) {
58 | self.range = range
59 | self.buffer = buffer
60 | self.readOnly = readOnly
61 | self.enabled = enabled
62 | }
63 |
64 | convenience init(range: ClosedRange, readOnly: Bool, enabled: Bool) {
65 | self.init(range: range, buffer: [UInt8](repeating: 0xFF, count: range.count), readOnly: readOnly, enabled: enabled)
66 | }
67 |
68 | convenience init(range: ClosedRange, block: MemoryBlock) {
69 | self.init(range: range, buffer: block.buffer, readOnly: block.readOnly, enabled: block.enabled)
70 | }
71 |
72 | func reset() {
73 | self.buffer = [UInt8](repeating: 0xFF, count: range.count)
74 | }
75 |
76 | func contains(address: UInt16)-> Bool {
77 | return range.contains(address)
78 | }
79 |
80 | func readByte(address: UInt16) throws -> UInt8 {
81 | if range.contains(address) == false {
82 | throw MemoryAccessError.addressOutOfRange
83 | }
84 |
85 | if enabled == false {
86 | return 0xFF
87 | }
88 |
89 | let index = Int(address - range.lowerBound) + offset
90 |
91 | return buffer[index % buffer.count]
92 | }
93 |
94 | func readBytes(address start: UInt16, count: UInt16) throws -> [UInt8] {
95 | let end = start &+ count - 1
96 |
97 | if range.contains(start) == false {
98 | throw MemoryAccessError.addressOutOfRange
99 | }
100 |
101 | if range.contains(end) == false {
102 | throw MemoryAccessError.addressOutOfRange
103 | }
104 |
105 | if enabled == false {
106 | return [UInt8](repeating: 0xFF, count: Int(count))
107 | }
108 |
109 | return (start...end).map { address in
110 | let index = Int(address - range.lowerBound) + offset
111 | return buffer[index % buffer.count]
112 | }
113 | }
114 |
115 | func writeByte(address: UInt16, byte: UInt8) throws {
116 | if range.contains(address) == false {
117 | throw MemoryAccessError.addressOutOfRange
118 | }
119 |
120 | if readOnly {
121 | return
122 | }
123 |
124 | if enabled == false {
125 | return
126 | }
127 |
128 | let index = Int(address - range.lowerBound) + offset
129 |
130 | buffer[index % buffer.count] = byte
131 | version = version &+ 1
132 | }
133 | }
134 |
135 | class MemoryBlockBanked: MemoryBlock {
136 | var banks: [[UInt8]]
137 | var bankIndex: UInt16 = 0 {
138 | didSet {
139 | super.buffer = banks[Int(bankIndex)]
140 | }
141 | }
142 |
143 | init(range: ClosedRange, banks: [[UInt8]], readOnly: Bool, enabled: Bool) {
144 | self.banks = banks
145 | super.init(range: range, buffer: banks[0], readOnly: readOnly, enabled: enabled)
146 | }
147 |
148 | convenience override init(range: ClosedRange, buffer: [UInt8], readOnly: Bool, enabled: Bool) {
149 | self.init(range: range, banks: buffer.chunked(into: range.count), readOnly: readOnly, enabled: enabled)
150 | }
151 |
152 | convenience init(range: ClosedRange, readOnly: Bool, enabled: Bool) {
153 | self.init(range: range, buffer: [UInt8](repeating: 0xFF, count: range.count), readOnly: readOnly, enabled: enabled)
154 | }
155 | }
156 |
157 | struct Subscriber {
158 | let predicate: (UInt16, UInt8) -> Bool
159 | let handler: (UInt16, UInt8) -> Void
160 | }
161 |
162 | public class MemoryAccessArray: MemoryAccess {
163 | private var arr: [MemoryAccess]
164 | private var subscribers: [Subscriber] = []
165 | private(set) var version: UInt64 = 0
166 |
167 | var count: Int {
168 | get { arr.count }
169 | }
170 |
171 | init(_ arr: [MemoryAccess] = []) {
172 | self.arr = arr
173 | }
174 |
175 | func copy(other: MemoryAccessArray) {
176 | self.arr = other.arr;
177 | self.subscribers = other.subscribers
178 | }
179 |
180 | func find(address: UInt16) -> MemoryAccess? {
181 | return arr.first { $0.contains(address: address) }
182 | }
183 |
184 | func find(type: T.Type) -> MemoryAccess? {
185 | return arr.first { $0 is T }
186 | }
187 |
188 | func remove(item: MemoryAccess) {
189 | let index = arr.firstIndex { x in
190 | return x === item
191 | }
192 |
193 | if index != nil {
194 | arr.remove(at: index!)
195 | }
196 | }
197 |
198 | func insert(item: MemoryAccess, index: Int = 0) {
199 | arr.insert(item, at: index)
200 | }
201 |
202 | func contains(address: UInt16) -> Bool {
203 | let block = find(address: address)
204 | return block != nil ? true : false
205 | }
206 |
207 | func readByte(address: UInt16) throws -> UInt8 {
208 | if let block = find(address: address) {
209 | return try block.readByte(address: address)
210 | }
211 |
212 | throw MemoryAccessError.addressOutOfRange
213 | }
214 |
215 | func writeByte(address: UInt16, byte: UInt8) throws {
216 | try writeByte(address: address, byte: byte, publish: true)
217 | }
218 |
219 | func writeByte(address: UInt16, byte: UInt8, publish: Bool) throws {
220 | if let block = find(address: address) {
221 | try block.writeByte(address: address, byte: byte)
222 |
223 | if publish {
224 | let subs = subscribers.filter({ $0.predicate(address, byte) })
225 | subs.forEach { $0.handler(address, byte) }
226 | }
227 |
228 | version = version &+ 1
229 |
230 | return
231 | }
232 |
233 | throw MemoryAccessError.addressOutOfRange
234 | }
235 |
236 | internal func subscribe(_ predicate: @escaping (UInt16, UInt8) -> Bool, handler: @escaping (UInt16, UInt8) -> Void) {
237 | subscribers.append(Subscriber(predicate: predicate, handler: handler))
238 | }
239 |
240 | internal func subscribe(address: UInt16, handler: @escaping (UInt8) -> Void) {
241 | subscribe({ (a, b) in a == address }, handler: { _, byte in handler(byte) })
242 | }
243 | }
244 |
245 | public struct Address {
246 | let address: UInt16
247 | let arr: MemoryAccessArray
248 |
249 | init(_ address: UInt16, _ arr: MemoryAccessArray) {
250 | self.address = address
251 | self.arr = arr
252 | }
253 |
254 | func read() -> UInt8 {
255 | return try! arr.readByte(address: address)
256 | }
257 |
258 | func write(_ byte: UInt8, publish: Bool = true) {
259 | try! arr.writeByte(address: address, byte: byte, publish: publish)
260 | }
261 |
262 | func writeBit(_ bit: UInt8, as value: Bool) {
263 | var byte = read()
264 | byte[bit] = value
265 | write(byte)
266 | }
267 |
268 | func subscribe(handler: @escaping (UInt8) -> Void) {
269 | arr.subscribe(address: address, handler: handler);
270 | }
271 |
272 | func subscribe(_ predicate: @escaping (UInt8) -> Bool, handler: @escaping (UInt8) -> Void) {
273 | arr.subscribe({ (a, b) in a == self.address && predicate(b) }, handler: { _, byte in handler(byte) })
274 | }
275 | }
276 |
277 | public class MMU: MemoryAccessArray {
278 | private var queue: [Command] = []
279 | private var cycles: Int16 = 0
280 |
281 | var bios: MemoryBlock
282 | var vramTileData: MemoryBlock
283 | var vramTileMaps: MemoryBlock
284 | var oam: MemoryBlock
285 | var waveformRam: MemoryBlock
286 | var wram: MemoryBlock
287 | var echo: MemoryBlock
288 | var hram: MemoryBlock
289 | var rest: MemoryBlock
290 |
291 | lazy var serialDataTransfer = Address(0xFF01, self)
292 | lazy var serialDataControl = Address(0xFF02, self)
293 | lazy var dividerRegister = Address(0xFF04, self)
294 | lazy var timerCounter = Address(0xFF05, self)
295 | lazy var timerModulo = Address(0xFF06, self)
296 | lazy var timerControl = Address(0xFF07, self)
297 | lazy var interruptFlags = Address(0xFF0F, self)
298 | lazy var lcdControl = Address(0xFF40, self)
299 | lazy var lcdStatus = Address(0xFF41, self)
300 | lazy var scrollY = Address(0xFF42, self)
301 | lazy var scrollX = Address(0xFF43, self)
302 | lazy var lcdY = Address(0xFF44, self)
303 | lazy var lcdYCompare = Address(0xFF45, self)
304 | lazy var dmaTransfer = Address(0xFF46, self)
305 | lazy var bgPalette = Address(0xFF47, self)
306 | lazy var obj0Palette = Address(0xFF48, self)
307 | lazy var obj1Palette = Address(0xFF49, self)
308 | lazy var biosRegister = Address(0xFF50, self)
309 | lazy var interruptsEnabled = Address(0xFFFF, self)
310 | lazy var joypad = Address(0xFF00, self)
311 | lazy var windowY = Address(0xFF4A, self)
312 | lazy var windowX = Address(0xFF4B, self)
313 |
314 | lazy var nr10 = Address(0xFF10, self)
315 | lazy var nr11 = Address(0xFF11, self)
316 | lazy var nr12 = Address(0xFF12, self)
317 | lazy var nr13 = Address(0xFF13, self)
318 | lazy var nr14 = Address(0xFF14, self)
319 | lazy var nr21 = Address(0xFF16, self)
320 | lazy var nr22 = Address(0xFF17, self)
321 | lazy var nr23 = Address(0xFF18, self)
322 | lazy var nr24 = Address(0xFF19, self)
323 | lazy var nr30 = Address(0xFF1A, self)
324 | lazy var nr31 = Address(0xFF1B, self)
325 | lazy var nr32 = Address(0xFF1C, self)
326 | lazy var nr33 = Address(0xFF1D, self)
327 | lazy var nr34 = Address(0xFF1E, self)
328 | lazy var nr41 = Address(0xFF20, self)
329 | lazy var nr42 = Address(0xFF21, self)
330 | lazy var nr43 = Address(0xFF22, self)
331 | lazy var nr44 = Address(0xFF23, self)
332 | lazy var nr50 = Address(0xFF24, self)
333 | lazy var nr51 = Address(0xFF25, self)
334 | lazy var nr52 = Address(0xFF26, self)
335 |
336 | public init() {
337 | self.bios = MemoryBlock(range: 0x0000...0x00FF, buffer: biosProgram, readOnly: true, enabled: true)
338 | self.vramTileData = MemoryBlock(range: 0x8000...0x97FF, readOnly: false, enabled: true)
339 | self.vramTileMaps = MemoryBlock(range: 0x9800...0x9FFF, readOnly: false, enabled: true)
340 | self.oam = MemoryBlock(range: 0xFE00...0xFE9F, readOnly: false, enabled: true)
341 | self.waveformRam = MemoryBlock(range: 0xFF30...0xFF3F, readOnly: false, enabled: true)
342 | self.wram = MemoryBlock(range: 0xC000...0xCFFF, readOnly: false, enabled: true)
343 | self.echo = MemoryBlock(range: 0xE000...0xFDFF, block: wram)
344 | self.hram = MemoryBlock(range: 0xFF80...0xFFFE, readOnly: false, enabled: true)
345 | self.rest = MemoryBlock(range: 0x0000...0xFFFF, readOnly: false, enabled: true)
346 |
347 | super.init([
348 | bios,
349 | vramTileData,
350 | vramTileMaps,
351 | wram,
352 | echo,
353 | oam,
354 | waveformRam,
355 | hram,
356 | rest
357 | ])
358 |
359 | self.biosRegister.subscribe { byte in
360 | if byte == 1 {
361 | self.remove(item: self.bios)
362 | }
363 | }
364 |
365 | self.dmaTransfer.subscribe { byte in
366 | self.startDMATransfer(byte: byte)
367 | }
368 | }
369 |
370 | func insertCartridge(_ cart: Cartridge) {
371 | self.remove(item: bios)
372 |
373 | if let found = self.find(type: Cartridge.self){
374 | self.remove(item: found)
375 | }
376 |
377 | self.insert(item: bios, index: 0)
378 | self.insert(item: cart, index: 1) // Always insert cartridge after bios
379 | }
380 |
381 | func startDMATransfer(byte: UInt8) {
382 | let start = UInt16(byte) << 8
383 | for offset in 0..<0xA0 {
384 | queue.append(Command(cycles: 1) {
385 | let data = try self.readByte(address: start + UInt16(offset))
386 | try self.writeByte(address: 0xFE00 + UInt16(offset), byte: data)
387 | return nil
388 | })
389 | }
390 | }
391 |
392 | public func run(mmuCycles: Int16) throws {
393 | if queue.count > 0 {
394 | cycles = cycles + mmuCycles
395 |
396 | while cycles > 0 && queue.count > 0 {
397 | let cmd = queue.removeFirst()
398 | let next = try cmd.run()
399 |
400 | cycles = cycles - Int16(cmd.cycles)
401 |
402 | if next != nil {
403 | queue.insert(next!, at: 0)
404 | }
405 | }
406 |
407 | // Don't want the MMU to be constanly accumulating cycles since it doesn't have regular work to perform..
408 | if cycles > 0 {
409 | cycles = 0
410 | }
411 | }
412 | }
413 |
414 | public func reset() {
415 | cycles = 0
416 | queue.removeAll()
417 | vramTileData.reset()
418 | vramTileMaps.reset()
419 | oam.reset()
420 | waveformRam.reset()
421 | wram.reset()
422 | echo.reset()
423 | hram.reset()
424 | rest.reset()
425 | }
426 | }
427 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/PPU.swift:
--------------------------------------------------------------------------------
1 | // TODO: self.objectsEnabled is not currently being used anywhere
2 | // TODO: Handle sprite priority: https://youtu.be/HyzD8pNlpwI?t=2179
3 | // TODO: Handle transparent pixel: https://youtu.be/HyzD8pNlpwI?t=3308
4 | // TODO: Double-break code in pixelTransfer() function is horrible - even for my standards..
5 | // TODO: Need a helper function like: let pixels = fill(pallete, lsb: [0,1,2,3], hsb: [0,1,2,3])
6 | // TODO: And another: let pixels = mix(base: pixels1, with: pixels2, from: someIndex)
7 |
8 | import Foundation
9 | import UIKit
10 | import SwiftUI
11 |
12 | public struct Pixel {
13 | public var r, g, b, a: UInt8
14 |
15 | public init(r: UInt8, g: UInt8, b: UInt8, a: UInt8 = 255) {
16 | self.r = r
17 | self.g = g
18 | self.b = b
19 | self.a = a
20 | }
21 |
22 | public static let white = Pixel(r: 255, g: 255, b: 255)
23 | public static let lightGray = Pixel(r: 192, g: 192, b: 192)
24 | public static let darkGray = Pixel(r: 96, g: 96, b: 96)
25 | public static let black = Pixel(r: 0, g: 0, b: 0)
26 | public static let transparent = Pixel(r: 0, g: 0, b: 0, a: 0)
27 |
28 | public static func random() -> Pixel {
29 | return Pixel(r: UInt8.random(in: 0...255), g: UInt8.random(in: 0...255), b: UInt8.random(in: 0...255))
30 | }
31 | }
32 |
33 | public struct Bitmap {
34 | public private(set) var pixels: [Pixel]
35 | public let width: Int
36 | public var height: Int {
37 | return pixels.count / width
38 | }
39 |
40 | public init(width: Int, pixels: [Pixel]) {
41 | self.width = width
42 | self.pixels = pixels
43 | }
44 |
45 | public init(width: Int, height: Int, pixel: Pixel) {
46 | self.pixels = Array(repeating: pixel, count: width * height)
47 | self.width = width
48 | }
49 |
50 | subscript(x: Int, y: Int) -> Pixel {
51 | get { return pixels[y * width + x] }
52 | set { pixels[y * width + x] = newValue }
53 | }
54 | }
55 |
56 | extension UIImage {
57 | convenience init?(bitmap: Bitmap) {
58 | let alphaInfo = CGImageAlphaInfo.premultipliedLast
59 | let bytesPerPixel = MemoryLayout.size
60 | let bytesPerRow = bitmap.width * bytesPerPixel
61 |
62 | guard let providerRef = CGDataProvider(data: Data(
63 | bytes: bitmap.pixels, count: bitmap.height * bytesPerRow
64 | ) as CFData) else {
65 | return nil
66 | }
67 |
68 | guard let cgImage = CGImage(
69 | width: bitmap.width,
70 | height: bitmap.height,
71 | bitsPerComponent: 8,
72 | bitsPerPixel: bytesPerPixel * 8,
73 | bytesPerRow: bytesPerRow,
74 | space: CGColorSpaceCreateDeviceRGB(),
75 | bitmapInfo: CGBitmapInfo(rawValue: alphaInfo.rawValue),
76 | provider: providerRef,
77 | decode: nil,
78 | shouldInterpolate: true,
79 | intent: .defaultIntent
80 | ) else {
81 | return nil
82 | }
83 |
84 | self.init(cgImage: cgImage)
85 | }
86 | }
87 |
88 | public class LCDBitmap: UIView {
89 | private let imageView = UIImageView()
90 | private var displayLink: CADisplayLink?
91 | internal var bitmap = Bitmap(width: 160, height: 144, pixel: Pixel(r: 0, g: 0, b: 0))
92 |
93 | internal var enabled: Bool {
94 | get { return displayLink != nil ? displayLink!.isPaused == false : false }
95 | set {
96 | newValue ? on() : off()
97 | }
98 | }
99 |
100 | override init(frame: CGRect) {
101 | super.init(frame: frame)
102 | didLoad()
103 | }
104 |
105 | required init?(coder aDecoder: NSCoder) {
106 | super.init(coder: aDecoder)
107 | didLoad()
108 | }
109 |
110 | convenience init() {
111 | self.init(frame: CGRect.zero)
112 | }
113 |
114 | func didLoad() {
115 | addSubview(imageView)
116 |
117 | imageView.translatesAutoresizingMaskIntoConstraints = false
118 | imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
119 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
120 | imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
121 | imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
122 | imageView.contentMode = .scaleAspectFit
123 | imageView.backgroundColor = .none
124 | imageView.layer.magnificationFilter = .nearest
125 | }
126 |
127 | private func on() {
128 | if displayLink == nil {
129 | displayLink = CADisplayLink(target: self, selector: #selector(refresh))
130 | displayLink!.add(to: .main, forMode: .common)
131 | }
132 |
133 | displayLink!.isPaused = false
134 | }
135 |
136 | private func off() {
137 | if displayLink != nil {
138 | displayLink?.isPaused = true
139 | }
140 | }
141 |
142 | @objc private func refresh(_ displayLink: CADisplayLink) {
143 | imageView.image = UIImage(bitmap: bitmap)
144 | }
145 | }
146 |
147 | public struct LCDBitmapView: UIViewRepresentable {
148 | var child: LCDBitmap
149 |
150 | public func makeUIView(context: Context) -> UIView {
151 | child
152 | }
153 |
154 | public func updateUIView(_ uiView: UIView, context: Context) { }
155 | }
156 |
157 | struct Object {
158 | let x: UInt8
159 | let y: UInt8
160 | let index: UInt8
161 | let attributes: UInt8
162 | }
163 |
164 | let defaultPalette: [UInt8: Pixel] = [
165 | 0: Pixel.white,
166 | 1: Pixel.lightGray,
167 | 2: Pixel.darkGray,
168 | 3: Pixel.black
169 | ]
170 |
171 | public class PPU {
172 | public let view: LCDBitmapView
173 | private let mmu: MMU
174 | private var queue: [Command] = []
175 | private var cycles: Int16 = 0
176 | private var windowTileMap: UInt8 = 0
177 | private var windowEnabled = false
178 | private var backgroundTileSet: UInt8 = 0
179 | private var backgroundTileMap: UInt8 = 0
180 | private var backgroundEnabled = false
181 | private var bgPalette = defaultPalette
182 | private var obj0Palette = defaultPalette
183 | private var obj1Palette = defaultPalette
184 | private var objSize: [UInt8] = [8, 8]
185 | private var objectsEnabled = false
186 | private var objectsMemo = Memo<[Object]>()
187 | private var objectsTileDataMemo = Memo<[[UInt8]]>()
188 |
189 | public init(_ mmu: MMU) {
190 | self.view = LCDBitmapView(child: LCDBitmap())
191 | self.mmu = mmu
192 |
193 | self.mmu.lcdControl.subscribe { byte in
194 | self.view.child.enabled = byte.bit(7)
195 | self.windowTileMap = byte.bit(6) ? 1 : 0
196 | self.windowEnabled = byte.bit(5)
197 | self.backgroundTileSet = byte.bit(4) ? 1 : 0
198 | self.backgroundTileMap = byte.bit(3) ? 1 : 0
199 | self.objSize = byte.bit(2) ? [8, 16] : [8, 8]
200 | self.objectsEnabled = byte.bit(1)
201 | self.backgroundEnabled = byte.bit(0)
202 | }
203 |
204 | self.mmu.lcdY.subscribe { ly in
205 | let lyc = self.mmu.lcdYCompare.read()
206 | self.setLYEqualsLYC(ly == lyc)
207 | }
208 |
209 | self.mmu.lcdYCompare.subscribe { lyc in
210 | let ly = self.mmu.lcdY.read()
211 | self.setLYEqualsLYC(ly == lyc)
212 | }
213 |
214 | self.mmu.bgPalette.subscribe { byte in
215 | self.bgPalette[0] = defaultPalette[byte.crumb(0)]
216 | self.bgPalette[1] = defaultPalette[byte.crumb(1)]
217 | self.bgPalette[2] = defaultPalette[byte.crumb(2)]
218 | self.bgPalette[3] = defaultPalette[byte.crumb(3)]
219 | }
220 |
221 | self.mmu.obj0Palette.subscribe { byte in
222 | self.obj0Palette[0] = Pixel.transparent
223 | self.obj0Palette[1] = defaultPalette[byte.crumb(1)]
224 | self.obj0Palette[2] = defaultPalette[byte.crumb(2)]
225 | self.obj0Palette[3] = defaultPalette[byte.crumb(3)]
226 | }
227 |
228 | self.mmu.obj1Palette.subscribe { byte in
229 | self.obj1Palette[0] = Pixel.transparent
230 | self.obj1Palette[1] = defaultPalette[byte.crumb(1)]
231 | self.obj1Palette[2] = defaultPalette[byte.crumb(2)]
232 | self.obj1Palette[3] = defaultPalette[byte.crumb(3)]
233 | }
234 |
235 | self.mmu.serialDataControl.subscribe({ (b) in b == 0x81 }) { _ in
236 | let byte = self.mmu.serialDataTransfer.read()
237 | let scalar = UnicodeScalar(byte)
238 | let char = Character(scalar)
239 | print(char, terminator: "")
240 | }
241 | }
242 |
243 | func setLYEqualsLYC(_ equal: Bool) {
244 | var stat = mmu.lcdStatus.read()
245 | var flags = mmu.interruptFlags.read()
246 |
247 | defer {
248 | mmu.lcdStatus.write(stat)
249 | mmu.interruptFlags.write(flags)
250 | }
251 |
252 | stat[2] = equal
253 |
254 | if stat.bit(6) && stat.bit(2) {
255 | flags[Interrupts.lcdStat.bit] = true
256 | }
257 | }
258 |
259 | func setMode(_ mode: UInt8) {
260 | var stat = mmu.lcdStatus.read()
261 | var flags = mmu.interruptFlags.read()
262 |
263 | defer {
264 | mmu.lcdStatus.write(stat)
265 | mmu.interruptFlags.write(flags)
266 | }
267 |
268 | stat[0] = mode[0]
269 | stat[1] = mode[1]
270 |
271 | if mode == 1 {
272 | flags[Interrupts.vBlank.bit] = true
273 | }
274 |
275 | if stat.bit(3) && mode == 0 {
276 | flags[Interrupts.lcdStat.bit] = true
277 | }
278 |
279 | if stat.bit(4) && mode == 1 {
280 | flags[Interrupts.lcdStat.bit] = true
281 | }
282 |
283 | if stat.bit(5) && mode == 2 {
284 | flags[Interrupts.lcdStat.bit] = true
285 | }
286 | }
287 |
288 | struct OamScanData {
289 | let bgTileData: [UInt16]
290 | let winTileData: [UInt16]?
291 | let wy: UInt8
292 | let wx: UInt8
293 | let objectsWithTileData: [(object: Object, data: [UInt8])]
294 | let bgy: UInt8
295 | let objSizeY: Int
296 | }
297 |
298 | func oamScan(ly: UInt8, scx: UInt8, scy: UInt8, continuation: @escaping (OamScanData) -> Command) -> Command {
299 | return Command(cycles: 40) {
300 | self.setMode(2)
301 |
302 | let bgy = scy &+ ly
303 | let bgTileMapRow = Int16(bgy / 8)
304 | let bgTileMapStartIndex = UInt16(bgTileMapRow * 32)
305 | let bgTileMapPointer: UInt16 = self.backgroundTileMap == 1 ? 0x9C00 : 0x9800
306 | let bgTileIndices: [UInt8] = try self.mmu.vramTileMaps.readBytes(address: bgTileMapPointer &+ bgTileMapStartIndex, count: 32 )
307 | let bgTileDataPointer: UInt16 = self.backgroundTileSet == 1 ? 0x8000 : 0x9000
308 | let bgTileData: [UInt16] = try bgTileIndices.map { idx in
309 | if bgTileDataPointer == 0x9000 {
310 | let delta = Int16(idx.toInt8()) * 16 + Int16(bgy % 8) * 2
311 | let address = bgTileDataPointer &+ delta.toUInt16()
312 | return try self.mmu.vramTileData.readWord(address: address)
313 | } else {
314 | let offset = UInt16(idx) * 16 + UInt16(bgy % 8) * 2
315 | let address = bgTileDataPointer &+ offset
316 | return try self.mmu.vramTileData.readWord(address: address)
317 | }
318 | }
319 |
320 | let wy = self.mmu.windowY.read()
321 | let wx = self.mmu.windowX.read()
322 | var winTileData: [UInt16]? = nil
323 |
324 | if self.windowEnabled && ly >= wy && wy.isBetween(0, 143) && wx.isBetween(0, 166) {
325 | let wly = ly - wy
326 | let winTileMapRow = Int16(wly / 8)
327 | let winTileMapStartIndex = UInt16(winTileMapRow * 20)
328 | let winTileMapPointer: UInt16 = self.windowTileMap == 1 ? 0x9C00 : 0x9800
329 | let winTileIndices: [UInt8] = try self.mmu.vramTileMaps.readBytes(address: winTileMapPointer &+ winTileMapStartIndex, count: 20 )
330 | let winTileDataPointer: UInt16 = self.backgroundTileSet == 1 ? 0x8000 : 0x9000
331 | winTileData = try winTileIndices.map { idx in
332 | if winTileDataPointer == 0x9000 {
333 | let delta = Int16(idx.toInt8()) * 16 + Int16(wly % 8) * 2
334 | let address = winTileDataPointer &+ delta.toUInt16()
335 | return try self.mmu.vramTileData.readWord(address: address)
336 | } else {
337 | let offset = UInt16(idx) * 16 + UInt16(wly % 8) * 2
338 | let address = winTileDataPointer &+ offset
339 | return try self.mmu.vramTileData.readWord(address: address)
340 | }
341 | }
342 | }
343 |
344 | let allObjects = self.objectsMemo.get(deps: [self.mmu.oam.version]) {
345 | return try! self.mmu.oam.readBytes(address: 0xFE00, count: 160).chunked(into: 4).map { arr in
346 | return Object(x: arr[1], y: arr[0], index: arr[2], attributes: arr[3])
347 | }
348 | }
349 | let objSizeY = Int(self.objSize[1])
350 | let visibleObjects = allObjects.filter { (o: Object) -> Bool in
351 | let dy = Int16(o.y) - Int16(bgy)
352 | return (dy).isBetween(17 - objSizeY, 16) && o.x > 0
353 | }.prefix(10)
354 | var deps: [AnyHashable] = visibleObjects.map { $0.index }
355 | deps.append(objSizeY)
356 | deps.append(self.mmu.vramTileData.version)
357 | let objTileData = self.objectsTileDataMemo.get(deps: deps) {
358 | return visibleObjects.map ({ (o: Object) -> [UInt8] in
359 | let offset = UInt16(o.index) * 16
360 | let address: UInt16 = 0x8000 &+ offset
361 | let data = try! self.mmu.vramTileData.readBytes(address: address, count: UInt16(objSizeY) * 2)
362 | return data
363 | })
364 | }
365 | let objectsWithTileData = Array(zip(visibleObjects, objTileData))
366 |
367 | return continuation(OamScanData(bgTileData: bgTileData, winTileData: winTileData, wy: wy, wx: wx, objectsWithTileData: objectsWithTileData, bgy: bgy, objSizeY: objSizeY))
368 | }
369 | }
370 |
371 | func pixelTransfer(ly: UInt8, scx: UInt8, data: OamScanData, continuation: @escaping () -> Command) -> Command {
372 | return Command(cycles: 144) {
373 | self.setMode(3)
374 |
375 | var pixels = self.backgroundEnabled ? [Pixel]() : [Pixel](repeating: Pixel.white, count: 256)
376 |
377 | if self.backgroundEnabled {
378 | for data in data.bgTileData {
379 | let arr = data.toBytes()
380 | let lsb = arr[0]
381 | let hsb = arr[1]
382 |
383 | for idx in (0...7).reversed() {
384 | let bit = UInt8(idx) // Bit 7 represents the most leftmost pixel (idx=0)
385 | let v1: UInt8 = lsb.bit(bit) ? 1 : 0
386 | let v2: UInt8 = hsb.bit(bit) ? 2 : 0
387 |
388 | pixels.append(self.bgPalette[v1 + v2]!)
389 | }
390 | }
391 |
392 | if data.winTileData != nil {
393 | var x = Int(data.wx) - 7
394 |
395 | for data in data.winTileData! {
396 | let arr = data.toBytes()
397 | let lsb = arr[0]
398 | let hsb = arr[1]
399 |
400 | for idx in (0...7).reversed() {
401 | let bit = UInt8(idx) // Bit 7 represents the most leftmost pixel (idx=0)
402 | let v1: UInt8 = lsb.bit(bit) ? 1 : 0
403 | let v2: UInt8 = hsb.bit(bit) ? 2 : 0
404 |
405 | pixels[(x + Int(scx)) % pixels.count] = self.bgPalette[v1 + v2]!
406 | x = x + 1
407 |
408 | if x >= pixels.count {
409 | break
410 | }
411 | }
412 |
413 | if x >= pixels.count {
414 | break
415 | }
416 | }
417 | }
418 | }
419 |
420 | for obj in data.objectsWithTileData {
421 | let palette = obj.object.attributes.bit(4) ? self.obj1Palette : self.obj0Palette
422 | let flipY = obj.object.attributes.bit(6)
423 | let flipX = obj.object.attributes.bit(5)
424 | let line = Int(data.bgy) - Int(obj.object.y) + 16 // Why does this work?
425 | let lineIndex = Int(flipY ? data.objSizeY - line - 1 : line)
426 | let lsb = obj.data[lineIndex * 2]
427 | let hsb = obj.data[lineIndex * 2 + 1]
428 |
429 | for idx in (0...7) {
430 | let x = idx + Int(obj.object.x) - 8
431 |
432 | if x >= 0 {
433 | let bit = UInt8(flipX ? idx : 7 - idx) // Bit 7 represents the most leftmost pixel (idx=0)
434 | let v1: UInt8 = lsb.bit(bit) ? 1 : 0
435 | let v2: UInt8 = hsb.bit(bit) ? 2 : 0
436 | let p = v1 + v2
437 | if p > 0 {
438 | pixels[(x + Int(scx)) % pixels.count] = palette[p]!
439 | }
440 | }
441 | }
442 | }
443 |
444 | for col in 0.. Command {
454 | return Command(cycles: 44) {
455 | self.setMode(0)
456 |
457 | // Increment ly at the end of the blanking period
458 | return Command(cycles: 0) {
459 | self.mmu.lcdY.write(ly + 1)
460 | return nil
461 | }
462 | }
463 | }
464 |
465 | func vBlank(ly: UInt8) -> Command {
466 | return Command(cycles: 228) {
467 | if ly == 144 {
468 | self.setMode(1)
469 | }
470 |
471 | // Increment or reset ly at the end of the blanking period
472 | return Command(cycles: 0) {
473 | self.mmu.lcdY.write(ly < 153 ? ly + 1 : 0)
474 | return nil
475 | }
476 | }
477 | }
478 |
479 | func fetchNextCommand() -> Command {
480 | let ly = mmu.lcdY.read()
481 | let scx = mmu.scrollX.read()
482 | let scy = mmu.scrollY.read()
483 |
484 | if ly < view.child.bitmap.height {
485 | return self.oamScan(ly: ly, scx: scx, scy: scy) { data in
486 | return self.pixelTransfer(ly: ly, scx: scx, data: data) {
487 | return self.hBlank(ly: ly);
488 | }
489 | }
490 | } else {
491 | return self.vBlank(ly: ly)
492 | }
493 | }
494 |
495 | public func run(ppuCycles: Int16) throws {
496 | if view.child.enabled {
497 | cycles = cycles + ppuCycles
498 |
499 | while cycles > 0 {
500 | let cmd = queue.count > 0 ? queue.removeFirst() : fetchNextCommand()
501 | let next = try cmd.run()
502 |
503 | cycles = cycles - Int16(cmd.cycles)
504 |
505 | if next != nil {
506 | queue.insert(next!, at: 0)
507 | }
508 | }
509 | }
510 | }
511 |
512 | public func reset() {
513 | cycles = 0
514 | queue.removeAll()
515 | objectsMemo.invalidate()
516 | objectsTileDataMemo.invalidate()
517 | }
518 | }
519 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/Timer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class Timer {
4 | private let mmu: MMU
5 | private let divTreshold: UInt = 16
6 | private var counterThreshold: UInt = 16
7 | private var enabled = false
8 |
9 | private var counterCycles: UInt = 0 {
10 | didSet {
11 | let result = counterCycles.quotientAndRemainder(dividingBy: counterThreshold)
12 |
13 | if result.quotient > 0 {
14 | let previousValue = mmu.timerCounter.read()
15 | let nextValue = previousValue &+ UInt8(result.quotient)
16 |
17 | if nextValue < previousValue {
18 | // Overflowed
19 | mmu.timerCounter.write(mmu.timerModulo.read())
20 | mmu.interruptFlags.writeBit(Interrupts.timer.bit, as: true)
21 | } else {
22 | mmu.timerCounter.write(nextValue)
23 | }
24 |
25 | counterCycles = result.remainder
26 | }
27 | }
28 | }
29 |
30 | private var divCycles: UInt = 0 {
31 | didSet {
32 | let result = divCycles.quotientAndRemainder(dividingBy: divTreshold)
33 |
34 | if result.quotient > 0 {
35 | let previousValue = mmu.dividerRegister.read()
36 | let nextValue = previousValue &+ UInt8(result.quotient)
37 | mmu.dividerRegister.write(nextValue, publish: false)
38 | divCycles = result.remainder
39 | }
40 | }
41 | }
42 |
43 | public init(_ mmu: MMU) {
44 | self.mmu = mmu
45 |
46 | self.mmu.dividerRegister.subscribe { _ in
47 | self.mmu.dividerRegister.write(0, publish: false)
48 | }
49 |
50 | self.mmu.timerControl.subscribe { control in
51 | self.enabled = control.bit(2)
52 |
53 | let speed = control & 0b00000011
54 |
55 | if speed == 0 {
56 | self.counterThreshold = 64
57 | } else if speed == 1 {
58 | self.counterThreshold = 1
59 | } else if speed == 2 {
60 | self.counterThreshold = 4
61 | } else {
62 | self.counterThreshold = 16
63 | }
64 | }
65 | }
66 |
67 | public func run(timerCycles: Int16) throws {
68 | if enabled {
69 | counterCycles = counterCycles &+ UInt(timerCycles)
70 | }
71 |
72 | divCycles = divCycles &+ UInt(timerCycles)
73 | }
74 |
75 | public func reset() {
76 | divCycles = 0
77 | counterCycles = 0
78 | counterThreshold = 16
79 | enabled = false
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Emulator/UI.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 | import UniformTypeIdentifiers
4 |
5 | extension UTType {
6 | public static var gb: UTType { UTType(importedAs: "com.swiftboy.gameboyfile" )}
7 | }
8 |
9 | // Source: https://github.com/markrenaud/FilePicker/blob/main/Sources/FilePicker/FilePickerUIRepresentable.swift
10 | public struct FilePickerUIRepresentable: UIViewControllerRepresentable {
11 | public typealias UIViewControllerType = UIDocumentPickerViewController
12 | public typealias PickedURLsCompletionHandler = (_ urls: [URL]) -> Void
13 |
14 | @Environment(\.dismiss) var dismiss
15 |
16 | public let types: [UTType]
17 | public let allowMultiple: Bool
18 | public let pickedCompletionHandler: PickedURLsCompletionHandler
19 |
20 | public init(types: [UTType], allowMultiple: Bool, onPicked completionHandler: @escaping PickedURLsCompletionHandler) {
21 | self.types = types
22 | self.allowMultiple = allowMultiple
23 | self.pickedCompletionHandler = completionHandler
24 | }
25 |
26 | public func makeCoordinator() -> Coordinator {
27 | Coordinator(parent: self)
28 | }
29 |
30 | public func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
31 | let picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
32 | picker.delegate = context.coordinator
33 | picker.allowsMultipleSelection = allowMultiple
34 | return picker
35 | }
36 |
37 | public func updateUIViewController(_ controller: UIDocumentPickerViewController, context: Context) {}
38 |
39 | public class Coordinator: NSObject, UIDocumentPickerDelegate {
40 | var parent: FilePickerUIRepresentable
41 |
42 | init(parent: FilePickerUIRepresentable) {
43 | self.parent = parent
44 | }
45 |
46 | public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
47 | parent.pickedCompletionHandler(urls)
48 | parent.dismiss()
49 | }
50 | }
51 | }
52 |
53 | struct PressableView : View {
54 | @Binding var pressedBinding: Bool
55 | @State private var pressed = false {
56 | didSet {
57 | pressedBinding = pressed
58 | haptics.impactOccurred(intensity: 0.5);
59 | if let cb = pressed ? onPressedCallback : onReleasedCallback {
60 | cb()
61 | }
62 | }
63 | }
64 |
65 | private let getChildView: (Bool) -> C
66 | private var haptics: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
67 | private var onPressedCallback: (() -> Void)?
68 | private var onReleasedCallback: (() -> Void)?
69 |
70 | var body: some View {
71 | getChildView(pressed)
72 | .gesture(dragGesture)
73 | }
74 |
75 | private var dragGesture: some Gesture {
76 | DragGesture( minimumDistance: 0, coordinateSpace: .local)
77 | .onChanged { _ in
78 | if !pressed {
79 | pressed = true
80 | }
81 | }
82 | .onEnded { _ in
83 | if pressed {
84 | pressed = false
85 | }
86 | }
87 | }
88 | }
89 |
90 | extension PressableView {
91 | init (_ getChildView: @escaping (Bool) -> C) {
92 | self._pressedBinding = Binding.constant(false)
93 | self.getChildView = getChildView
94 | }
95 |
96 | init (_ pressed: Binding, _ getChildView: @escaping (Bool) -> C) {
97 | self._pressedBinding = pressed
98 | self.getChildView = getChildView
99 | }
100 |
101 | func impact(_ strength: UIImpactFeedbackGenerator.FeedbackStyle) -> Self {
102 | var next = self;
103 | next.haptics = UIImpactFeedbackGenerator(style: strength);
104 |
105 | return next;
106 | }
107 |
108 | func onPressed(_ onPressedCallback: @escaping () -> Void) -> Self {
109 | var next = self
110 | next.onPressedCallback = onPressedCallback
111 |
112 | return next;
113 | }
114 |
115 | func onReleased(_ onReleasedCallback: @escaping () -> Void) -> Self {
116 | var next = self
117 | next.onReleasedCallback = onReleasedCallback
118 |
119 | return next;
120 | }
121 | }
122 |
123 | struct DraggableView : View {
124 | @Binding var dragOffsetBinding: CGFloat
125 | @Binding var draggingBinding: Bool
126 |
127 | @State private var dragging = false
128 | @State private var prevDragTranslation = CGSize.zero
129 | @State private var dragOffset = CGFloat(0) {
130 | didSet {
131 | dragOffsetBinding = dragOffset
132 | if let cb = onDraggedCallback {
133 | cb(dragOffset)
134 | }
135 | }
136 | }
137 |
138 | private let getChildView: (CGFloat, Bool) -> C
139 | private var onDraggedCallback: ((CGFloat) -> Void)?
140 | private var onReleasedCallback: ((CGFloat) -> Void)?
141 |
142 | var body: some View {
143 | getChildView(dragOffset, dragging)
144 | .gesture(dragGesture)
145 | }
146 |
147 | private var dragGesture: some Gesture {
148 | DragGesture(minimumDistance: 0, coordinateSpace: .global)
149 | .onChanged { val in
150 | let dragAmount = val.translation.height - prevDragTranslation.height
151 | dragOffset += dragAmount
152 | prevDragTranslation = val.translation
153 | if !dragging {
154 | dragging = true
155 | }
156 | }
157 | .onEnded { val in
158 | prevDragTranslation = .zero
159 | if let cb = onReleasedCallback {
160 | cb(dragOffset)
161 | }
162 | dragOffset = 0
163 | if dragging {
164 | dragging = false
165 | }
166 | }
167 | }
168 | }
169 |
170 | extension DraggableView {
171 | init (_ dragOffsetBinding: Binding, _ draggingBinding: Binding, _ getChildView: @escaping (CGFloat, Bool) -> C) {
172 | self._dragOffsetBinding = dragOffsetBinding
173 | self._draggingBinding = draggingBinding
174 | self.getChildView = getChildView
175 | }
176 |
177 | init (_ dragOffsetBinding: Binding, _ getChildView: @escaping (CGFloat, Bool) -> C) {
178 | self.init(dragOffsetBinding, Binding.constant(false), getChildView)
179 | }
180 |
181 | init (_ getChildView: @escaping (CGFloat, Bool) -> C) {
182 | self.init(Binding.constant(CGFloat(0)), getChildView)
183 | }
184 |
185 | func onDragged(_ onDraggedCallback: @escaping (CGFloat) -> Void) -> Self {
186 | var next = self
187 | next.onDraggedCallback = onDraggedCallback
188 |
189 | return next;
190 | }
191 |
192 | func onReleased(_ onReleasedCallback: @escaping (CGFloat) -> Void) -> Self {
193 | var next = self
194 | next.onReleasedCallback = onReleasedCallback
195 |
196 | return next;
197 | }
198 | }
199 |
200 |
201 | struct GameButtonView: View where S : Shape {
202 | var shape: S
203 | var label: String?
204 | var width: CGFloat = 50
205 | var height: CGFloat = 50
206 | var onPressed: () -> Void
207 | var onReleased: () -> Void
208 |
209 | var body: some View {
210 | VStack {
211 | PressableView { pressed in
212 | shape
213 | .fill(.white)
214 | .frame(width: width, height: height)
215 | .scaleEffect(pressed ? 1.2 : 1)
216 | .animation(.spring().speed(4), value: pressed)
217 | }
218 | .onPressed(onPressed)
219 | .onReleased(onReleased)
220 |
221 | if label != nil {
222 | Text(label!)
223 | .font(.footnote)
224 | .foregroundColor(.white)
225 | }
226 | }
227 | .rotation3DEffect(Angle(degrees: -30), axis: (x: 0, y: 0, z: 1))
228 | }
229 | }
230 |
231 | struct DPadView: View {
232 | @EnvironmentObject private var buttons: Buttons
233 |
234 | var body: some View {
235 | VStack {
236 | GameButtonView(shape: Circle(), onPressed: { buttons.up = true }, onReleased: { buttons.up = false })
237 | .offset(x: 0, y: 10)
238 | HStack(spacing: 40) {
239 | GameButtonView(shape: Circle(), onPressed: { buttons.left = true }, onReleased: { buttons.left = false })
240 | GameButtonView(shape: Circle(), onPressed: { buttons.right = true }, onReleased: { buttons.right = false })
241 | }
242 | GameButtonView(shape: Circle(), onPressed: { buttons.down = true }, onReleased: { buttons.down = false })
243 | .offset(x: 0, y: -10)
244 | }
245 | }
246 | }
247 |
248 | struct ABView: View {
249 | @EnvironmentObject private var buttons: Buttons
250 |
251 | var body: some View {
252 | HStack {
253 | GameButtonView(shape: Circle(), label: "B", onPressed: { buttons.b = true }, onReleased: { buttons.b = false })
254 | .offset(x: 0, y: 30)
255 | GameButtonView(shape: Circle(), label: "A", onPressed: { buttons.a = true }, onReleased: { buttons.a = false })
256 | }
257 | }
258 | }
259 |
260 | struct StartSelectView: View {
261 | @EnvironmentObject private var buttons: Buttons
262 |
263 | var body: some View {
264 | HStack {
265 | GameButtonView(
266 | shape: RoundedRectangle(cornerRadius: 5),
267 | label: "START",
268 | width: 45,
269 | height: 10,
270 | onPressed: { buttons.start = true },
271 | onReleased: { buttons.start = false }
272 | )
273 | .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
274 | GameButtonView(
275 | shape: RoundedRectangle(cornerRadius: 5),
276 | label: "SELECT",
277 | width: 45,
278 | height: 10,
279 | onPressed: { buttons.select = true },
280 | onReleased: { buttons.select = false }
281 | )
282 | .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
283 | }
284 | }
285 | }
286 |
287 | struct TitleView: View {
288 | var title: String
289 | var onReleased: () -> Void
290 |
291 | var body: some View {
292 | PressableView { pressed in
293 | Text(title)
294 | .fontWeight(.bold)
295 | .textCase(.uppercase)
296 | .foregroundColor(pressed ? .black : .white)
297 | .padding(.vertical, 5)
298 | .background(Rectangle().fill(pressed ? .white : .white.opacity(0)))
299 | }
300 | .onReleased(onReleased)
301 | }
302 | }
303 |
304 | struct GameLibraryItemView: View {
305 | var game: Cartridge
306 | @State private var confirmDelete = false
307 | @EnvironmentObject private var gameLibraryManager: GameLibraryManager
308 | @Environment(\.dismiss) private var dismissLibraryView
309 |
310 | private func delay(numSeconds: Double, cb: @escaping () -> Void) {
311 | DispatchQueue.main.asyncAfter(deadline: .now() + numSeconds) {
312 | cb()
313 | }
314 | }
315 |
316 | func shouldHighlight(_ pressed: Bool) -> Bool {
317 | return gameLibraryManager.inserted === game || pressed
318 | }
319 |
320 | var body: some View {
321 | HStack (alignment: .top) {
322 | VStack {
323 | PressableView { pressed in
324 | Text(game.title)
325 | .font(.title2)
326 | .fontWeight(.bold)
327 | .textCase(.uppercase)
328 | .lineLimit(1)
329 | .foregroundColor(shouldHighlight(pressed) ? .white : .black)
330 | .background(Rectangle().fill(shouldHighlight(pressed) ? .black : .black.opacity(0)))
331 | .frame(maxWidth: .infinity, alignment: .leading)
332 | .padding(.trailing, 20)
333 | }
334 | .onReleased {
335 | gameLibraryManager.insertCartridge(game)
336 | dismissLibraryView()
337 | }
338 | Text(game.type == .unsupported ? "Not Supported ❌" : "Supported")
339 | .font(.caption)
340 | .fontWeight(.bold)
341 | .textCase(.uppercase)
342 | .foregroundColor(.black.opacity(0.4))
343 | .frame(maxWidth: .infinity, alignment: .leading)
344 | }
345 | if game.canBeDeleted {
346 | Button(role: .none, action: {
347 | if confirmDelete {
348 | gameLibraryManager.deleteCartridge(game)
349 | } else {
350 | confirmDelete = true
351 | delay(numSeconds: 5) {
352 | confirmDelete = false
353 | }
354 | }
355 | }) {
356 | Label(confirmDelete ? "Confirm" : "", systemImage: "trash")
357 | .foregroundColor(confirmDelete ? .red : .black.opacity(0.4))
358 | }
359 | .padding(.top, 5)
360 | .animation(.easeInOut, value: confirmDelete)
361 | }
362 | }
363 | .padding([.leading, .trailing])
364 | }
365 | }
366 |
367 | struct GameLibraryModalView: View {
368 | var landscape = false
369 | @State private var showFilePicker = false
370 | @EnvironmentObject private var gameLibraryManager: GameLibraryManager
371 | @Environment(\.dismiss) private var dismiss
372 |
373 | var body: some View {
374 | VStack {
375 | ScrollView(.vertical, showsIndicators: true) {
376 | VStack(spacing: 20) {
377 | ForEach(gameLibraryManager.library.sortedByDeletablilityAndTitle()) { game in
378 | GameLibraryItemView(game: game)
379 | }
380 | }
381 | }
382 | .padding(.top, 20)
383 | .frame(maxHeight: .infinity)
384 | .animation(.easeInOut, value: gameLibraryManager.library.count)
385 |
386 | HStack (spacing: 10) {
387 | PressableView { pressed in
388 | Text("Import Game")
389 | .fontWeight(.bold)
390 | .textCase(.uppercase)
391 | .foregroundColor(pressed ? .black : .white)
392 | .frame(maxWidth: .infinity)
393 | .frame(height: 40)
394 | .background(
395 | pressed ?
396 | AnyView(RoundedRectangle(cornerRadius: 10).stroke(.black, lineWidth: 4)) :
397 | AnyView(RoundedRectangle(cornerRadius: 10).fill(.black))
398 | )
399 | }
400 | .onReleased {
401 | showFilePicker = true
402 | }
403 | .sheet(isPresented: $showFilePicker) {
404 | FilePickerUIRepresentable(types: [.gb], allowMultiple: true) { urls in
405 | gameLibraryManager.importURLs(urls: urls)
406 | }
407 | .ignoresSafeArea()
408 | }
409 |
410 | if landscape {
411 | PressableView { pressed in
412 | Text("Close")
413 | .fontWeight(.bold)
414 | .textCase(.uppercase)
415 | .foregroundColor(pressed ? .black : .white)
416 | .frame(maxWidth: 200)
417 | .frame(height: 40)
418 | .background(
419 | pressed ?
420 | AnyView(RoundedRectangle(cornerRadius: 10).stroke(.black, lineWidth: 4)) :
421 | AnyView(RoundedRectangle(cornerRadius: 10).fill(.black))
422 | )
423 | }
424 | .onReleased {
425 | dismiss()
426 | }
427 | }
428 | }
429 | .padding()
430 | }
431 | .frame(maxHeight: .infinity)
432 | .frame(maxWidth: .infinity)
433 | .background(.white)
434 | }
435 | }
436 |
437 | struct GameBoyView: View {
438 | private var lcd: LCDBitmapView
439 | @State private var showGameLibrary: Bool = false
440 | @EnvironmentObject private var gameLibraryManager: GameLibraryManager
441 |
442 | init(lcd: LCDBitmapView) {
443 | self.lcd = lcd
444 | }
445 |
446 | var body: some View {
447 | ZStack {
448 | GeometryReader { geometry in
449 | Group {
450 | if geometry.size.width > geometry.size.height {
451 | HStack {
452 | VStack {
453 | Spacer()
454 | DPadView()
455 | Spacer()
456 | }
457 | .padding(.trailing, 20)
458 | .frame(width: geometry.size.width * 0.2)
459 | lcd
460 | VStack {
461 | Spacer()
462 | ABView()
463 | Spacer()
464 | StartSelectView()
465 | }
466 | .frame(width: geometry.size.width * 0.2)
467 | }
468 | } else {
469 | VStack{
470 | TitleView(title: gameLibraryManager.inserted.title) {
471 | showGameLibrary = true
472 | }
473 | .padding(.top, 20)
474 | lcd.frame(height: geometry.size.height * 0.5)
475 | VStack {
476 | Spacer()
477 | HStack {
478 | DPadView()
479 | Spacer()
480 | ABView()
481 | }
482 | Spacer()
483 | StartSelectView()
484 | }
485 | .padding()
486 | }
487 | }
488 | }
489 | .sheet(isPresented: $showGameLibrary) {
490 | GameLibraryModalView(landscape: geometry.size.width > geometry.size.height)
491 | }
492 | }
493 | .background(.black)
494 | }
495 | .frame(width: .infinity, height: .infinity)
496 | .sheet(isPresented: $showGameLibrary) {
497 | GameLibraryModalView()
498 | }
499 | }
500 | }
501 |
--------------------------------------------------------------------------------
/app/SwiftBoy/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Swift Boy
9 | CFBundleDocumentTypes
10 |
11 |
12 | CFBundleTypeName
13 | Game Boy files
14 | LSHandlerRank
15 | Alternate
16 | LSItemContentTypes
17 |
18 | com.swiftboy.gameboyfile
19 |
20 |
21 |
22 | CFBundleExecutable
23 | $(EXECUTABLE_NAME)
24 | CFBundleIdentifier
25 | $(PRODUCT_BUNDLE_IDENTIFIER)
26 | CFBundleInfoDictionaryVersion
27 | 6.0
28 | CFBundleName
29 | $(PRODUCT_NAME)
30 | CFBundlePackageType
31 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
32 | CFBundleShortVersionString
33 | 1.0
34 | CFBundleVersion
35 | 1
36 | LSRequiresIPhoneOS
37 |
38 | UIApplicationSceneManifest
39 |
40 | UIApplicationSupportsMultipleScenes
41 |
42 | UISceneConfigurations
43 |
44 | UIWindowSceneSessionRoleApplication
45 |
46 |
47 | UISceneConfigurationName
48 | Default Configuration
49 | UISceneDelegateClassName
50 | $(PRODUCT_MODULE_NAME).SceneDelegate
51 | UISceneStoryboardFile
52 | Main
53 |
54 |
55 |
56 |
57 | UILaunchScreen
58 |
59 | UIColorName
60 | LaunchScreenColor
61 |
62 | UIMainStoryboardFile
63 | Main
64 | UIRequiredDeviceCapabilities
65 |
66 | armv7
67 |
68 | UISupportedInterfaceOrientations
69 |
70 | UIInterfaceOrientationPortrait
71 | UIInterfaceOrientationLandscapeLeft
72 | UIInterfaceOrientationLandscapeRight
73 |
74 | UISupportedInterfaceOrientations~ipad
75 |
76 | UIInterfaceOrientationPortrait
77 | UIInterfaceOrientationPortraitUpsideDown
78 | UIInterfaceOrientationLandscapeLeft
79 | UIInterfaceOrientationLandscapeRight
80 |
81 | UISupportsDocumentBrowser
82 |
83 | UTImportedTypeDeclarations
84 |
85 |
86 | UTTypeConformsTo
87 |
88 | public.data
89 |
90 | UTTypeDescription
91 | GB Data
92 | UTTypeIconFiles
93 |
94 | UTTypeIdentifier
95 | com.swiftboy.gameboyfile
96 | UTTypeTagSpecification
97 |
98 | public.filename-extension
99 |
100 | gb
101 | GB
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/app/SwiftBoy/ROMs/cpu_instrs.gb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/app/SwiftBoy/ROMs/cpu_instrs.gb
--------------------------------------------------------------------------------
/app/SwiftBoy/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
4 | var window: UIWindow?
5 |
6 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
7 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
8 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
9 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
10 | guard let _ = (scene as? UIWindowScene) else { return }
11 | }
12 |
13 | func sceneDidDisconnect(_ scene: UIScene) {
14 | // Called as the scene is being released by the system.
15 | // This occurs shortly after the scene enters the background, or when its session is discarded.
16 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
17 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
18 |
19 | guard let viewController = self.window?.rootViewController as? ViewController else { return }
20 | guard let onClose = viewController.onClose else { return }
21 |
22 | onClose()
23 | }
24 |
25 | func sceneDidBecomeActive(_ scene: UIScene) {
26 | // Called when the scene has moved from an inactive state to an active state.
27 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
28 | }
29 |
30 | func sceneWillResignActive(_ scene: UIScene) {
31 | // Called when the scene will move from an active state to an inactive state.
32 | // This may occur due to temporary interruptions (ex. an incoming phone call).
33 | }
34 |
35 | func sceneWillEnterForeground(_ scene: UIScene) {
36 | // Called as the scene transitions from the background to the foreground.
37 | // Use this method to undo the changes made on entering the background.
38 | }
39 |
40 | func sceneDidEnterBackground(_ scene: UIScene) {
41 | // Called as the scene transitions from the foreground to the background.
42 | // Use this method to save data, release shared resources, and store enough scene-specific state information
43 | // to restore the scene back to its current state.
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/SwiftBoy/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 |
4 | class ViewController: UIViewController {
5 | var onClose: (() -> Void)?
6 |
7 | override func viewDidLoad() {
8 | super.viewDidLoad()
9 |
10 | let mmu = MMU()
11 | let ppu = PPU(mmu)
12 | let cpu = CPU(mmu)
13 | let apu = APU(mmu)
14 | let timer = Timer(mmu)
15 | let joypad = Joypad(mmu)
16 | let clock = Clock(mmu, ppu, cpu, apu, timer)
17 | let glm = GameLibraryManager(clock)
18 |
19 | clock.start()
20 |
21 | let ui = UIHostingController(rootView: GameBoyView(lcd: ppu.view).environmentObject(joypad.buttons).environmentObject(glm))
22 |
23 | view.backgroundColor = .black
24 | view.translatesAutoresizingMaskIntoConstraints = false
25 | view.addSubview(ui.view)
26 |
27 | ui.view.translatesAutoresizingMaskIntoConstraints = false
28 | ui.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
29 | ui.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
30 | ui.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
31 | ui.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
32 |
33 | self.onClose = {
34 | glm.inserted.saveRam()
35 | }
36 | }
37 | }
38 |
39 |
40 |
--------------------------------------------------------------------------------
/assets/icloud-drive-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/icloud-drive-portrait.png
--------------------------------------------------------------------------------
/assets/icon-rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/icon-rounded.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/icon.sketch
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | v3
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/assets/import-menu-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/import-menu-landscape.png
--------------------------------------------------------------------------------
/assets/import-menu-portrait.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/import-menu-portrait.gif
--------------------------------------------------------------------------------
/assets/import-menu-portrait.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/import-menu-portrait.mov
--------------------------------------------------------------------------------
/assets/import-menu-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/import-menu-portrait.png
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-landscape.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-landscape.gif
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-landscape.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-landscape.mov
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-landscape.png
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-portrait.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-portrait.gif
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-portrait.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-portrait.mov
--------------------------------------------------------------------------------
/assets/super-marioland-gameplay-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-gameplay-portrait.png
--------------------------------------------------------------------------------
/assets/super-marioland-main-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-main-landscape.png
--------------------------------------------------------------------------------
/assets/super-marioland-main-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/super-marioland-main-portrait.png
--------------------------------------------------------------------------------
/assets/tetris-gameplay-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-gameplay-landscape.png
--------------------------------------------------------------------------------
/assets/tetris-gameplay-portrait.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-gameplay-portrait.gif
--------------------------------------------------------------------------------
/assets/tetris-gameplay-portrait.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-gameplay-portrait.mov
--------------------------------------------------------------------------------
/assets/tetris-main-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-main-landscape.png
--------------------------------------------------------------------------------
/assets/tetris-main-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-main-portrait.png
--------------------------------------------------------------------------------
/assets/tetris-music-landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-music-landscape.png
--------------------------------------------------------------------------------
/assets/tetris-music-portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bberak/swift-boy/4051da989d38dd39192130b0169245a4abdd4035/assets/tetris-music-portrait.png
--------------------------------------------------------------------------------
/blog-posts/part-one.md:
--------------------------------------------------------------------------------
1 | # An introduction to the magical world of software emulation with Swift
2 |
3 | ## Overview
4 |
5 | - Intro
6 | - Selecting the hardware to emulate (scope)
7 | - Accuracy vs happiness
8 | - High-level vs low-level languages
9 | - HLE vs LLE
10 | - Exploring the CPU
11 | - 16-bit address space
12 | - 8-bit data bus
13 | - Registers
14 | - PC
15 | - SP
16 | - Flags
17 | - Instruction format
18 | - Cycles
19 | - The instruction set
20 | - The basic architecture
21 | - Counting cycles
22 | - Handling branching
23 | - Writing instructions for insane people
24 | - Writing instructions for pragmatic people
25 |
26 | ## Intro
27 |
28 | As a teenager in the early 2000's, tinkering with PCs and sinking countless hours into online gaming was a favourite pastime of mine. Nothing gave me the warm fuzzies more than throwing Gran Turismo into my CD-ROM and seeing the Playstation startup screen come to life on whatever semi-legal emulator was around at the time.
29 |
30 | Throughout my career as a software developer I had always toyed with the idea of attempting to write an emulator of my own. Having spent the majority of my working life in the higher levels of the computing stack -- I had always found the prospect of going lower to be overly daunting or intimidating.
31 |
32 | In this series of articles I will attempt to demistify some parts of the emulation black box and shed some light into an area of software development that the vast majority of programmers won't necessarily get to experience in their day-to-day work.
33 |
34 | This article is aimed at absolute beginners (such as myself) who generally work with high-level programming languages, and who don't necessarily need to understand the ins-and-outs of the hardware onto which they deploy their code. As such, a lot of the terminology I use and concepts I describe may not be completely accurate -- but will hopefully serve as a general purpose guide for further exploration.
35 |
36 | > DISCLAIMER: the purpose of this writing is not to develop a working emulator from scratch, but to provide some guidance on how this could be achieved using comparable hardware. My goal is to focus more on the delightful aspects of emulator development than on the mundane nitty-gritty details.
37 |
38 | ## Selecting an emulation target
39 |
40 | As a way to motivate myself to learn yet another high-level programming language (Swift), I set myself a goal of writing a basic emulator for one of my favourite childhood consoles - the original 1989 Nintendo Game Boy handheld (also referred to as the Game Boy DMG).
41 |
42 | The choice of the hardware to emulate is important because..
43 |
44 | Indeed, a basic emulator for the Game Boy can be written just as well in [JavaScript](https://github.com/juchi/gameboy.js/) as it might be using a systems-level programming language such as [Rust](https://rylev.github.io/DMG-01/).
--------------------------------------------------------------------------------
/gb-cpu-code-generator/format.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const _ = require("lodash");
3 | const inputPath = "generated.swift"
4 | const outputPath = "formatted.swift";
5 |
6 | const text = fs.readFileSync(inputPath, "utf8");
7 | const all = text.split("\n");
8 |
9 | const result = all.reduce((acc, line) => {
10 | const trimmed = _.trim(line);
11 |
12 | if (trimmed.startsWith("OpCode.byte") || trimmed.startsWith("OpCode.word")) {
13 | acc.sourceBlocks.push([line])
14 | acc.commentBlocks.push([])
15 | } else if (trimmed.startsWith("//")) {
16 | const block = _.last(acc.commentBlocks)
17 | block.push(line)
18 | } else {
19 | const block = _.last(acc.sourceBlocks)
20 | if (block)
21 | block.push(line)
22 | }
23 |
24 | return acc;
25 | }, { sourceBlocks: [], commentBlocks: [] });
26 |
27 | const writeLine = (line) => {
28 | fs.appendFileSync(outputPath, `${line}\n`);
29 | };
30 |
31 | if (fs.existsSync(outputPath))
32 | fs.unlinkSync(outputPath);
33 |
34 | result.sourceBlocks.forEach((sb, index) => {
35 | const cb = result.commentBlocks[index];
36 |
37 | cb.map(_.trim).map(x => "\t" + x).forEach((line, idx) => {
38 | if (idx == (cb.length -1) && line == "\t//") {
39 | //-- Ignore unecessary spacers
40 | } else {
41 | writeLine(line)
42 | }
43 | })
44 | sb.forEach(writeLine)
45 | })
--------------------------------------------------------------------------------
/gb-cpu-code-generator/generate.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const fs = require("fs");
3 | const _ = require("lodash");
4 | const outputPath = "generated.swift";
5 |
6 | if (fs.existsSync(outputPath))
7 | fs.unlinkSync(outputPath);
8 |
9 | fetch("https://gist.githubusercontent.com/bberak/ca001281bb8431d2706afd31401e802b/raw/118ac680ac43cc3153c3c33344a692d37d2fd5a7/gb-instructions-db.json")
10 | .then((res) => res.json())
11 | .then((arr) => {
12 | const byteOps = _.sortBy(arr.filter((x) => x.opCode.length == 2), (x) => x.opCode);
13 | const wordOps = _.sortBy(arr.filter((x) => x.opCode.length == 4), (x) => x.opCode);
14 | writeLine("let instructions: [OpCode: Instruction] = [");
15 | [...byteOps, ...wordOps].forEach(writeInstruction);
16 | writeLine("]");
17 | });
18 |
19 | const writeLine = (line) => {
20 | fs.appendFileSync(outputPath, `${line}\n`);
21 | };
22 |
23 | const writeInstruction = (op) => {
24 | const cycles = op.cycles.length === 1 ? op.cycles : _.last(op.cycles.split("/"));
25 |
26 | writeLine(`\t// ${op.mnemonic}`);
27 | writeLine(`\t//`);
28 | writeLine(`\t// Cycles: ${op.cycles}`);
29 | writeLine(`\t// Bytes: ${op.bytes}`);
30 | writeLine(`\t// Flags: ${sanitizeFlags(op.flags)}`);
31 | writeLine(`\t//`);
32 |
33 | (op.description || "").split("\n").forEach((x) => writeLine(`\t// ${_.trim(x)}`));
34 |
35 | if (op.opCode.length === 2)
36 | writeLine(`\tOpCode.byte(0x${op.opCode}): Instruction.atomic(cycles: ${cycles}) { cpu in`);
37 | else
38 | writeLine(`\tOpCode.word(0x${op.opCode.replace("CB", "")}): Instruction.atomic(cycles: ${cycles}) { cpu in`);
39 |
40 |
41 | if (op.mnemonic.startsWith("LD")) {
42 | writeLD(op);
43 | writeNotImplementedError(op);
44 | }
45 | else if (op.mnemonic.startsWith("BIT")) {
46 | writeBIT(op);
47 | //-- Pretty confident these instruction are generated correctly..
48 | }
49 | else if (op.mnemonic.startsWith("RES")) {
50 | writeRES(op);
51 | //-- Pretty confident these instruction are generated correctly..
52 | }
53 | else if (op.mnemonic.startsWith("SET")) {
54 | writeSET(op);
55 | //-- Pretty confident these instruction are generated correctly..
56 | }
57 | else {
58 | writeFlags(op.flags);
59 | writeNotImplementedError(op);
60 | }
61 |
62 | writeLine(`\t},`);
63 | };
64 |
65 | const writeLD = (op) => {
66 | /*
67 | d8 - 8-bit immediate data value
68 | d16 - 16-bit immediate data value
69 | a8 - 8-bit immediate value specifying an address in the range 0xFF00 - 0xFFFF
70 | a16 - 16-bit immediate address value
71 | s8 - 8-bit signed immediate data value
72 | */
73 | const operands = op.mnemonic.replace(",", "").split(" ");
74 |
75 | if (operands.length === 3) {
76 | const destination = operands[1];
77 | const wordOperation = countLetters(destination) == 2;
78 | const source = operands[2];
79 |
80 | if (source === "d8") writeLine("\t\t//let data = try cpu.readNextByte()");
81 |
82 | if (source === "d16") writeLine("\t\t//let data = try cpu.readNextWord()");
83 |
84 | if (source === "a8") {
85 | writeLine("\t\t//let address = UInt16(try cpu.readNextByte() + 0xFF00)");
86 |
87 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: address)");
88 | else writeLine("\t\t//let data = try cpu.mmu.readWord(address: address)");
89 | }
90 |
91 | if (source === "a16") {
92 | writeLine("\t\t//let address = try cpu.readNextWord()");
93 |
94 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: address)");
95 | else writeLine("\t\t//let data = try cpu.mmu.readWord(address: address)");
96 | }
97 |
98 | if (source === "A")
99 | writeLine("\t\t//let data = cpu.a")
100 |
101 | if (source === "F")
102 | writeLine("\t\t//let data = cpu.f")
103 |
104 | if (source === "AF")
105 | writeLine("\t\t//let data = cpu.af");
106 |
107 | if (source === "(AF)") {
108 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: cpu.af)");
109 | else writeLine("\t\t//let data = try cpu.mmu.readByte(address: cpu.af)");
110 | }
111 |
112 | if (source === "B")
113 | writeLine("\t\t//let data = cpu.b")
114 |
115 | if (source === "C")
116 | writeLine("\t\t//let data = cpu.c")
117 |
118 | if (source === "BC")
119 | writeLine("\t\t//let data = cpu.bc");
120 |
121 | if (source === "(BC)") {
122 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: cpu.bc)");
123 | else writeLine("\t\t//let data = try cpu.mmu.readByte(address: cpu.bc)");
124 | }
125 |
126 | if (source === "D")
127 | writeLine("\t\t//let data = cpu.d")
128 |
129 | if (source === "E")
130 | writeLine("\t\t//let data = cpu.e")
131 |
132 | if (source === "DE")
133 | writeLine("\t\t//let data = cpu.de");
134 |
135 | if (source === "(DE)") {
136 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: cpu.de)");
137 | else writeLine("\t\t//let data = try cpu.mmu.readByte(address: cpu.de)");
138 | }
139 |
140 | if (source === "H")
141 | writeLine("\t\t//let data = cpu.h")
142 |
143 | if (source === "L")
144 | writeLine("\t\t//let data = cpu.l")
145 |
146 | if (source === "HL")
147 | writeLine("\t\t//let data = cpu.hl");
148 |
149 | if (source === "(HL)") {
150 | if (wordOperation) writeLine("\t\t//let data = try cpu.mmu.readWord(address: cpu.hl)");
151 | else writeLine("\t\t//let data = try cpu.mmu.readByte(address: cpu.hl)");
152 | }
153 |
154 | if (destination.indexOf("(") !== -1) {
155 | if (wordOperation) writeLine(`\t\t//try cpu.mmu.writeWord(address: cpu.${sanitizeRegister(destination)}, word: data)`)
156 | else writeLine(`\t\t//try cpu.mmu.writeByte(address: cpu.${sanitizeRegister(destination)}, byte: data)`)
157 | }
158 | else
159 | writeLine(`\t\t//cpu.${sanitizeRegister(destination)} = data`)
160 | }
161 |
162 | writeFlags(op.flags);
163 | };
164 |
165 | const writeBIT = (op) => {
166 | const operands = op.mnemonic.replace(",", "").split(" ");
167 |
168 | if (operands.length === 3) {
169 | const bit = operands[1];
170 | const source = operands[2];
171 |
172 | if (source === "A")
173 | writeLine("\t\tlet data = cpu.a")
174 |
175 | if (source === "F")
176 | writeLine("\t\tlet data = cpu.f")
177 |
178 | if (source === "AF")
179 | writeLine("\t\tlet data = cpu.af");
180 |
181 | if (source === "(AF)")
182 | writeLine("\t\tlet data = try cpu.mmu.readByte(address: cpu.af)");
183 |
184 | if (source === "B")
185 | writeLine("\t\tlet data = cpu.b")
186 |
187 | if (source === "C")
188 | writeLine("\t\tlet data = cpu.c")
189 |
190 | if (source === "BC")
191 | writeLine("\t\tlet data = cpu.bc");
192 |
193 | if (source === "(BC)")
194 | writeLine("\t\tlet data = try cpu.mmu.readByte(address: cpu.bc)");
195 |
196 | if (source === "D")
197 | writeLine("\t\tlet data = cpu.d")
198 |
199 | if (source === "E")
200 | writeLine("\t\tlet data = cpu.e")
201 |
202 | if (source === "DE")
203 | writeLine("\t\tlet data = cpu.de");
204 |
205 | if (source === "(DE)")
206 | writeLine("\t\tlet data = try cpu.mmu.readByte(address: cpu.de)");
207 |
208 | if (source === "H")
209 | writeLine("\t\tlet data = cpu.h")
210 |
211 | if (source === "L")
212 | writeLine("\t\tlet data = cpu.l")
213 |
214 | if (source === "HL")
215 | writeLine("\t\tlet data = cpu.hl");
216 |
217 | if (source === "(HL)")
218 | writeLine("\t\tlet data = try cpu.mmu.readByte(address: cpu.hl)");
219 |
220 | writeLine(`\t\tcpu.flags.zero = !data.bit(${bit})`);
221 | writeLine("\t\tcpu.flags.subtract = false");
222 | writeLine("\t\tcpu.flags.halfCarry = true");
223 | } else {
224 | writeFlags(op.flags);
225 | }
226 | };
227 |
228 | const writeRES = (op) => {
229 | const operands = op.mnemonic.replace(",", "").split(" ");
230 |
231 | if (operands.length === 3) {
232 | const bit = operands[1];
233 | const source = operands[2];
234 |
235 | if (source === "A")
236 | writeLine("\t\tvar data = cpu.a")
237 |
238 | if (source === "F")
239 | writeLine("\t\tvar data = cpu.f")
240 |
241 | if (source === "AF")
242 | writeLine("\t\tvar data = cpu.af");
243 |
244 | if (source === "(AF)")
245 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.af)");
246 |
247 | if (source === "B")
248 | writeLine("\t\tvar data = cpu.b")
249 |
250 | if (source === "C")
251 | writeLine("\t\tvar data = cpu.c")
252 |
253 | if (source === "BC")
254 | writeLine("\t\tvar data = cpu.bc");
255 |
256 | if (source === "(BC)")
257 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.bc)");
258 |
259 | if (source === "D")
260 | writeLine("\t\tvar data = cpu.d")
261 |
262 | if (source === "E")
263 | writeLine("\t\tvar data = cpu.e")
264 |
265 | if (source === "DE")
266 | writeLine("\t\tvar data = cpu.de");
267 |
268 | if (source === "(DE)")
269 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.de)");
270 |
271 | if (source === "H")
272 | writeLine("\t\tvar data = cpu.h")
273 |
274 | if (source === "L")
275 | writeLine("\t\tvar data = cpu.l")
276 |
277 | if (source === "HL")
278 | writeLine("\t\tvar data = cpu.hl");
279 |
280 | if (source === "(HL)")
281 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.hl)");
282 |
283 | writeLine(`\t\tdata = data.reset(${bit})`);
284 |
285 | if (source.indexOf("(") !== -1)
286 | writeLine(`\t\ttry cpu.mmu.writeByte(address: cpu.${sanitizeRegister(source)}, byte: data)`)
287 | else
288 | writeLine(`\t\tcpu.${sanitizeRegister(source)} = data`)
289 | }
290 | };
291 |
292 | const writeSET = (op) => {
293 | const operands = op.mnemonic.replace(",", "").split(" ");
294 |
295 | if (operands.length === 3) {
296 | const bit = operands[1];
297 | const source = operands[2];
298 |
299 | if (source === "A")
300 | writeLine("\t\tvar data = cpu.a")
301 |
302 | if (source === "F")
303 | writeLine("\t\tvar data = cpu.f")
304 |
305 | if (source === "AF")
306 | writeLine("\t\tvar data = cpu.af");
307 |
308 | if (source === "(AF)")
309 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.af)");
310 |
311 | if (source === "B")
312 | writeLine("\t\tvar data = cpu.b")
313 |
314 | if (source === "C")
315 | writeLine("\t\tvar data = cpu.c")
316 |
317 | if (source === "BC")
318 | writeLine("\t\tvar data = cpu.bc");
319 |
320 | if (source === "(BC)")
321 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.bc)");
322 |
323 | if (source === "D")
324 | writeLine("\t\tvar data = cpu.d")
325 |
326 | if (source === "E")
327 | writeLine("\t\tvar data = cpu.e")
328 |
329 | if (source === "DE")
330 | writeLine("\t\tvar data = cpu.de");
331 |
332 | if (source === "(DE)")
333 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.de)");
334 |
335 | if (source === "H")
336 | writeLine("\t\tvar data = cpu.h")
337 |
338 | if (source === "L")
339 | writeLine("\t\tvar data = cpu.l")
340 |
341 | if (source === "HL")
342 | writeLine("\t\tvar data = cpu.hl");
343 |
344 | if (source === "(HL)")
345 | writeLine("\t\tvar data = try cpu.mmu.readByte(address: cpu.hl)");
346 |
347 | writeLine(`\t\tdata = data.set(${bit})`);
348 |
349 | if (source.indexOf("(") !== -1)
350 | writeLine(`\t\ttry cpu.mmu.writeByte(address: cpu.${sanitizeRegister(source)}, byte: data)`)
351 | else
352 | writeLine(`\t\tcpu.${sanitizeRegister(source)} = data`)
353 | }
354 | };
355 |
356 | const writeFlags = (flags) => {
357 | if (flags.Z) writeLine("\t\t//cpu.flags.zero = result.zero");
358 | if (flags.N) writeLine("\t\t//cpu.flags.subtract = result.subtract");
359 | if (flags.H) writeLine("\t\t//cpu.flags.halfCarry = result.halfCarry");
360 | if (flags.CY) writeLine("\t\t//cpu.flags.carry = result.carry");
361 | };
362 |
363 | const writeNotImplementedError = (op) => {
364 | if (op.opCode.length === 2)
365 | writeLine(`\t\tthrow CPUError.instructionNotImplemented(OpCode.byte(0x${op.opCode}))`);
366 | else
367 | writeLine(`\t\tthrow CPUError.instructionNotImplemented(OpCode.word(0x${op.opCode.replace("CB", "")}))`);
368 | };
369 |
370 | const sanitizeFlags = (flags) => {
371 | let result = "";
372 |
373 | if (flags.Z) result += flags.Z;
374 | else result += "-";
375 |
376 | result += " ";
377 |
378 | if (flags.N) result += flags.N;
379 | else result += "-";
380 |
381 | result += " ";
382 |
383 | if (flags.H) result += flags.H;
384 | else result += "-";
385 |
386 | result += " ";
387 |
388 | if (flags.CY) result += flags.CY;
389 | else result += "-";
390 |
391 | return result;
392 | };
393 |
394 | const countLetters = (destination) => {
395 | return (destination.match(/is/g) || []).length;
396 | };
397 |
398 | const sanitizeRegister = (destination) => {
399 | let result = destination
400 | let arr = ["+", "-", "(", ")"]
401 |
402 | arr.forEach(x => {
403 | result = result.replace(x, "")
404 | })
405 |
406 | return result.toLowerCase()
407 | };
408 |
--------------------------------------------------------------------------------
/gb-cpu-code-generator/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gb-cpu-code-generator",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "lodash": {
8 | "version": "4.17.21",
9 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
10 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
11 | },
12 | "node-fetch": {
13 | "version": "2.6.1",
14 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
15 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/gb-cpu-code-generator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gb-cpu-code-generator",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "lodash": "^4.17.21",
13 | "node-fetch": "^2.6.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------