-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
775f67f
commit cc98a46
Showing
60 changed files
with
6,316 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
build/ | ||
dugb | ||
dugb.exe | ||
dawngb | ||
dawngb.exe | ||
rom/ | ||
|
||
src/godot/**/*.gb | ||
!src/godot/**/hello.gb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Akihiro Otomo | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# DawnGB | ||
|
||
DawnGB is GameBoy Color emulator written in Go. | ||
|
||
You can play on [web](https://dawngb.vercel.app/)! | ||
|
||
## Screenshots | ||
|
||
### Desktop | ||
|
||
<img width="300" alt="prism" src="https://gyazo.com/82888eedb9501fb6a7c83c2b76f1fe8a.webp" /> <img width="300" alt="megaman" src="https://gyazo.com/6a65b22547c7cddeb07a77ad5400afc4.webp" /> | ||
<img width="300" alt="shantae" src="https://gyazo.com/d0293d5fc976614a0322f44b3e6c8130.webp" /> <img width="300" alt="pokered" src="https://gyazo.com/043aa023624a1da45f6e8487cf33143d.webp" /> | ||
|
||
### Browser(Web) | ||
|
||
<img width="28.5%" height="20%" src="https://gyazo.com/3bf894c527bdd932aab37e0c82f67091.webp" /> <img width="62%" src="https://gyazo.com/9e773470f1db70aad0098e6d98187e4f.webp" /> | ||
|
||
## Features | ||
|
||
- GB(DMG) and GBC(CGB) support | ||
- MBC1, MBC3, MBC5, MBC30 support | ||
- Sound(APU) support | ||
- Libretro support(run `make libretro`) | ||
- Multiplatform support | ||
- Work on Browser([here](https://dawngb.vercel.app/)) | ||
|
||
## Usage | ||
|
||
- Desktop: Run `go run ./src/ebi` and drag and drop a ROM file into the window. | ||
- Browser: Visit [here](https://dawngb.vercel.app/). | ||
|
||
Key mapping is as follows: | ||
|
||
- `A`: X | ||
- `B`: Z | ||
- `Start`: Enter | ||
- `Select`: Backspace | ||
- `↑` `↓` `←` `→`: Arrow keys | ||
|
||
## Internal | ||
|
||
```sh | ||
. | ||
├── core # Emulator core | ||
├── src # Frontend | ||
└── util # Utility (should be renamed to "internal" in the future) | ||
``` | ||
|
||
## Accuracy | ||
|
||
Keep the code as simple as possible, so synchronization is done at each instruction, and line-rendering is done at once on HBlank. | ||
|
||
So game like "Prehistorik Man", which modifies the PPU registers during mid-frame, may not draw correctly. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package core | ||
|
||
import ( | ||
"image/color" | ||
"io" | ||
|
||
"github.com/akatsuki105/dawngb/core/gb" | ||
) | ||
|
||
type ID = string | ||
|
||
const ( | ||
GB ID = "GB" | ||
) | ||
|
||
type Core interface { | ||
ID() ID // Get Core ID | ||
Reset(hasBIOS bool) | ||
LoadROM(romData []byte) error // romData is mutable(not copied). | ||
SRAM() []byte | ||
LoadSRAM(sramData []byte) error | ||
RunFrame() | ||
Resolution() (w int, h int) // Get display resolution | ||
Screen() []color.RGBA | ||
SetKeyInput(key string, press bool) | ||
Title() string // Get title of the game | ||
|
||
// Serialize | ||
Serialize(state io.Writer) | ||
Deserialize(state io.Reader) | ||
} | ||
|
||
func New(id ID, audioBuffer io.Writer) Core { | ||
switch id { | ||
case GB: | ||
return gb.New(audioBuffer) | ||
default: | ||
panic("invalid core id. valid core id is {GB}") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package audio | ||
|
||
import "io" | ||
|
||
type Audio interface { | ||
Reset(hasBIOS bool) | ||
|
||
Tick(cycles int64) | ||
CatchUp() | ||
|
||
Read(addr uint16) uint8 | ||
Write(addr uint16, val uint8) | ||
} | ||
|
||
type audio struct { | ||
enabled bool | ||
|
||
ch1, ch2 *square | ||
ch3 *wave | ||
ch4 *noise | ||
|
||
sampleBuffer io.Writer | ||
cycles int64 // 遅れているサイクル数(8.3MHzのマスターサイクル単位) | ||
|
||
sequencerCounter int64 // (フレームシーケンサの)512Hzを生み出すためのカウンタ (ref: https://gbdev.io/pandocs/Audio_details.html#div-apu) | ||
sequencerStep int64 // 512Hzから 64, 128, 256Hzなどの生み出すためのカウンタ | ||
|
||
sampleTimer int64 // 1サンプルを生み出すために44100Hzを生み出すためのカウンタ | ||
|
||
ioreg [0x30]uint8 | ||
volume [2]int // NR50(Left, Right) | ||
} | ||
|
||
func New(sampleBuffer io.Writer) Audio { | ||
return &audio{ | ||
sampleBuffer: sampleBuffer, | ||
} | ||
} | ||
|
||
func (a *audio) Reset(hasBIOS bool) { | ||
a.enabled = false | ||
a.ch1 = newSquareChannel(true) | ||
a.ch2 = newSquareChannel(false) | ||
a.ch3 = newWaveChannel() | ||
a.ch4 = newNoiseChannel() | ||
a.cycles = 0 | ||
a.sequencerCounter = 0 | ||
a.sequencerStep = 0 | ||
a.sampleTimer = 0 | ||
a.volume = [2]int{7, 7} | ||
a.ioreg = [0x30]uint8{} | ||
if !hasBIOS { | ||
a.skipBIOS() | ||
} | ||
} | ||
|
||
func (a *audio) skipBIOS() { | ||
a.Write(0xFF10, 0x80) | ||
a.Write(0xFF11, 0xBF) | ||
a.Write(0xFF12, 0xF3) | ||
a.Write(0xFF13, 0xFF) | ||
a.Write(0xFF14, 0xBF) | ||
a.Write(0xFF16, 0x3F) | ||
a.Write(0xFF17, 0x00) | ||
a.Write(0xFF18, 0xFF) | ||
a.Write(0xFF19, 0xBF) | ||
a.Write(0xFF1A, 0x7F) | ||
a.Write(0xFF1B, 0xFF) | ||
a.Write(0xFF1C, 0x9F) | ||
a.Write(0xFF1D, 0xFF) | ||
a.Write(0xFF1E, 0xBF) | ||
a.Write(0xFF20, 0xFF) | ||
a.Write(0xFF21, 0x00) | ||
a.Write(0xFF22, 0x00) | ||
a.Write(0xFF23, 0xBF) | ||
a.Write(0xFF24, 0x77) | ||
a.Write(0xFF25, 0xF3) | ||
a.Write(0xFF26, 0xF1) | ||
} | ||
|
||
func (a *audio) Tick(cycles int64) { | ||
a.cycles += cycles | ||
} | ||
|
||
func (a *audio) CatchUp() { | ||
apuCycles := a.cycles / 2 // APU は 4.19MHz で動作する, マスターサイクルを 8.3MHz とすると 8.3MHz / 4.19MHz = 2 | ||
|
||
for i := int64(0); i < apuCycles; i++ { | ||
if a.enabled { | ||
if a.sequencerCounter > 0 { | ||
a.sequencerCounter-- | ||
} else { | ||
is64Hz := a.sequencerStep == 7 // Envelope sweep | ||
is128Hz := a.sequencerStep == 2 || a.sequencerStep == 6 // CH1 freq sweep | ||
is256Hz := a.sequencerStep == 0 || a.sequencerStep == 2 || a.sequencerStep == 4 || a.sequencerStep == 6 // Sound length | ||
|
||
if is256Hz { | ||
a.ch1.clock256Hz() | ||
a.ch2.clock256Hz() | ||
a.ch3.clock256Hz() | ||
a.ch4.clock256Hz() | ||
} | ||
if is128Hz { | ||
a.ch1.clock128Hz() | ||
} | ||
if is64Hz { | ||
a.ch1.clock64Hz() | ||
a.ch2.clock64Hz() | ||
a.ch4.clock64Hz() | ||
} | ||
|
||
a.sequencerStep = (a.sequencerStep + 1) % 8 | ||
a.sequencerCounter = 8192 // 512Hz = 4194304/8192 | ||
} | ||
|
||
a.ch1.clockTimer() | ||
a.ch2.clockTimer() | ||
a.ch3.clockTimer() | ||
a.ch4.clockTimer() | ||
|
||
// サンプルを生成 | ||
if a.sampleTimer <= 0 { | ||
sample := (a.ch1.getOutput() + a.ch2.getOutput() + a.ch3.getOutput() + a.ch4.getOutput()) // 各チャンネルの出力(音量=波)を足し合わせたものがサンプル | ||
if a.sampleBuffer != nil { | ||
left := uint8((sample * a.volume[0]) / 7) | ||
right := uint8((sample * a.volume[1]) / 7) | ||
a.sampleBuffer.Write([]byte{0, left, 0, right}) | ||
} | ||
|
||
a.sampleTimer = 95 // 44100Hzにダウンサンプリングしたい = 44100Hzごとにサンプルを生成したい = 95APUサイクルごとにサンプルを生成したい(4194304/44100 = 95) | ||
} | ||
a.sampleTimer-- | ||
} | ||
} | ||
|
||
a.cycles -= apuCycles * 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package audio | ||
|
||
type noise struct { | ||
enabled bool | ||
ignored bool // Ignore sample output | ||
|
||
length int // 音の残り再生時間 | ||
stop bool | ||
|
||
envelope *envelope | ||
|
||
lfsr uint16 // Noiseの疑似乱数(lfsr: Linear Feedback Shift Register = 疑似乱数生成アルゴリズム) | ||
|
||
// この2つでノイズの周波数(疑似乱数の生成頻度)を決める | ||
octave int // ノイズ周波数2(オクターブ指定) | ||
divisor int // ノイズ周波数1(カウント指定) | ||
period int | ||
|
||
width int | ||
|
||
output int | ||
} | ||
|
||
func newNoiseChannel() *noise { | ||
return &noise{ | ||
ignored: true, | ||
envelope: newEnvelope(), | ||
lfsr: 1, | ||
width: 15, | ||
} | ||
} | ||
|
||
func (ch *noise) clock64Hz() { | ||
if ch.enabled { | ||
ch.envelope.update() | ||
} | ||
} | ||
|
||
func (ch *noise) clock256Hz() { | ||
if ch.stop && ch.length > 0 { | ||
ch.length-- | ||
if ch.length <= 0 { | ||
ch.enabled = false | ||
} | ||
} | ||
} | ||
|
||
func (ch *noise) clockTimer() { | ||
// ch.enabledに関わらず、乱数は生成される | ||
result := 0 | ||
ch.period-- | ||
if ch.period <= 0 { | ||
ch.period = ch.calcFreqency() | ||
if ch.octave < 14 { | ||
mask := ((ch.lfsr ^ (ch.lfsr >> 1)) & 1) | ||
ch.lfsr = ((ch.lfsr >> 1) ^ (mask << (ch.width - 1))) & 0x7FFF | ||
} | ||
} | ||
|
||
if (ch.lfsr & 1) == 0 { | ||
result = ch.envelope.volume | ||
} | ||
|
||
if !ch.enabled { | ||
result = 0 | ||
} | ||
|
||
ch.output = result | ||
} | ||
|
||
func (ch *noise) calcFreqency() int { | ||
freq := 1 | ||
if ch.divisor != 0 { | ||
freq = 2 * ch.divisor | ||
} | ||
freq <<= ch.octave | ||
return freq * 8 | ||
} | ||
|
||
func (ch *noise) getOutput() int { | ||
if !ch.ignored { | ||
if ch.enabled { | ||
return ch.output | ||
} | ||
} | ||
return 0 | ||
} | ||
|
||
func (ch *noise) tryRestart() { | ||
ch.enabled = ch.dacEnable() | ||
ch.envelope.reset() | ||
if ch.length == 0 { | ||
ch.length = 64 | ||
} | ||
ch.lfsr = 0x7FFF >> (15 - ch.width) | ||
} | ||
|
||
func (ch *noise) dacEnable() bool { | ||
return ((ch.envelope.initialVolume != 0) || ch.envelope.direction) | ||
} |
Oops, something went wrong.