├── .codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── funding.yml └── workflows │ ├── codecov.yaml │ ├── go.yaml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── nesgo │ ├── README.md │ ├── main.go │ └── version.go ├── nesgoemu │ ├── README.md │ ├── main.go │ └── version.go └── nesgogg │ ├── README.md │ ├── main.go │ └── version.go ├── docs └── gui.md ├── examples ├── blue │ └── main.go └── debugprint │ └── main.go ├── go.mod ├── go.sum ├── internal ├── ast │ ├── argument.go │ ├── branching.go │ ├── constant.go │ ├── doc.go │ ├── error.go │ ├── file.go │ ├── for.go │ ├── function.go │ ├── identifier.go │ ├── if.go │ ├── import.go │ ├── instruction.go │ ├── list.go │ ├── nes.go │ ├── node.go │ ├── package.go │ ├── statement.go │ ├── tests │ │ ├── branch_test.go │ │ ├── call_test.go │ │ ├── const_test.go │ │ ├── for_test.go │ │ ├── function_test.go │ │ ├── if_test.go │ │ ├── instruction_test.go │ │ ├── test_helper.go │ │ └── var_test.go │ ├── type.go │ ├── value.go │ └── variable.go ├── compiler │ ├── compiler.go │ ├── config.go │ ├── constant.go │ ├── expression.go │ ├── file.go │ ├── file_test.go │ ├── function.go │ ├── function_test.go │ ├── import.go │ ├── import_test.go │ ├── label.go │ ├── label_test.go │ ├── output.go │ ├── package.go │ ├── test_helper.go │ └── variable.go ├── gocc │ ├── errors │ │ └── errors.go │ ├── lang.bnf │ ├── lexer │ │ ├── acttab.go │ │ ├── lexer.go │ │ └── transitiontable.go │ ├── parser │ │ ├── action.go │ │ ├── actiontable.go │ │ ├── context.go │ │ ├── gototable.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ └── productionstable.go │ ├── token │ │ ├── context.go │ │ └── token.go │ └── util │ │ ├── litconv.go │ │ └── rune.go └── testroms │ └── nestest │ ├── nestest.log │ ├── nestest.nes │ ├── nestest_no_ppu.log │ └── nestest_test.go └── pkg ├── apu ├── const.go └── const_translation.go ├── bus ├── bus.go ├── controller.go ├── cpu.go ├── mapper.go ├── memory.go └── ppu.go ├── ca65 ├── external.go └── mapper.go ├── controller ├── const.go ├── const_translation.go ├── controller.go └── controller_test.go ├── cpu ├── addressing.go ├── cpu.go ├── debugger.go ├── emulation.go ├── flags.go ├── helper.go ├── instruction.go ├── interrupt.go ├── params.go ├── timing.go ├── tracing.go └── unofficial.go ├── gamegenie ├── gamegenie.go ├── gamegenie_test.go └── translation.go ├── mapper ├── mapper.go ├── mapperbase │ ├── bank.go │ ├── base.go │ ├── chr.go │ ├── hook.go │ ├── nametable.go │ └── prg.go ├── mapperdb │ ├── axrom.go │ ├── axrom_test.go │ ├── base.go │ ├── cnrom.go │ ├── cnrom_test.go │ ├── gtrom.go │ ├── gtrom_test.go │ ├── mmc1.go │ ├── mmc1_test.go │ ├── nrom.go │ ├── nrom_test.go │ ├── unrom512.go │ ├── unrom512_test.go │ ├── uxrom.go │ └── uxrom_test.go └── mock.go ├── memory ├── memory.go ├── memory_test.go ├── modes.go └── ram.go ├── nes ├── compiler.go ├── const.go ├── cpu.go ├── cpu_test.go ├── debugger │ ├── cpu.go │ ├── debugger.go │ ├── helper.go │ ├── mapper.go │ └── ppu.go ├── input.go ├── nogui.go ├── option.go ├── params.go ├── start.go ├── system.go └── variables.go ├── neslib ├── controller.go ├── doc.go ├── init.go ├── init_test.go ├── math.go ├── math_test.go ├── ppu.go └── rand.go └── ppu ├── addressing ├── addressing.go ├── register.go └── register_test.go ├── colors.go ├── const.go ├── const_translation.go ├── control ├── const.go └── control.go ├── mask ├── const.go └── mask.go ├── memory └── memory.go ├── nametable ├── nametable.go └── nametable_test.go ├── nmi └── nmi.go ├── palette ├── palette.go └── palette_test.go ├── ppu.go ├── ppu_test.go ├── register.go ├── render.go ├── renderstate └── renderstate.go ├── screen └── screen.go ├── sprites ├── sprite.go └── sprites.go ├── status └── status.go └── tiles └── tiles.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: header, changes, diff 3 | coverage: 4 | status: 5 | patch: false 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **System (please complete the following information):** 20 | - OS: 21 | - Version / Commit: 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | patreon: cornel 2 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yaml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [review_requested, ready_for_review] 9 | 10 | jobs: 11 | codecov: 12 | timeout-minutes: 15 13 | 14 | name: Coverage 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Set up Go 1.x 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: "1.20" 21 | id: go 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Get dependencies 29 | run: go mod download 30 | 31 | - name: Run tests with coverage 32 | run: make test-coverage 33 | 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v3 36 | with: 37 | file: ./.testCoverage 38 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - ready_for_review 13 | 14 | jobs: 15 | build: 16 | if: ${{ github.event_name == 'push' || !github.event.pull_request.draft || !contains(github.event.commits[0].message, '[skip ci]') }} 17 | timeout-minutes: 15 18 | 19 | name: Build 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | go: [ "1.19", "1.20" ] 24 | 25 | steps: 26 | - name: Set up Go 1.x 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: ${{ matrix.go }} 30 | id: go 31 | 32 | - name: Check out code into the Go module directory 33 | uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Install linters 38 | run: make install-linters 39 | 40 | - name: Install ca65 41 | run: sudo apt-get install -y cc65 42 | 43 | - name: Get dependencies 44 | run: go mod download 45 | 46 | - name: Run tests 47 | run: make test-no-gui 48 | 49 | - name: Run linter 50 | run: make lint 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - 11 | name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - 16 | name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: '1.20' 20 | check-latest: true 21 | cache: true 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v4 25 | with: 26 | version: latest 27 | args: release --clean 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | .idea 3 | .vscode 4 | *.iml 5 | *.local 6 | /*.log 7 | *.out 8 | *.prof 9 | *.test 10 | .DS_Store 11 | *.dmp 12 | *.db 13 | 14 | *.asm 15 | *.o 16 | *.dbg 17 | examples/**/*.nes 18 | examples/colortest 19 | .testCoverage 20 | dist/ 21 | internal/testroms/commercial/ 22 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - id: nesgodisasm 7 | binary: nesgodisasm 8 | dir: cmd/nesgodisasm 9 | env: 10 | - CGO_ENABLED=0 11 | targets: 12 | - go_first_class 13 | flags: 14 | - -trimpath 15 | ldflags: 16 | - -s -w -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} 17 | 18 | universal_binaries: 19 | - replace: false 20 | 21 | archives: 22 | - id: nesgodisasm 23 | builds: ['nesgodisasm'] 24 | name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 25 | replacements: 26 | 386: 32bit 27 | amd64: 64bit 28 | darwin: macos 29 | format_overrides: 30 | - goos: windows 31 | format: zip 32 | 33 | checksum: 34 | name_template: 'checksums.txt' 35 | 36 | snapshot: 37 | name_template: "{{ .Tag }}-snapshot" 38 | 39 | changelog: 40 | skip: true 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.1.1] - 2022-08-02 6 | 7 | nesgodisasm v0.1.1 8 | 9 | Added: 10 | 11 | * add var aliases for zeropage accesses 12 | * support code/data logs 13 | * support more mappers 14 | * unofficial instruction opcodes are bundled 15 | 16 | Fixed: 17 | 18 | * fix wrong address in comments for non standard rom base addresses 19 | * support data references into instruction opcodes 20 | 21 | ## [0.1.0] - 2022-06-26 22 | 23 | First version of nesgodisasm released. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANGCI_VERSION = v1.53.3 2 | 3 | help: ## show help, shown by default if no target is specified 4 | @grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | generate: ## regenerate all files 7 | cd internal/gocc && gocc -p github.com/retroenv/nesgo/internal/gocc -a lang.bnf 8 | 9 | lint: ## run code linters 10 | golangci-lint run 11 | 12 | build-all: ## build code with all 3 GUI mode settings 13 | go build ./... 14 | go build -tags noopengl,sdl ./... 15 | go build -tags nogui ./... 16 | 17 | test: install run-tests ## run tests 18 | go test -timeout 10s -race ./... 19 | 20 | run-tests: 21 | nesgo -q -o ./examples/blue/main.nes ./examples/blue/main.go 22 | nesgo -q -o ./examples/debugprint/main.nes ./examples/debugprint/main.go 23 | 24 | test-no-gui: install-no-gui run-tests ## run unit tests with gui disabled 25 | go test -timeout 10s -tags nogui ./... 26 | 27 | test-coverage: ## run unit tests and create test coverage 28 | go test -timeout 10s -tags nogui ./... -coverprofile .testCoverage -covermode=atomic -coverpkg=./... 29 | 30 | test-coverage-web: test-coverage ## run unit tests and show test coverage in browser 31 | go tool cover -func .testCoverage | grep total | awk '{print "Total coverage: "$$3}' 32 | go tool cover -html=.testCoverage 33 | 34 | install: ## install all binaries 35 | go install ./cmd/... 36 | 37 | install-no-gui: ## install all binaries with gui disabled 38 | go install -tags nogui ./cmd/... 39 | 40 | install-linters: ## install all used linters 41 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_VERSION} 42 | 43 | release: ## build release binaries for current git tag and publish on github 44 | goreleaser release 45 | 46 | release-snapshot: ## build release binaries from current git state as snapshot 47 | goreleaser release --snapshot --rm-dist 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nesgo - Golang based tooling for the NES 2 | 3 | [![Build status](https://github.com/retroenv/nesgo/actions/workflows/go.yaml/badge.svg?branch=main)](https://github.com/retroenv/nesgo/actions) 4 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/retroenv/nesgo) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/retroenv/nesgo)](https://goreportcard.com/report/github.com/retroenv/nesgo) 6 | [![codecov](https://codecov.io/gh/retroenv/nesgo/branch/main/graph/badge.svg?token=NS5UY28V3A)](https://codecov.io/gh/retroenv/nesgo) 7 | 8 | nesgo offers tooling for the Nintendo Entertainment System (NES), written in Golang. 9 | 10 | ## Available tools 11 | 12 | | Tool | Description | 13 | |----------------------------------------------------------------------------|---------------------------------| 14 | | [nesgo](https://github.com/retroenv/nesgo/tree/main/cmd/nesgo) | Golang to NES compiler | 15 | | [nesgoemu](https://github.com/retroenv/nesgo/tree/main/cmd/nesgoemu) | Emulator for NES ROMs | 16 | | [nesgogg](https://github.com/retroenv/nesgo/tree/main/cmd/nesgogg) | NES Game Genie decoder/encoder | 17 | 18 | check the README of each tool for a more detailed description and instructions on how to install and use them. 19 | 20 | ## Project layout 21 | 22 | ├─ cmd tools main directories 23 | ├─ docs documentation 24 | ├─ example/ NES Program examples in Golang 25 | ├─ internal/ internal compiler code 26 | ├─ pkg/ libraries used by different packages and tools 27 | ├─ pkg/neslib helper useful for writing NES programs in Golang 28 | -------------------------------------------------------------------------------- /cmd/nesgo/README.md: -------------------------------------------------------------------------------- 1 | # nesgo - Golang to NES ROM compiler 2 | 3 | nesgo allows you to write programs for the Nintendo Entertainment System (NES) using Golang. 4 | 5 | ## Features 6 | 7 | - Code autocompletion in any IDE that supports Golang 8 | - The code can be debugged directly from an IDE at source level, it will be executed in the built-in Emulator 9 | - Easy unit testing of code 10 | - Simple code documentation generation 11 | - Alias functions for 6502 CPU instructions to allow full control over output 12 | * Outputs a ca65 compatible .asm file to allow easy inspection of generated code 13 | 14 | Check the [issue tracker](https://github.com/retroenv/nesgo/issues?q=is%3Aissue+is%3Aopen+label%3Acompiler) for planned features or known bugs. 15 | 16 | **nesgo is in an early stage of development and needs more work before it 17 | can be used to build a larger project for NES!** 18 | 19 | ## Installation 20 | 21 | Your system needs to have a recent [Golang](https://go.dev/) version installed. 22 | 23 | [cc65](https://github.com/cc65/cc65) needs to be installed, it is used for generating 24 | the final .nes file from assembly output generated by nesgo. 25 | It is planned to remove this dependency in future versions. 26 | 27 | To use the GUI mode check [GUI installation](https://github.com/retroenv/nesgo/blob/main/docs/gui.md) to set up the GUI dependencies. 28 | 29 | Install the latest stable version by running: 30 | 31 | ``` 32 | go install github.com/retroenv/nesgo/cmd/nesgo@latest 33 | ``` 34 | 35 | The latest development version can be installed using: 36 | 37 | ``` 38 | git clone https://github.com/retroenv/nesgo.git 39 | cd nesgo 40 | go build ./cmd/nesgo 41 | # use the dev version: 42 | ./nesgo 43 | ``` 44 | 45 | ## Usage 46 | 47 | See one of the examples on how to write code for the NES using Golang. 48 | 49 | The first compilation can take a few minutes depending on the system, 50 | this is due to Golang compiling the CGO GUI dependencies. 51 | 52 | nesgo can be used in different ways: 53 | 54 | 1. Compile a project to a .nes file: 55 | `nesgo -f ./examples/blue/main.go -o ./examples/blue/main.nes` 56 | 57 | 2. Use `go build ./examples/blue/main.go` to compile the program as a 58 | static binary including the Emulator 59 | 60 | 3. Run the code in the Emulator using `go run ./examples/blue/main.go` 61 | 62 | 4. Debug the program using your IDE of choice using the Delve debugger, 63 | set breakpoints, watch memory or CPU register, execute the code step by step etc 64 | 65 | 5. Run the generated .nes file using the `go run ./cmd/nesgoemu -f examples/blue/main.nes` 66 | 67 | ## Options 68 | 69 | ``` 70 | usage: nesgo [options] 71 | 72 | -o string 73 | name of the output .nes file 74 | -q perform operations quietly 75 | ``` 76 | 77 | ## Differences / Limitations 78 | 79 | * `return` has to be used instead of `rts` - it will get automatically 80 | added at the end of functions that are not inlined 81 | * `goto` has to be used instead of `jump` - it is limited to the labels in the 82 | current function as jump destination 83 | * for instructions that accept multiple addressing modes, the parameters can be 84 | cast into a helper type to set the mode, using the identifiers 85 | `ZeroPage`, `Absolute` or `Indirect`. If the instruction supports 86 | an immediate parameter, it will be set by default 87 | -------------------------------------------------------------------------------- /cmd/nesgo/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a Golang for NES Compiler 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/retroenv/nesgo/internal/compiler" 10 | "github.com/retroenv/nesgo/pkg/ca65" 11 | "github.com/retroenv/retrogolib/buildinfo" 12 | ) 13 | 14 | type optionFlags struct { 15 | input string 16 | output string 17 | 18 | quiet bool 19 | } 20 | 21 | func main() { 22 | options := readArguments() 23 | 24 | if !options.quiet { 25 | printBanner(options) 26 | fmt.Printf("Compiling %s\n", options.input) 27 | } 28 | 29 | if err := compileFile(options); err != nil { 30 | fmt.Println(fmt.Errorf("error: %w", err)) 31 | os.Exit(1) 32 | } 33 | 34 | if !options.quiet { 35 | fmt.Printf("Output file %s created successfully\n", options.output) 36 | } 37 | } 38 | 39 | func readArguments() optionFlags { 40 | flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 41 | options := optionFlags{} 42 | 43 | flags.StringVar(&options.output, "o", "", "name of the output .nes file") 44 | flags.BoolVar(&options.quiet, "q", false, "perform operations quietly") 45 | 46 | err := flags.Parse(os.Args[1:]) 47 | args := flags.Args() 48 | if err != nil || len(args) == 0 || options.output == "" { 49 | printBanner(options) 50 | fmt.Printf("usage: nesgo [options] \n\n") 51 | flags.PrintDefaults() 52 | os.Exit(1) 53 | } 54 | 55 | options.input = args[0] 56 | return options 57 | } 58 | 59 | func printBanner(options optionFlags) { 60 | if !options.quiet { 61 | fmt.Println("[---------------------------------]") 62 | fmt.Println("[ nesgo - Golang for NES Compiler ]") 63 | fmt.Printf("[---------------------------------]\n\n") 64 | fmt.Printf("version: %s\n\n", buildinfo.Version(version, commit, date)) 65 | } 66 | } 67 | 68 | func compileFile(options optionFlags) error { 69 | cfg := &compiler.Config{} 70 | c, err := compiler.New(cfg) 71 | if err != nil { 72 | return fmt.Errorf("creating compiler: %w", err) 73 | } 74 | 75 | data, err := os.ReadFile(options.input) 76 | if err != nil { 77 | return fmt.Errorf("reading file: %w", err) 78 | } 79 | if err = c.Parse(options.input, data); err != nil { 80 | return fmt.Errorf("parsing file '%s': %w", options.input, err) 81 | } 82 | 83 | asmFile, objectFile, err := c.OutputAsmFile(options.output) 84 | if err != nil { 85 | return fmt.Errorf("compiling to file '%s' failed: %w", options.output, err) 86 | } 87 | 88 | // TODO pass real options 89 | ca65Config := ca65.Config{ 90 | PrgBase: 0x8000, 91 | PRGSize: 0x8000, 92 | CHRSize: 0x2000, 93 | } 94 | 95 | if err = ca65.AssembleUsingExternalApp(asmFile, objectFile, options.output, ca65Config); err != nil { 96 | return fmt.Errorf("creating .nes file '%s' failed: %w", options.output, err) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /cmd/nesgo/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | version = "dev" 5 | commit = "" 6 | date = "" 7 | ) 8 | -------------------------------------------------------------------------------- /cmd/nesgoemu/README.md: -------------------------------------------------------------------------------- 1 | # nesgoemu - Emulator for NES ROMs 2 | 3 | nesgoemu allows you to emulate ROMs for the Nintendo Entertainment System (NES). 4 | 5 | ## Features 6 | 7 | * Offers the GUI in SDL or OpenGL mode 8 | * Can be used headless without a GUI 9 | * Supports outputting of CPU traces 10 | * Supports undocumented 6502 CPU opcodes 11 | 12 | Check the [issue tracker](https://github.com/retroenv/nesgo/labels/emulator) for planned features or known bugs. 13 | 14 | ## Installation 15 | 16 | Your system needs to have a recent [Golang](https://go.dev/) version installed. 17 | 18 | Check [GUI installation](https://github.com/retroenv/nesgo/blob/main/docs/gui.md) to set up the GUI dependencies. 19 | 20 | Install the latest stable version by running: 21 | 22 | ``` 23 | go install github.com/retroenv/nesgo/cmd/nesgoemu@latest 24 | ``` 25 | 26 | The latest development version can be installed using: 27 | 28 | ``` 29 | git clone https://github.com/retroenv/nesgo.git 30 | cd nesgo 31 | go build ./cmd/nesgoemu 32 | # use the dev version: 33 | ./nesgoemu 34 | ``` 35 | 36 | ## Usage 37 | 38 | Emulate a ROM: 39 | 40 | ``` 41 | nesgoemu example.nes 42 | ``` 43 | 44 | ## Options 45 | 46 | ``` 47 | usage: nesgoemu [options] 48 | 49 | -a string 50 | listening address for the debug server to use (default "127.0.0.1:8080") 51 | -c console mode, disable GUI 52 | -d start built-in webserver for debug mode 53 | -e int 54 | entrypoint to start the CPU (default -1) 55 | -s int 56 | stop execution at address (default -1) 57 | -t print CPU tracing 58 | ``` 59 | -------------------------------------------------------------------------------- /cmd/nesgoemu/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a NES ROM emulator 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/retroenv/nesgo/pkg/nes" 10 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 11 | "github.com/retroenv/retrogolib/buildinfo" 12 | ) 13 | 14 | type optionFlags struct { 15 | input string 16 | 17 | debug bool 18 | debugAddress string 19 | 20 | entrypoint int 21 | noGui bool 22 | stopAt int 23 | tracing bool 24 | } 25 | 26 | func main() { 27 | options := readArguments() 28 | 29 | if err := emulateFile(options); err != nil { 30 | fmt.Println(fmt.Errorf("emulation failed: %w", err)) 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func readArguments() optionFlags { 36 | flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 37 | options := optionFlags{} 38 | 39 | flags.BoolVar(&options.debug, "d", false, "start built-in webserver for debug mode") 40 | flags.StringVar(&options.debugAddress, "a", "127.0.0.1:8080", "listening address for the debug server to use") 41 | flags.IntVar(&options.entrypoint, "e", -1, "entrypoint to start the CPU") 42 | flags.BoolVar(&options.noGui, "c", false, "console mode, disable GUI") 43 | flags.IntVar(&options.stopAt, "s", -1, "stop execution at address") 44 | flags.BoolVar(&options.tracing, "t", false, "print CPU tracing") 45 | 46 | err := flags.Parse(os.Args[1:]) 47 | args := flags.Args() 48 | if err != nil || len(args) == 0 { 49 | printBanner() 50 | fmt.Printf("usage: nesgoemu [options] \n\n") 51 | flags.PrintDefaults() 52 | os.Exit(1) 53 | } 54 | options.input = args[0] 55 | 56 | return options 57 | } 58 | 59 | func printBanner() { 60 | fmt.Println("[-----------------------------]") 61 | fmt.Println("[ nesgoemu - NES ROM emulator ]") 62 | fmt.Printf("[-----------------------------]\n\n") 63 | fmt.Printf("version: %s\n\n", buildinfo.Version(version, commit, date)) 64 | } 65 | 66 | func emulateFile(options optionFlags) error { 67 | file, err := os.Open(options.input) 68 | if err != nil { 69 | return fmt.Errorf("opening file '%s': %w", options.input, err) 70 | } 71 | 72 | cart, err := cartridge.LoadFile(file) 73 | if err != nil { 74 | return fmt.Errorf("reading file: %w", err) 75 | } 76 | _ = file.Close() 77 | 78 | opts := []nes.Option{ 79 | nes.WithEmulator(), 80 | nes.WithCartridge(cart), 81 | } 82 | 83 | if options.debug { 84 | opts = append(opts, nes.WithDebug(options.debugAddress)) 85 | } 86 | if options.tracing { 87 | opts = append(opts, nes.WithTracing()) 88 | } 89 | if options.entrypoint >= 0 { 90 | opts = append(opts, nes.WithEntrypoint(options.entrypoint)) 91 | } 92 | if options.stopAt >= 0 { 93 | opts = append(opts, nes.WithStopAt(options.stopAt)) 94 | } 95 | if options.noGui { 96 | opts = append(opts, nes.WithDisabledGUI()) 97 | } 98 | 99 | nes.Start(nil, opts...) 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /cmd/nesgoemu/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | version = "dev" 5 | commit = "" 6 | date = "" 7 | ) 8 | -------------------------------------------------------------------------------- /cmd/nesgogg/README.md: -------------------------------------------------------------------------------- 1 | # nesgogg - NES Game Genie decoder/encoder 2 | 3 | ## Installation 4 | 5 | There are different options to install nesgogg, the binary releases do not have any dependencies, 6 | compiling the tool from source code needs to have a recent version of [Golang](https://go.dev/) installed. 7 | 8 | 1. Download and unpack a binary release from [Releases](https://github.com/retroenv/nesgo/releases) 9 | 10 | 2. Install the latest release from source: 11 | 12 | ``` 13 | go install github.com/retroenv/nesgo/cmd/nesgogg@latest 14 | ``` 15 | 16 | 3. Build the current development version: 17 | 18 | ``` 19 | git clone https://github.com/retroenv/nesgo.git 20 | cd nesgo 21 | go build ./cmd/nesgogg 22 | # use the dev version: 23 | ./nesgogg 24 | ``` 25 | 26 | ## Usage 27 | 28 | Decode a Game Genie code: 29 | 30 | ``` 31 | nesgogg 32 | 33 | # For example: 34 | nesgogg PIGOAP 35 | ``` 36 | 37 | Encode a Game Genie code: 38 | 39 | ``` 40 | nesgogg -a 0x94A7 -v 0x02 41 | ``` 42 | 43 | ## Options 44 | 45 | ``` 46 | usage: nesgogg [options] 47 | 48 | -a string 49 | address to patch in decimal or hex with 0x prefix 50 | -c string 51 | compare value in decimal or hex with 0x prefix 52 | -v string 53 | value to write in decimal or hex with 0x prefix 54 | ``` 55 | -------------------------------------------------------------------------------- /cmd/nesgogg/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements a NES Game Genie decoder/encoder 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/retroenv/nesgo/pkg/gamegenie" 11 | "github.com/retroenv/retrogolib/buildinfo" 12 | ) 13 | 14 | type optionFlags struct { 15 | code string 16 | address string 17 | value string 18 | compare string 19 | } 20 | 21 | func main() { 22 | options := readArguments() 23 | 24 | if options.code != "" { 25 | if err := decode(options); err != nil { 26 | fmt.Println(fmt.Errorf("decoding failed: %w", err)) 27 | os.Exit(1) 28 | } 29 | return 30 | } 31 | 32 | if err := encode(options); err != nil { 33 | fmt.Println(fmt.Errorf("encoding failed: %w", err)) 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func readArguments() optionFlags { 39 | flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 40 | options := optionFlags{} 41 | 42 | flags.StringVar(&options.address, "a", "", "address to patch in decimal or hex with 0x prefix") 43 | flags.StringVar(&options.value, "v", "", "value to write in decimal or hex with 0x prefix") 44 | flags.StringVar(&options.compare, "c", "", "compare value in decimal or hex with 0x prefix") 45 | 46 | err := flags.Parse(os.Args[1:]) 47 | // nolint:ifshort 48 | args := flags.Args() 49 | 50 | if err != nil || (len(args) == 0 && options.address == "") { 51 | printBanner() 52 | fmt.Printf("usage: nesgogg [options] \n\n") 53 | flags.PrintDefaults() 54 | os.Exit(1) 55 | } 56 | if len(args) > 0 { 57 | options.code = args[0] 58 | } 59 | 60 | return options 61 | } 62 | 63 | func printBanner() { 64 | fmt.Println("[------------------------------------------]") 65 | fmt.Println("[ nesgogg - NES Game Genie decoder/encoder ]") 66 | fmt.Printf("[------------------------------------------]\n\n") 67 | fmt.Printf("version: %s\n\n", buildinfo.Version(version, commit, date)) 68 | } 69 | 70 | func decode(options optionFlags) error { 71 | patch, err := gamegenie.Decode(options.code) 72 | if err != nil { 73 | return fmt.Errorf("decoding code: %w", err) 74 | } 75 | 76 | fmt.Printf("address: 0x%04X\n", patch.Address) 77 | fmt.Printf("value: 0x%02X\n", patch.Data) 78 | 79 | if patch.HasCompare { 80 | fmt.Printf("compare: 0x%02X\n", patch.Compare) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func encode(options optionFlags) error { 87 | address, err := strconv.ParseUint(options.address, 0, 16) 88 | if err != nil { 89 | return fmt.Errorf("parsing address: %w", err) 90 | } 91 | 92 | value, err := strconv.ParseUint(options.value, 0, 8) 93 | if err != nil { 94 | return fmt.Errorf("parsing value: %w", err) 95 | } 96 | 97 | patch := gamegenie.Patch{ 98 | Address: uint16(address), 99 | Data: byte(value), 100 | } 101 | 102 | if options.compare != "" { 103 | patch.HasCompare = true 104 | compare, err := strconv.ParseUint(options.compare, 0, 8) 105 | if err != nil { 106 | return fmt.Errorf("parsing compare: %w", err) 107 | } 108 | patch.Compare = byte(compare) 109 | } 110 | 111 | code, err := gamegenie.Encode(patch) 112 | if err != nil { 113 | return fmt.Errorf("encoding code: %w", err) 114 | } 115 | fmt.Printf("Game Genie code: %s\n", code) 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /cmd/nesgogg/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | version = "1.0.0" 5 | commit = "" 6 | date = "" 7 | ) 8 | -------------------------------------------------------------------------------- /docs/gui.md: -------------------------------------------------------------------------------- 1 | ## Installation of GUI dependencies 2 | 3 | The integrated OpenGL GUI support is enabled by default. Debugging 4 | code will execute the built-in Emulator and GUI by default. 5 | 6 | To select the GUI mode to use, set the following build flags: 7 | 8 | * `nogui`: disables all GUI modules 9 | * `noopengl` `sdl` enables the SDL GUI 10 | 11 | The following libraries need to be installed, depending on the operating system: 12 | 13 | ### **macOS** 14 | 15 | Xcode or Command Line Tools for Xcode: 16 | 17 | ``` 18 | xcode-select --install 19 | ``` 20 | 21 | ### **Ubuntu/Debian-like** 22 | 23 | For OpenGL support: 24 | 25 | ``` 26 | apt install build-essential libgl1-mesa-dev xorg-dev 27 | ``` 28 | 29 | For SDL support: 30 | 31 | ``` 32 | apt install libsdl2{,-image,-mixer,-ttf,-gfx}-dev 33 | ``` 34 | 35 | ### **CentOS/Fedora-like** 36 | 37 | For OpenGL support: 38 | 39 | ``` 40 | yum install @development-tools libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel libXxf86vm-devel 41 | ``` 42 | 43 | For SDL support: 44 | 45 | ``` 46 | yum install SDL2{,_image,_mixer,_ttf,_gfx}-devel 47 | ``` 48 | 49 | ### Windows 50 | 51 | For SDL support: 52 | 53 | 1. Install [msys2](http://www.msys2.org/) 54 | 2. Start msys2 and execute: 55 | ``` 56 | pacman -S --needed base-devel mingw-w64-i686-toolchain mingw-w64-x86_64-toolchain mingw64/mingw-w64-x86_64-SDL2 57 | ``` 58 | 3. Add `c:\tools\msys64\mingw64\bin\` to the user path environment variable 59 | -------------------------------------------------------------------------------- /examples/blue/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/retroenv/nesgo/pkg/nes" 5 | . "github.com/retroenv/nesgo/pkg/neslib" 6 | ) 7 | 8 | var backgroundColor = NewUint8(COLOR_MEDIUM_BLUE) 9 | 10 | func main() { 11 | Start(resetHandler) 12 | } 13 | 14 | func resetHandler() { 15 | Init() 16 | 17 | WaitSync() // wait for VSYNC 18 | ClearRAM() // clear RAM 19 | WaitSync() // wait for VSYNC (and PPU warmup) 20 | VariableInit() // initialize variables after RAM has been cleared 21 | 22 | StartPPUTransfer(PALETTE_START) 23 | PPUTransfer(backgroundColor) 24 | PPUMask(MASK_BG_CLIP | MASK_SPR_CLIP | MASK_BG | MASK_SPR) 25 | 26 | for { 27 | Nop() // execute instruction to trigger ppu 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/debugprint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/retroenv/nesgo/pkg/nes" 7 | . "github.com/retroenv/nesgo/pkg/neslib" 8 | ) 9 | 10 | var keyState = NewUint8(0) 11 | 12 | func main() { 13 | Start(resetHandler) 14 | } 15 | 16 | func resetHandler() { 17 | for { 18 | ReadJoypad(0) 19 | Cmp(keyState) 20 | if Bne() { 21 | fmt.Printf("key state changed: %d\n", *A) 22 | Sta(keyState) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/retroenv/nesgo 2 | 3 | go 1.19 4 | 5 | require github.com/retroenv/retrogolib v0.0.0-20230722175549-eebe871ac8f3 6 | 7 | require ( 8 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect 9 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect 10 | github.com/veandco/go-sdl2 v0.5.0-alpha.4 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= 2 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 3 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= 4 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 5 | github.com/retroenv/retrogolib v0.0.0-20230722175549-eebe871ac8f3 h1:JKibozfFzoE7RXpgZ0vnIaNepDpbZrtJ7Ppy7WZ+42g= 6 | github.com/retroenv/retrogolib v0.0.0-20230722175549-eebe871ac8f3/go.mod h1:xtIXsyqyacCNdwl6BX9yyZoS0lsH7vSUraBTQPwv8QA= 7 | github.com/veandco/go-sdl2 v0.5.0-alpha.4 h1:+XyPktayFrjYA6n3qUWiQDw6jOVJye27q8gmXvnYAHQ= 8 | github.com/veandco/go-sdl2 v0.5.0-alpha.4/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= 9 | -------------------------------------------------------------------------------- /internal/ast/argument.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ArgumentValue is an instruction argument value. 9 | type ArgumentValue struct { 10 | Value string 11 | } 12 | 13 | // String implement the fmt.Stringer interface. 14 | func (a ArgumentValue) String() string { 15 | return a.Value 16 | } 17 | 18 | // ArgumentParam is an instruction argument parameter reference. 19 | type ArgumentParam struct { 20 | Index int 21 | } 22 | 23 | // String implement the fmt.Stringer interface. 24 | func (a ArgumentParam) String() string { 25 | return fmt.Sprintf("param[%d]", a.Index) 26 | } 27 | 28 | // Arguments defines a list of arguments. 29 | type Arguments []Node 30 | 31 | // String implement the fmt.Stringer interface. 32 | func (a Arguments) String() string { 33 | args := make([]string, 0, len(a)) 34 | for _, arg := range a { 35 | args = append(args, arg.String()) 36 | } 37 | s := strings.Join(args, ",") 38 | return s 39 | } 40 | -------------------------------------------------------------------------------- /internal/ast/constant.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Constant is a constant declaration. 10 | type Constant struct { 11 | Name string 12 | Value int64 13 | 14 | // if set, marks the value replaced from the resolved alias 15 | AliasName string 16 | // package will be first the import name, then replaced by the full package path 17 | AliasPackage string 18 | } 19 | 20 | // NewConstant returns a constant specification. 21 | func NewConstant(expr *Identifier, arg any) (Node, error) { 22 | constant := &Constant{ 23 | Name: expr.Name, 24 | } 25 | 26 | switch val := arg.(type) { 27 | case *Identifier: 28 | sl := strings.Split(val.Name, ".") 29 | constant.AliasPackage = sl[0] 30 | constant.AliasName = sl[1] 31 | 32 | case *Value: 33 | i, err := strconv.ParseInt(val.Value, 0, 64) 34 | if err != nil { 35 | return nil, fmt.Errorf("parsing constant '%s': %w", val.Value, err) 36 | } 37 | constant.Value = i 38 | 39 | default: 40 | return nil, fmt.Errorf("type %T is not supported as constant argument", arg) 41 | } 42 | return constant, nil 43 | } 44 | 45 | // String implement the fmt.Stringer interface. 46 | func (c Constant) String() string { 47 | return fmt.Sprintf("const, %s, %d", c.Name, c.Value) 48 | } 49 | -------------------------------------------------------------------------------- /internal/ast/doc.go: -------------------------------------------------------------------------------- 1 | // Package ast implements utility functions for generating abstract syntax 2 | // trees from Go BNF. 3 | package ast 4 | -------------------------------------------------------------------------------- /internal/ast/error.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "errors" 4 | 5 | // errors. 6 | var ( 7 | ErrIfBranchingEmpty = errors.New("if statement with branch can not have an empty block, goto or break expected") 8 | ErrBreakNotAfterBranching = errors.New("break statement has to be after a branching instruction") 9 | ErrContinueNotAfterBranching = errors.New("continue statement has to be after a branching instruction") 10 | ErrForOnlySimpleConditions = errors.New("only simple conditions are supported in for loops") 11 | ErrForOnlySimplePostExpressions = errors.New("only simple statements are supported in for loop post expressions") 12 | ErrInvalidInitializer = errors.New("variable initialization has to use constructors like NewUint8()") 13 | ErrInvalidVariableName = errors.New("variable can not use reserved name") 14 | ) 15 | -------------------------------------------------------------------------------- /internal/ast/file.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // NewFile initializes file relevant data structures. 9 | func NewFile(pkg *Package, content any) (Node, error) { 10 | f := &File{ 11 | Package: pkg, 12 | } 13 | list, ok := content.(*NodeList) 14 | if !ok { 15 | // file does not contain any code 16 | return f, nil 17 | } 18 | 19 | var err error 20 | for _, node := range list.Nodes { 21 | if l, ok := node.(*NodeList); ok { 22 | for _, n := range l.Nodes { 23 | if err = f.indexNode(n); err != nil { 24 | return nil, err 25 | } 26 | } 27 | continue 28 | } 29 | 30 | if err = f.indexNode(node); err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | return f, nil 36 | } 37 | 38 | // File is a .go file. 39 | type File struct { 40 | Package *Package 41 | Imports []*Import 42 | Constants []*Constant 43 | Variables []*Variable 44 | Functions []*Function 45 | } 46 | 47 | // String implement the fmt.Stringer interface. 48 | func (f *File) String() string { 49 | b := &strings.Builder{} 50 | _, _ = fmt.Fprintln(b, f.Package) 51 | for _, v := range f.Imports { 52 | _, _ = fmt.Fprintln(b, v) 53 | } 54 | for _, v := range f.Constants { 55 | _, _ = fmt.Fprintln(b, v) 56 | } 57 | for _, v := range f.Variables { 58 | _, _ = fmt.Fprintln(b, v) 59 | } 60 | for _, v := range f.Functions { 61 | _, _ = fmt.Fprintln(b, v) 62 | } 63 | return b.String() 64 | } 65 | 66 | func (f *File) indexNode(node Node) error { 67 | switch n := node.(type) { 68 | case *Constant: 69 | f.Constants = append(f.Constants, n) 70 | case *Function: 71 | f.Functions = append(f.Functions, n) 72 | case *Import: 73 | f.Imports = append(f.Imports, n) 74 | case *Variable: 75 | f.Variables = append(f.Variables, n) 76 | default: 77 | return fmt.Errorf("type %T is not supported as top file declaration", node) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/ast/identifier.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | // Identifier is an identifier declaration. 4 | type Identifier struct { 5 | Name string 6 | } 7 | 8 | // NewIdentifier returns a new identifier. 9 | func NewIdentifier(name string) (Node, error) { 10 | return NewIdentifierNoError(name), nil 11 | } 12 | 13 | // NewIdentifierNoError returns a new identifier. 14 | func NewIdentifierNoError(name string) Node { 15 | return &Identifier{ 16 | Name: name, 17 | } 18 | } 19 | 20 | // String implement the fmt.Stringer interface. 21 | func (i Identifier) String() string { 22 | return i.Name 23 | } 24 | -------------------------------------------------------------------------------- /internal/ast/if.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // NewIfStatement returns an if statement, resolved as instructions. 8 | func NewIfStatement(not bool, branch *Branching, block Node) (Node, error) { 9 | list, ok := block.(*NodeList) 10 | if !ok { 11 | return nil, fmt.Errorf("expression type %T is not supported for if statement blocks", block) 12 | } 13 | branch.Not = not 14 | 15 | switch len(list.Nodes) { 16 | case 0: 17 | return nil, ErrIfBranchingEmpty 18 | 19 | case 1: 20 | n := list.Nodes[0] 21 | if b, ok := n.(*Branching); ok { 22 | return handleIfBlockBranching(branch, b) 23 | } 24 | } 25 | 26 | labelIfNot := &Label{Name: "if_not_" + branch.Instruction} 27 | resolved := &NodeList{ 28 | Nodes: []Node{ 29 | branch, 30 | }, 31 | } 32 | 33 | if not { 34 | branch.Destination = labelIfNot 35 | branch.DestinationName = labelIfNot.Name 36 | } else { 37 | labelIf := &Label{Name: "if_" + branch.Instruction} 38 | branch.Destination = labelIf 39 | branch.DestinationName = labelIf.Name 40 | jmp := &Branching{ 41 | Instruction: JmpInstruction, 42 | DestinationName: labelIfNot.Name, 43 | Destination: labelIfNot, 44 | } 45 | 46 | resolved.AddNodes(jmp, labelIf) 47 | } 48 | resolved.AddNodes(list.Nodes...) 49 | resolved.AddNodes(labelIfNot) 50 | return resolved, nil 51 | } 52 | 53 | func handleIfBlockBranching(branch *Branching, block *Branching) (Node, error) { 54 | switch block.Instruction { 55 | case GotoInstruction: 56 | branch.DestinationName = block.DestinationName 57 | return branch, nil 58 | 59 | case breakStatement, continueStatement: 60 | return NewNodeList(branch, block) 61 | 62 | default: 63 | return nil, fmt.Errorf("if block does not supported branching "+ 64 | "instruction '%s'", block.Instruction) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/ast/import.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Import is an import declaration. 9 | type Import struct { 10 | Alias string 11 | Path string 12 | } 13 | 14 | // NewImport an import declaration. 15 | func NewImport(alias string, lib string) (Node, error) { 16 | lib = strings.Trim(lib, `"`) 17 | 18 | if alias != "." { 19 | sl := strings.Split(lib, "/") 20 | name := sl[len(sl)-1] 21 | alias = name 22 | } 23 | 24 | return &Import{ 25 | Alias: alias, 26 | Path: lib, 27 | }, nil 28 | } 29 | 30 | // String implement the fmt.Stringer interface. 31 | func (i Import) String() string { 32 | return fmt.Sprintf("import, %s, %s", i.Alias, i.Path) 33 | } 34 | -------------------------------------------------------------------------------- /internal/ast/list.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // NodeList contains a list of nodes. 9 | type NodeList struct { 10 | Nodes []Node 11 | } 12 | 13 | // NewNodeList returns a statement list. 14 | func NewNodeList(nodes ...any) (Node, error) { 15 | top := &NodeList{} 16 | 17 | for _, node := range nodes { 18 | if node == nil { 19 | continue 20 | } 21 | n, ok := node.(Node) 22 | if !ok { 23 | return nil, fmt.Errorf("unexpected parameter type %T", node) 24 | } 25 | top.AddNodes(n) 26 | } 27 | 28 | return top, nil 29 | } 30 | 31 | // AddNodes adds a node to the node list if the node is not nil or its 32 | // content is empty. 33 | func (t *NodeList) AddNodes(nodes ...Node) { 34 | for _, node := range nodes { 35 | if node == nil { 36 | continue 37 | } 38 | 39 | switch n := node.(type) { 40 | case *NodeList: 41 | t.Nodes = append(t.Nodes, n.Nodes...) 42 | default: 43 | t.Nodes = append(t.Nodes, node) 44 | } 45 | } 46 | } 47 | 48 | // String implement the fmt.Stringer interface. 49 | func (t *NodeList) String() string { 50 | b := &strings.Builder{} 51 | for _, n := range t.Nodes { 52 | _, _ = fmt.Fprintf(b, "%s\n", n) 53 | } 54 | s := b.String() 55 | s = strings.TrimSuffix(s, "\n") 56 | return s 57 | } 58 | 59 | // ExpressionList contains a list of operands and expressions. 60 | type ExpressionList struct { 61 | NodeList 62 | } 63 | 64 | // NewExpressionList returns a expression list. 65 | func NewExpressionList(operand1 any, expression string, 66 | operands ...any) (any, error) { 67 | list, ok := operand1.(*ExpressionList) 68 | if !ok { 69 | list = &ExpressionList{} 70 | node, ok := operand1.(Node) 71 | if !ok { 72 | return nil, fmt.Errorf("unexpected parameter type %T", operand1) 73 | } 74 | list.AddNodes(node) 75 | } 76 | 77 | s := &Statement{ 78 | Op: expression, 79 | } 80 | list.AddNodes(s) 81 | 82 | for _, n := range operands { 83 | switch val := n.(type) { 84 | case *Identifier: 85 | list.AddNodes(val) 86 | case *Value: 87 | list.AddNodes(val) 88 | case *ExpressionList: 89 | if expression == "cast" { 90 | list.AddNodes(&Statement{Op: "("}) 91 | list.AddNodes(val.Nodes...) 92 | list.AddNodes(&Statement{Op: ")"}) 93 | } else { 94 | list.AddNodes(val.Nodes...) 95 | } 96 | default: 97 | return nil, fmt.Errorf("unexpected parameter type %T", n) 98 | } 99 | } 100 | 101 | return list, nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/ast/nes.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | // CPURegisters ... 4 | var CPURegisters = map[string]struct{}{ 5 | "A": {}, 6 | "X": {}, 7 | "Y": {}, 8 | } 9 | -------------------------------------------------------------------------------- /internal/ast/node.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | // Node represents an ast node. 4 | type Node interface { 5 | String() string 6 | } 7 | -------------------------------------------------------------------------------- /internal/ast/package.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "fmt" 4 | 5 | // Package is a package declaration. 6 | type Package struct { 7 | Name string 8 | } 9 | 10 | // NewPackage initializes a package declaration. 11 | func NewPackage(name string) (Node, error) { 12 | return &Package{ 13 | Name: name, 14 | }, nil 15 | } 16 | 17 | // String implement the fmt.Stringer interface. 18 | func (p Package) String() string { 19 | return fmt.Sprintf("package, %s", p.Name) 20 | } 21 | -------------------------------------------------------------------------------- /internal/ast/statement.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Statement is a statement declaration. 9 | type Statement struct { 10 | Op string 11 | Arguments []string 12 | } 13 | 14 | // String implement the fmt.Stringer interface. 15 | func (s Statement) String() string { 16 | return fmt.Sprintf("op, %s, %s", s.Op, strings.Join(s.Arguments, ",")) 17 | } 18 | 19 | // NewAssignStatement returns an assignment statement. 20 | func NewAssignStatement(id *Identifier, val any) (Node, error) { 21 | s := &Statement{ 22 | Op: "=", 23 | } 24 | 25 | switch n := val.(type) { 26 | case *Identifier: 27 | s.Arguments = []string{id.Name, n.Name} 28 | case *Value: 29 | s.Arguments = []string{id.Name, n.Value} 30 | default: 31 | return nil, fmt.Errorf("type %T is not supported for assign statements", val) 32 | } 33 | 34 | return s, nil 35 | } 36 | 37 | // NewReturnStatement returns a return statement. 38 | func NewReturnStatement() (Node, error) { 39 | return newInstruction(ReturnInstruction, nil) 40 | } 41 | -------------------------------------------------------------------------------- /internal/ast/tests/branch_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var branchLabel = []byte(` 8 | a: 9 | `) 10 | var branchLabelIr = ` 11 | label, a 12 | ` 13 | 14 | var branchLabelStatement = []byte(` 15 | a: Dex() 16 | `) 17 | var branchLabelStatementIr = ` 18 | label, a 19 | inst, dex 20 | ` 21 | 22 | var branchLabelBranching = []byte(` 23 | a: 24 | if Bne() { 25 | goto a 26 | } 27 | `) 28 | var branchLabelBranchingIr = ` 29 | label, a 30 | inst, bne, a 31 | ` 32 | 33 | var branchTestCases = []testCase{ 34 | { 35 | "label with branching", 36 | branchLabelBranching, 37 | branchLabelBranchingIr, 38 | "", 39 | }, 40 | { 41 | "simple label", 42 | branchLabel, 43 | branchLabelIr, 44 | "", 45 | }, 46 | { 47 | "label with instruction", 48 | branchLabelStatement, 49 | branchLabelStatementIr, 50 | "", 51 | }, 52 | } 53 | 54 | func TestBranch(t *testing.T) { 55 | for _, test := range branchTestCases { 56 | runTest(t, true, test) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/ast/tests/call_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var callNoParam = []byte(` 8 | Init() 9 | `) 10 | var callNoParamIr = ` 11 | call, Init 12 | ` 13 | 14 | var callSingleParam = []byte(` 15 | StartPPUTransfer(PALETTE_START) 16 | `) 17 | var callSingleParamIr = ` 18 | call, StartPPUTransfer, PALETTE_START 19 | ` 20 | 21 | var callSingleParamExpression = []byte(` 22 | PPUMask(MASK_BG_CLIP | MASK_SPR_CLIP | MASK_BG | MASK_SPR) 23 | `) 24 | var callSingleParamExpressionIr = ` 25 | call, PPUMask, MASK_BG_CLIP 26 | op, |, 27 | MASK_SPR_CLIP 28 | op, |, 29 | MASK_BG 30 | op, |, 31 | MASK_SPR 32 | ` 33 | 34 | var callFmtDebugPrint = []byte(` 35 | Dex() 36 | fmt.Println("debug") 37 | `) 38 | var callFmtDebugPrintIr = ` 39 | inst, dex 40 | ` 41 | 42 | var callTestCases = []testCase{ 43 | { 44 | "fmt debug print call", 45 | callFmtDebugPrint, 46 | callFmtDebugPrintIr, 47 | "", 48 | }, 49 | { 50 | "call without parameters", 51 | callNoParam, 52 | callNoParamIr, 53 | "", 54 | }, 55 | { 56 | "call with 1 parameter", 57 | callSingleParam, 58 | callSingleParamIr, 59 | "", 60 | }, 61 | { 62 | "call with 1 parameter expression", 63 | callSingleParamExpression, 64 | callSingleParamExpressionIr, 65 | "", 66 | }, 67 | } 68 | 69 | func TestCall(t *testing.T) { 70 | for _, test := range callTestCases { 71 | runTest(t, true, test) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/ast/tests/const_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var singleConstValueDec = []byte(` 8 | const bg_color = 1 9 | `) 10 | var singleConstValueDecIr = ` 11 | const, bg_color, 1 12 | ` 13 | 14 | var singleConstValueHex = []byte(` 15 | const bg_color = 0x1a 16 | `) 17 | var singleConstValueHexIr = ` 18 | const, bg_color, 26 19 | ` 20 | 21 | var singleConstValueBin = []byte(` 22 | const bg_color = 0b10000000 23 | `) 24 | var singleConstValueBinIr = ` 25 | const, bg_color, 128 26 | ` 27 | 28 | var constTestCases = []testCase{ 29 | { 30 | "single const declaration with binary value", 31 | singleConstValueBin, 32 | singleConstValueBinIr, 33 | "", 34 | }, 35 | { 36 | "single const declaration with hex value", 37 | singleConstValueHex, 38 | singleConstValueHexIr, 39 | "", 40 | }, 41 | { 42 | "single const declaration with decimal value", 43 | singleConstValueDec, 44 | singleConstValueDecIr, 45 | "", 46 | }, 47 | } 48 | 49 | func TestConst(t *testing.T) { 50 | for _, test := range constTestCases { 51 | runTest(t, false, test) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/ast/tests/function_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var emptyFunction = []byte(` 8 | func test() { 9 | } 10 | `) 11 | var emptyFunctionIr = ` 12 | func, test 13 | ` 14 | 15 | var inlineFunction = []byte(` 16 | func test(_ ...Inline) { 17 | } 18 | `) 19 | var inlineFunctionIr = ` 20 | func, inline, test 21 | ` 22 | 23 | var inlineFunctionSingleParam = []byte(` 24 | func test(data *uint8, _ ...Inline) { 25 | } 26 | `) 27 | var inlineFunctionSingleParamIr = ` 28 | func, inline, (data), test 29 | ` 30 | 31 | var inlineFunctionMultipleParams = []byte(` 32 | func test(data *uint8, value uint8, _ ...Inline) { 33 | } 34 | `) 35 | var inlineFunctionMultipleParamsIr = ` 36 | func, inline, (data, value), test 37 | ` 38 | 39 | var functionWithBody = []byte(` 40 | func test() { 41 | Dex() 42 | } 43 | `) 44 | var functionWithBodyIr = ` 45 | func, test 46 | inst, dex 47 | ` 48 | 49 | var functionWithReturn = []byte(` 50 | func test() { 51 | return 52 | } 53 | `) 54 | var functionWithReturnIr = ` 55 | func, test 56 | inst, rts 57 | ` 58 | 59 | var functionInlineRegisterParam = []byte(` 60 | func test(X, _ ...Inline) { 61 | } 62 | `) 63 | var functionInlineRegisterParamIr = ` 64 | func, inline, test 65 | ` 66 | 67 | var functionRegisterParam = []byte(` 68 | func test(index uint8) { 69 | Ldy(index) 70 | Dey() 71 | } 72 | `) 73 | var functionRegisterParamIr = ` 74 | func, (index), test 75 | inst, dey 76 | ` 77 | 78 | var functionTestCases = []testCase{ 79 | { 80 | "function with register as param", 81 | functionRegisterParam, 82 | functionRegisterParamIr, 83 | "", 84 | }, 85 | { 86 | "inline function with register as param", 87 | functionInlineRegisterParam, 88 | functionInlineRegisterParamIr, 89 | "", 90 | }, 91 | { 92 | "function with return in body", 93 | functionWithReturn, 94 | functionWithReturnIr, 95 | "", 96 | }, 97 | { 98 | "function with body", 99 | functionWithBody, 100 | functionWithBodyIr, 101 | "", 102 | }, 103 | { 104 | "function inlined with single param", 105 | inlineFunctionSingleParam, 106 | inlineFunctionSingleParamIr, 107 | "", 108 | }, 109 | { 110 | "function inlined with multiple params", 111 | inlineFunctionMultipleParams, 112 | inlineFunctionMultipleParamsIr, 113 | "", 114 | }, 115 | { 116 | "function inlined", 117 | inlineFunction, 118 | inlineFunctionIr, 119 | "", 120 | }, 121 | { 122 | "empty function", 123 | emptyFunction, 124 | emptyFunctionIr, 125 | "", 126 | }, 127 | } 128 | 129 | func TestFunction(t *testing.T) { 130 | for _, test := range functionTestCases { 131 | runTest(t, false, test) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /internal/ast/tests/if_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var ifBranchingEmpty = []byte(` 8 | if Bne() { 9 | } 10 | `) 11 | 12 | var ifBranchingGoto = []byte(` 13 | a: 14 | if Bne() { 15 | goto a 16 | } 17 | `) 18 | var ifBranchingGotoIr = ` 19 | label, a 20 | inst, bne, a 21 | ` 22 | 23 | var ifBranchingBreak = []byte(` 24 | a: 25 | if Bne() { 26 | break 27 | } 28 | `) 29 | var ifBranchingBreakIr = ` 30 | label, a 31 | inst, bne 32 | inst, break 33 | ` 34 | 35 | var ifBranchingInstruction = []byte(` 36 | if Bne() { 37 | Dex() 38 | } 39 | `) 40 | var ifBranchingInstructionIr = ` 41 | inst, bne, if_bne 42 | inst, jmp, if_not_bne 43 | label, if_bne 44 | inst, dex 45 | label, if_not_bne 46 | ` 47 | 48 | var ifNotBranchingInstruction = []byte(` 49 | if !Bne() { 50 | Dex() 51 | } 52 | `) 53 | var ifNotBranchingInstructionIr = ` 54 | inst, bne, if_not_bne 55 | inst, dex 56 | label, if_not_bne 57 | ` 58 | 59 | var ifBranchingVarDeclare = []byte(` 60 | if Bne() { 61 | Bne() 62 | } 63 | `) 64 | 65 | var ifTestCases = []testCase{ 66 | { 67 | "if with branching and unsupported branching block instruction", 68 | ifBranchingVarDeclare, 69 | "", 70 | "if block does not supported branching instruction 'bne'", 71 | }, 72 | { 73 | "if with not branching and instruction", 74 | ifNotBranchingInstruction, 75 | ifNotBranchingInstructionIr, 76 | "", 77 | }, 78 | { 79 | "if with branching and instruction", 80 | ifBranchingInstruction, 81 | ifBranchingInstructionIr, 82 | "", 83 | }, 84 | { 85 | "if with branching and break", 86 | ifBranchingBreak, 87 | ifBranchingBreakIr, 88 | "", 89 | }, 90 | { 91 | "if with branching and goto", 92 | ifBranchingGoto, 93 | ifBranchingGotoIr, 94 | "", 95 | }, 96 | { 97 | "if with branching and empty block", 98 | ifBranchingEmpty, 99 | "", 100 | "if statement with branch can not have an empty block, goto or break expected", 101 | }, 102 | } 103 | 104 | func TestIf(t *testing.T) { 105 | for _, test := range ifTestCases { 106 | runTest(t, true, test) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/ast/tests/instruction_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var instructionNoParam = []byte(` 8 | Lda() 9 | `) 10 | var instructionNoParamIr = ` 11 | inst, lda 12 | ` 13 | 14 | var instructionImmediate = []byte(` 15 | Lda(0x12) 16 | `) 17 | var instructionImmediateIr = ` 18 | inst, lda, immediate, 0x12 19 | ` 20 | 21 | var instructionAbsolute = []byte(` 22 | Lda(0x1234) 23 | `) 24 | var instructionAbsoluteIr = ` 25 | inst, lda, absolute, 0x1234 26 | ` 27 | 28 | var instructionAbsoluteConst = []byte(` 29 | Sta(JOYPAD1) 30 | `) 31 | var instructionAbsoluteConstIr = ` 32 | inst, sta, absolute, JOYPAD1 33 | ` 34 | 35 | var instructionAbsoluteCastConst = []byte(` 36 | Sta(Absolute(JOYPAD1)) 37 | `) 38 | var instructionAbsoluteCastConstIr = ` 39 | inst, sta, absolute, JOYPAD1 40 | ` 41 | 42 | var instructionAbsoluteX = []byte(` 43 | Lda(Absolute(0x1234), X) 44 | `) 45 | var instructionAbsoluteXIr = ` 46 | inst, lda, absolute x, 0x1234 47 | ` 48 | 49 | var instructionAbsoluteY = []byte(` 50 | Lda(Absolute(0x1234), Y) 51 | `) 52 | var instructionAbsoluteYIr = ` 53 | inst, lda, absolute y, 0x1234 54 | ` 55 | 56 | var instructionZeroPage = []byte(` 57 | Lda(ZeroPage(0x12)) 58 | `) 59 | var instructionZeroPageIr = ` 60 | inst, lda, zeropage, 0x12 61 | ` 62 | 63 | var instructionZeroPageX = []byte(` 64 | Lda(ZeroPage(0x12), X) 65 | `) 66 | var instructionZeroPageXIr = ` 67 | inst, lda, zeropage x, 0x12 68 | ` 69 | 70 | var instructionTestCases = []testCase{ 71 | { 72 | "instruction with absolute and y param", 73 | instructionAbsoluteY, 74 | instructionAbsoluteYIr, 75 | "", 76 | }, 77 | { 78 | "instruction with absolute and x param", 79 | instructionAbsoluteX, 80 | instructionAbsoluteXIr, 81 | "", 82 | }, 83 | { 84 | "instruction with absolute cast const param", 85 | instructionAbsoluteCastConst, 86 | instructionAbsoluteCastConstIr, 87 | "", 88 | }, 89 | { 90 | "instruction with zeropage and x param", 91 | instructionZeroPageX, 92 | instructionZeroPageXIr, 93 | "", 94 | }, 95 | { 96 | "instruction with zeropage param", 97 | instructionZeroPage, 98 | instructionZeroPageIr, 99 | "", 100 | }, 101 | { 102 | "instruction with absolute param", 103 | instructionAbsolute, 104 | instructionAbsoluteIr, 105 | "", 106 | }, 107 | { 108 | "instruction with absolute const param", 109 | instructionAbsoluteConst, 110 | instructionAbsoluteConstIr, 111 | "", 112 | }, 113 | { 114 | "instruction with immediate param", 115 | instructionImmediate, 116 | instructionImmediateIr, 117 | "", 118 | }, 119 | { 120 | "instruction without parameters", 121 | instructionNoParam, 122 | instructionNoParamIr, 123 | "", 124 | }, 125 | } 126 | 127 | func TestInstruction(t *testing.T) { 128 | for _, test := range instructionTestCases { 129 | runTest(t, true, test) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/ast/tests/test_helper.go: -------------------------------------------------------------------------------- 1 | // Package tests contains AST tests. 2 | package tests 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "testing" 10 | 11 | goccErrors "github.com/retroenv/nesgo/internal/gocc/errors" 12 | "github.com/retroenv/nesgo/internal/gocc/lexer" 13 | "github.com/retroenv/nesgo/internal/gocc/parser" 14 | "github.com/retroenv/retrogolib/assert" 15 | ) 16 | 17 | var header = []byte(`package main 18 | 19 | import . "github.com/retroenv/nesgo/pkg/nes" 20 | 21 | `) 22 | var headerMain = []byte(`func main() { 23 | `) 24 | 25 | var headerIr = `package, main 26 | import, ., github.com/retroenv/nesgo/pkg/nes 27 | ` 28 | var headerMainIr = `func, main 29 | ` 30 | 31 | var footer = []byte(`}`) 32 | 33 | type testCase struct { 34 | name string 35 | input []byte 36 | expectedIr string 37 | expectedError string 38 | } 39 | 40 | func runTest(t *testing.T, useMainFunc bool, test testCase) { 41 | t.Helper() 42 | 43 | buf := bytes.Buffer{} 44 | buf.Write(header) 45 | if useMainFunc { 46 | buf.Write(headerMain) 47 | } 48 | buf.Write(test.input) 49 | if useMainFunc { 50 | buf.Write(footer) 51 | } 52 | 53 | l := lexer.NewLexer(buf.Bytes()) 54 | p := parser.NewParser() 55 | res, err := p.Parse(l) 56 | if test.expectedError == "" { 57 | assert.NoError(t, err, test.name) 58 | } else { 59 | e := &goccErrors.Error{} 60 | if errors.As(err, &e) { 61 | assert.True(t, errors.As(err, &e), test.name) 62 | s := e.String() 63 | assert.True(t, strings.Contains(s, test.expectedError), 64 | fmt.Sprintf("%s:\n%s", test.name, s)) 65 | } else { 66 | assert.Error(t, err, test.expectedError, test.name) 67 | } 68 | return 69 | } 70 | 71 | s := fmt.Sprint(res) 72 | s = strings.TrimPrefix(s, headerIr) 73 | if useMainFunc { 74 | s = strings.TrimPrefix(s, headerMainIr) 75 | } 76 | 77 | s = strings.TrimSpace(s) 78 | expectedIr := strings.TrimSpace(test.expectedIr) 79 | assert.Equal(t, expectedIr, s, test.name) 80 | } 81 | -------------------------------------------------------------------------------- /internal/ast/tests/var_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/internal/ast" 7 | ) 8 | 9 | var singleVarType = []byte(` 10 | var i int8 11 | `) 12 | var singleVarTypeIr = ` 13 | var, i, int8 14 | ` 15 | 16 | var multipleVarsType = []byte(` 17 | var i, j int8 18 | `) 19 | var multipleVarsTypeIr = ` 20 | var, i, int8 21 | var, j, int8 22 | ` 23 | 24 | var multipleVarsTypeInitializer = []byte(` 25 | var i, j = NewUint8(2) 26 | `) 27 | var multipleVarsTypeInitializerIr = ` 28 | var, i, uint8, 2 29 | var, j, uint8, 2 30 | ` 31 | 32 | var singleVarInvalidType = []byte(` 33 | var i int128 34 | `) 35 | var singleVarTypeInvalidInitializer = []byte(` 36 | var i int8 = 1 37 | `) 38 | var singleVarTypeInvalidName = []byte(` 39 | var x int8 40 | `) 41 | 42 | var singleVarTypeValidInitializer = []byte(` 43 | var i = NewUint8(1) 44 | `) 45 | var singleVarTypeValidInitializerIr = ` 46 | var, i, uint8, 1 47 | ` 48 | 49 | var varGroupSingleType = []byte(` 50 | var ( 51 | i int8 52 | ) 53 | `) 54 | var varGroupSingleTypeIr = ` 55 | var, i, int8 56 | ` 57 | 58 | var varGroupMultipleTypes = []byte(` 59 | var ( 60 | i int8 61 | j int8 62 | ) 63 | `) 64 | var varGroupMultipleTypesIr = ` 65 | var, i, int8 66 | var, j, int8 67 | ` 68 | 69 | var varTestCases = []testCase{ 70 | { 71 | "var list declaration with type and valid initializer", 72 | multipleVarsTypeInitializer, 73 | multipleVarsTypeInitializerIr, 74 | "", 75 | }, 76 | { 77 | "var list declaration with type", 78 | multipleVarsType, 79 | multipleVarsTypeIr, 80 | "", 81 | }, 82 | { 83 | "var group declaration with multiple types", 84 | varGroupMultipleTypes, 85 | varGroupMultipleTypesIr, 86 | "", 87 | }, 88 | { 89 | "var group declaration with single type", 90 | varGroupSingleType, 91 | varGroupSingleTypeIr, 92 | "", 93 | }, 94 | { 95 | "single var declaration with reserve name", 96 | singleVarTypeInvalidName, 97 | "", 98 | ast.ErrInvalidVariableName.Error(), 99 | }, 100 | { 101 | "single var declaration with type and invalid initializer", 102 | singleVarTypeInvalidInitializer, 103 | "", 104 | ast.ErrInvalidInitializer.Error(), 105 | }, 106 | { 107 | "single var declaration with type and valid initializer", 108 | singleVarTypeValidInitializer, 109 | singleVarTypeValidInitializerIr, 110 | "", 111 | }, 112 | { 113 | "single var declaration with invalid type", 114 | singleVarInvalidType, 115 | "", 116 | "Expected one of:", 117 | }, 118 | { 119 | "single var declaration with type", 120 | singleVarType, 121 | singleVarTypeIr, 122 | "", 123 | }, 124 | } 125 | 126 | func TestVar(t *testing.T) { 127 | for _, test := range varTestCases { 128 | runTest(t, false, test) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/ast/type.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | var typeInitializer = map[string]string{ 4 | "NewInt8": "int8", 5 | "NewUint8": "uint8", 6 | "NewUint16": "uint16", 7 | } 8 | 9 | // Type is a type declaration. 10 | type Type struct { 11 | Name string 12 | InitializerUsed bool 13 | } 14 | 15 | // String implement the fmt.Stringer interface. 16 | func (t Type) String() string { 17 | return t.Name 18 | } 19 | 20 | // NewType returns a type. 21 | func NewType(name string) (Node, error) { 22 | t := &Type{} 23 | var ok bool 24 | t.Name, ok = typeInitializer[name] 25 | if ok { 26 | t.InitializerUsed = true 27 | } else { 28 | t.Name = name 29 | } 30 | return t, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/ast/value.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | // Value is a value definition. 4 | type Value struct { 5 | Value string 6 | } 7 | 8 | // NewValue returns a value. 9 | func NewValue(val string) (Node, error) { 10 | return &Value{ 11 | Value: val, 12 | }, nil 13 | } 14 | 15 | // String implement the fmt.Stringer interface. 16 | func (v Value) String() string { 17 | return v.Value 18 | } 19 | -------------------------------------------------------------------------------- /internal/ast/variable.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "fmt" 4 | 5 | var reservedNames = map[string]struct{}{ 6 | "x": {}, 7 | "X": {}, 8 | "y": {}, 9 | "Y": {}, 10 | } 11 | 12 | // Variable is a variable declaration. 13 | type Variable struct { 14 | Name string 15 | Type string 16 | Value string 17 | } 18 | 19 | // NewVariable creates a variable specification. 20 | func NewVariable(expr Node, t *Type, value any) (Node, error) { 21 | v := &Variable{ 22 | Type: t.Name, 23 | } 24 | 25 | switch val := value.(type) { 26 | case nil: 27 | 28 | case *Identifier: 29 | v.Value = val.Name 30 | 31 | case *Value: 32 | if !t.InitializerUsed { 33 | return nil, ErrInvalidInitializer 34 | } 35 | v.Value = val.Value 36 | 37 | default: 38 | return nil, fmt.Errorf("unexpected intitializer type %T", value) 39 | } 40 | 41 | switch e := expr.(type) { 42 | case *Identifier: 43 | if _, ok := reservedNames[e.Name]; ok { 44 | return nil, ErrInvalidVariableName 45 | } 46 | v.Name = e.Name 47 | return v, nil 48 | 49 | case *NodeList: 50 | vars := &NodeList{} 51 | for _, node := range e.Nodes { 52 | id, ok := node.(*Identifier) 53 | if !ok { 54 | return nil, fmt.Errorf("unexpected node list expression type %T", value) 55 | } 56 | 57 | newVar := &Variable{ 58 | Name: id.Name, 59 | Type: t.Name, 60 | Value: v.Value, 61 | } 62 | vars.Nodes = append(vars.Nodes, newVar) 63 | } 64 | return vars, nil 65 | 66 | default: 67 | return nil, fmt.Errorf("unexpected expression type %T", value) 68 | } 69 | } 70 | 71 | // String implement the fmt.Stringer interface. 72 | func (v Variable) String() string { 73 | if v.Value == "" { 74 | return fmt.Sprintf("var, %s, %s", v.Name, v.Type) 75 | } 76 | return fmt.Sprintf("var, %s, %s, %s", v.Name, v.Type, v.Value) 77 | } 78 | -------------------------------------------------------------------------------- /internal/compiler/config.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | // Config contains the compiler configuration. 4 | type Config struct { 5 | // DisableComments does not output any comments. 6 | DisableComments bool 7 | } 8 | 9 | func (c Config) validate() error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /internal/compiler/constant.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/retroenv/nesgo/internal/ast" 9 | ) 10 | 11 | func (p *Package) addConstants(file *File) error { 12 | for _, con := range file.Constants { 13 | name := con.Name 14 | if _, ok := p.constants[name]; ok { 15 | return fmt.Errorf("constant '%s' is defined multiple times", name) 16 | } 17 | 18 | if con.AliasName != "" { 19 | imp := file.importLookup[con.AliasPackage] 20 | con.AliasPackage = imp.Path 21 | } 22 | 23 | p.constants[name] = con 24 | } 25 | return nil 26 | } 27 | 28 | func (p *Package) findConstant(packages map[string]*Package, 29 | caller, constant string) (*ast.Constant, error) { 30 | sl := strings.Split(constant, ".") 31 | if len(sl) > 1 { 32 | // TODO support non dot imported functions 33 | return nil, errors.New("non dot imports from external packages are not support yet") 34 | } 35 | 36 | if con, ok := p.constants[constant]; ok { 37 | return con, nil 38 | } 39 | 40 | imports := p.functionFile[caller].Imports 41 | for _, imp := range imports { 42 | impPack := packages[imp.Path] 43 | if impPack == nil { 44 | continue 45 | } 46 | if con, ok := impPack.constants[constant]; ok { 47 | return con, nil 48 | } 49 | } 50 | 51 | return nil, fmt.Errorf("constant '%s' can not be found", constant) 52 | } 53 | 54 | func (p *Package) resolveConstantAlias(packages map[string]*Package, 55 | aliasCon *ast.Constant) error { 56 | pack, ok := packages[aliasCon.AliasPackage] 57 | if !ok { 58 | return fmt.Errorf("constant alias package '%s' can not be found", aliasCon.AliasPackage) 59 | } 60 | 61 | con, ok := pack.constants[aliasCon.AliasName] 62 | if !ok { 63 | return fmt.Errorf("constant alias '%s' in package '%s' can not be found", 64 | aliasCon.AliasName, aliasCon.AliasPackage) 65 | } 66 | 67 | aliasCon.Value = con.Value 68 | aliasCon.AliasName = "" 69 | aliasCon.AliasPackage = "" 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/compiler/file.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/retroenv/nesgo/internal/ast" 11 | "github.com/retroenv/nesgo/internal/gocc/lexer" 12 | "github.com/retroenv/nesgo/internal/gocc/parser" 13 | ) 14 | 15 | // File is a .go file. 16 | type File struct { 17 | Path string 18 | IsIgnored bool 19 | Package string 20 | 21 | Imports []*ast.Import 22 | Constants []*ast.Constant 23 | Variables []*ast.Variable 24 | Functions []*ast.Function 25 | 26 | importLookup map[string]*ast.Import 27 | } 28 | 29 | // parseFile parses the file using the lexer and parser and returns an AST 30 | // presentation of the file. 31 | func parseFile(fileName string, data []byte) (*File, error) { 32 | ignored, err := isFileIgnored(data) 33 | if err != nil { 34 | return nil, err 35 | } 36 | f := &File{ 37 | Path: fileName, 38 | importLookup: map[string]*ast.Import{}, 39 | } 40 | if ignored { 41 | f.IsIgnored = true 42 | return f, nil 43 | } 44 | 45 | l := lexer.NewLexer(data) 46 | p := parser.NewParser() 47 | 48 | res, err := p.Parse(l) 49 | if err != nil { 50 | return nil, fmt.Errorf("parsing file: %w", err) 51 | } 52 | 53 | astFile, ok := res.(*ast.File) 54 | if !ok { 55 | return nil, fmt.Errorf("unexpected file parse type %T", res) 56 | } 57 | f.Package = astFile.Package.Name 58 | f.Imports = astFile.Imports 59 | f.Constants = astFile.Constants 60 | f.Variables = astFile.Variables 61 | f.Functions = astFile.Functions 62 | 63 | for _, imp := range astFile.Imports { 64 | f.importLookup[imp.Alias] = imp 65 | } 66 | 67 | return f, nil 68 | } 69 | 70 | // isFileIgnored returns whether the file is to be ignored by the 71 | // compiler due to a set built flag. 72 | func isFileIgnored(data []byte) (bool, error) { 73 | reader := bytes.NewReader(data) 74 | buf := bufio.NewReader(reader) 75 | 76 | for i := 0; i < 2; i++ { 77 | b, err := buf.ReadBytes('\n') 78 | if err != nil { 79 | if err == io.EOF { 80 | return false, nil 81 | } 82 | return false, fmt.Errorf("reading line: %w", err) 83 | } 84 | b = bytes.TrimSpace(b) 85 | 86 | var tags []string 87 | if bytes.HasPrefix(b, buildHeader1) { 88 | b = b[len(buildHeader1):] 89 | tags = strings.Split(string(b), ",") 90 | } else { 91 | if !bytes.HasPrefix(b, buildHeader2) { 92 | continue 93 | } 94 | b = b[len(buildHeader2):] 95 | tags = strings.Split(string(b), "&&") 96 | } 97 | 98 | for _, tag := range tags { 99 | if strings.TrimSpace(tag) == nesGoIgnoreTag { 100 | return true, nil 101 | } 102 | } 103 | } 104 | 105 | return false, nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/compiler/file_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/retrogolib/assert" 7 | ) 8 | 9 | var fileIgnoredTestCases = []struct { 10 | name string 11 | input string 12 | expectedResult bool 13 | expectedError string 14 | }{ 15 | { 16 | "old single tag", 17 | `// +build !nesgo`, 18 | true, 19 | "", 20 | }, 21 | { 22 | "old multiple tags", 23 | `// +build !nesgo,!nogui`, 24 | true, 25 | "", 26 | }, 27 | { 28 | "new single tag", 29 | `//go:build !nesgo`, 30 | true, 31 | "", 32 | }, 33 | { 34 | "new multiple tags", 35 | `//go:build !nesgo && !nogui`, 36 | true, 37 | "", 38 | }, 39 | { 40 | "no build tag match", 41 | `//go:build macos`, 42 | false, 43 | "", 44 | }, 45 | { 46 | "no build header", 47 | `package nes`, 48 | false, 49 | "", 50 | }, 51 | } 52 | 53 | func TestIsFileIgnored(t *testing.T) { 54 | for _, test := range fileIgnoredTestCases { 55 | result, err := isFileIgnored([]byte(test.input + "\n")) 56 | if test.expectedError == "" { 57 | assert.NoError(t, err, test.name) 58 | } else { 59 | assert.Error(t, err, test.expectedError, test.name) 60 | return 61 | } 62 | assert.Equal(t, test.expectedResult, result) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/compiler/function_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import "testing" 4 | 5 | var functionRegisterParam = []byte(` 6 | func test() { 7 | Dex() 8 | testParam(0) 9 | } 10 | 11 | func testParam(index uint8) { 12 | Ldy(index) 13 | Dey() 14 | } 15 | `) 16 | var functionRegisterParamAssembly = ` 17 | .proc test 18 | dex 19 | ldy #$00 20 | jsr testParam 21 | rti 22 | .endproc 23 | 24 | .proc testParam 25 | dey 26 | rts 27 | .endproc 28 | ` 29 | 30 | var instructionRegisterParam = []byte(` 31 | func test() { 32 | Sta(0x200, X) 33 | } 34 | `) 35 | var instructionRegisterParamAssembly = ` 36 | .proc test 37 | sta $0200, X 38 | rti 39 | .endproc 40 | ` 41 | 42 | var functionTestCases = []testCase{ 43 | { 44 | "instruction with register as index param", 45 | instructionRegisterParam, 46 | instructionRegisterParamAssembly, 47 | }, 48 | { 49 | "function with register as param", 50 | functionRegisterParam, 51 | functionRegisterParamAssembly, 52 | }, 53 | } 54 | 55 | func TestFunction(t *testing.T) { 56 | for _, test := range functionTestCases { 57 | runCompileTest(t, test) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/compiler/import.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | ) 13 | 14 | var errNoModules = errors.New("no valid go.mod found") 15 | 16 | // currentPackage gets the current package based on the content of a go.mod 17 | // file in the given directory or any of its parent directories. 18 | // It returns the package name and the directory containing the go.mod file. 19 | func currentPackage() (pack string, directory string, err error) { 20 | parent, err := os.Getwd() 21 | if err != nil { 22 | // A nonexistent working directory can't be in a module. 23 | return "", "", fmt.Errorf("getting working directory: %w", err) 24 | } 25 | 26 | var info os.FileInfo 27 | for { 28 | info, err = os.Stat(filepath.Join(parent, "go.mod")) 29 | if err == nil && !info.IsDir() { 30 | break 31 | } 32 | d := filepath.Dir(parent) 33 | if len(d) >= len(parent) { 34 | return "", "", errNoModules // reached top of file system, no go.mod 35 | } 36 | parent = d 37 | } 38 | 39 | full := path.Join(parent, info.Name()) 40 | data, err := os.ReadFile(full) 41 | if err != nil { 42 | return "", "", fmt.Errorf("reading file '%s': %w", full, err) 43 | } 44 | 45 | reader := bytes.NewReader(data) 46 | buf := bufio.NewReader(reader) 47 | 48 | moduleText := []byte("module ") 49 | for { 50 | line, _, err := buf.ReadLine() 51 | if err != nil { 52 | if err == io.EOF { 53 | break 54 | } 55 | return "", "", err 56 | } 57 | if !bytes.HasPrefix(line, moduleText) { 58 | continue 59 | } 60 | line = bytes.TrimPrefix(line, moduleText) 61 | list := bytes.Split(line, []byte("\n")) 62 | if len(list) > 0 { 63 | return string(list[0]), parent, nil 64 | } 65 | } 66 | return "", "", errNoModules 67 | } 68 | -------------------------------------------------------------------------------- /internal/compiler/import_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCurrentPackage(t *testing.T) { 8 | if _, _, err := currentPackage(); err != nil { 9 | t.Error(err) 10 | return 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/compiler/label_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/internal/ast" 7 | "github.com/retroenv/retrogolib/assert" 8 | ) 9 | 10 | func TestFixLabelNameCollisions(t *testing.T) { 11 | fun := &Function{ 12 | Labels: map[string]*ast.Label{ 13 | "test": {Name: "test"}, 14 | }, 15 | } 16 | 17 | label := &ast.Label{Name: "test"} 18 | jmp1 := &ast.Branching{Instruction: "jmp", DestinationName: "test", Destination: label} 19 | jmp2 := &ast.Branching{Instruction: "bne", DestinationName: "test", Destination: label} 20 | 21 | nodes := []ast.Node{label, jmp1, jmp2} 22 | fixed := fixLabelNameCollisions(fun, nodes) 23 | 24 | assert.True(t, len(nodes) == len(fixed)) 25 | 26 | // make sure that the original objects have not been modified 27 | assert.Equal(t, "test", label.Name) 28 | assert.Equal(t, "test", jmp1.DestinationName) 29 | assert.Equal(t, "test", jmp2.DestinationName) 30 | 31 | newLabel, ok := fixed[0].(*ast.Label) 32 | assert.True(t, ok) 33 | newJmp1, ok := fixed[1].(*ast.Branching) 34 | assert.True(t, ok) 35 | newJmp2, ok := fixed[2].(*ast.Branching) 36 | assert.True(t, ok) 37 | 38 | assert.False(t, newLabel.Name == label.Name) 39 | assert.Equal(t, newLabel.Name, newJmp1.DestinationName) 40 | assert.True(t, newLabel == newJmp1.Destination) 41 | assert.Equal(t, newLabel.Name, newJmp2.DestinationName) 42 | assert.True(t, newLabel == newJmp2.Destination) 43 | } 44 | -------------------------------------------------------------------------------- /internal/compiler/test_helper.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/retroenv/retrogolib/assert" 9 | ) 10 | 11 | var testFileHeader = []byte(`package main 12 | 13 | import . "github.com/retroenv/nesgo/pkg/nes" 14 | 15 | func main() { 16 | Start(test) 17 | } 18 | `) 19 | 20 | type testCase struct { 21 | name string 22 | input []byte 23 | expectedAssembly string 24 | } 25 | 26 | func runCompileTest(t *testing.T, test testCase) { 27 | t.Helper() 28 | 29 | cfg := &Config{ 30 | DisableComments: true, 31 | } 32 | c, err := New(cfg) 33 | assert.NoError(t, err) 34 | 35 | buf := bytes.Buffer{} 36 | buf.Write(testFileHeader) 37 | buf.Write(test.input) 38 | 39 | assert.NoError(t, c.Parse("main.go", buf.Bytes())) 40 | 41 | assert.NoError(t, c.optimize()) 42 | 43 | for _, fun := range c.functions { 44 | assert.NoError(t, c.outputFunction(fun)) 45 | } 46 | 47 | s := strings.Join(c.output, "") 48 | s = strings.TrimSpace(s) 49 | expected := strings.TrimSpace(test.expectedAssembly) 50 | assert.Equal(t, expected, s, test.name) 51 | } 52 | -------------------------------------------------------------------------------- /internal/compiler/variable.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/retroenv/nesgo/internal/ast" 9 | ) 10 | 11 | func (p *Package) addVariables(variables []*ast.Variable) error { 12 | for _, v := range variables { 13 | name := v.Name 14 | if _, ok := p.variables[name]; ok { 15 | return fmt.Errorf("variable '%s' is defined multiple times", name) 16 | } 17 | p.variables[name] = v 18 | } 19 | return nil 20 | } 21 | 22 | func (p *Package) findVariable(packages map[string]*Package, 23 | caller, variable string) (*ast.Variable, error) { 24 | sl := strings.Split(variable, ".") 25 | if len(sl) > 1 { 26 | // TODO support non dot imported functions 27 | return nil, errors.New("non dot imports from external packages are not support yet") 28 | } 29 | 30 | if v, ok := p.variables[variable]; ok { 31 | return v, nil 32 | } 33 | 34 | imports := p.functionFile[caller].Imports 35 | for _, imp := range imports { 36 | impPack := packages[imp.Path] 37 | if v, ok := impPack.variables[variable]; ok { 38 | return v, nil 39 | } 40 | } 41 | 42 | return nil, fmt.Errorf("variable '%s' can not be found", variable) 43 | } 44 | 45 | func (c *Compiler) addVariable(variable *ast.Variable) { 46 | c.variables[variable.Name] = variable 47 | if variable.Value != "" { 48 | c.variablesInitialized[variable.Name] = variable 49 | } 50 | } 51 | 52 | func (c *Compiler) createVariableInitializations(mainPackage *Package) error { 53 | resetHandler := mainPackage.functions[c.resetHandler] 54 | 55 | fun, userCalled := c.functionsAdded[VarInitFunctionFullName] 56 | if !userCalled { 57 | call := &ast.Call{Function: VarInitFunctionName} 58 | resetHandler.Body.Nodes = append([]ast.Node{call}, resetHandler.Body.Nodes...) 59 | 60 | fun = &Function{ 61 | Definition: &ast.FunctionDefinition{ 62 | Inline: false, 63 | Name: VarInitFunctionName, 64 | }, 65 | Body: &ast.NodeList{}, 66 | } 67 | } 68 | 69 | for _, variable := range c.variablesInitialized { 70 | value := variable.Value 71 | if con, err := mainPackage.findConstant(c.packages, c.resetHandler, value); err == nil { 72 | value = fmt.Sprint(con.Value) 73 | } 74 | 75 | load := &ast.Instruction{ 76 | Name: "lda", 77 | Arguments: ast.Arguments{ 78 | &ast.ArgumentValue{Value: value}}, 79 | } 80 | store := &ast.Instruction{ 81 | Name: "sta", 82 | Arguments: ast.Arguments{ 83 | &ast.ArgumentValue{Value: variable.Name}}, 84 | } 85 | fun.Body.AddNodes(load, store) 86 | } 87 | 88 | ret, _ := ast.NewReturnStatement() 89 | fun.Body.AddNodes(ret) 90 | 91 | if !userCalled { 92 | return c.addFunction(VarInitFunctionName, fun) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/gocc/errors/errors.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package errors 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | 11 | "github.com/retroenv/nesgo/internal/gocc/token" 12 | ) 13 | 14 | type ErrorSymbol interface { 15 | } 16 | 17 | type Error struct { 18 | Err error 19 | ErrorToken *token.Token 20 | ErrorSymbols []ErrorSymbol 21 | ExpectedTokens []string 22 | StackTop int 23 | } 24 | 25 | func (e *Error) String() string { 26 | w := new(strings.Builder) 27 | if e.Err != nil { 28 | fmt.Fprintln(w, "Error ", e.Err) 29 | } else { 30 | fmt.Fprintln(w, "Error") 31 | } 32 | fmt.Fprintf(w, "Token: type=%d, lit=%s\n", e.ErrorToken.Type, e.ErrorToken.Lit) 33 | fmt.Fprintf(w, "Pos: offset=%d, line=%d, column=%d\n", e.ErrorToken.Pos.Offset, e.ErrorToken.Pos.Line, e.ErrorToken.Pos.Column) 34 | fmt.Fprint(w, "Expected one of: ") 35 | for _, sym := range e.ExpectedTokens { 36 | fmt.Fprint(w, string(sym), " ") 37 | } 38 | fmt.Fprintln(w, "ErrorSymbol:") 39 | for _, sym := range e.ErrorSymbols { 40 | fmt.Fprintf(w, "%v\n", sym) 41 | } 42 | 43 | return w.String() 44 | } 45 | 46 | func DescribeExpected(tokens []string) string { 47 | switch len(tokens) { 48 | case 0: 49 | return "unexpected additional tokens" 50 | 51 | case 1: 52 | return "expected " + tokens[0] 53 | 54 | case 2: 55 | return "expected either " + tokens[0] + " or " + tokens[1] 56 | 57 | case 3: 58 | // Oxford-comma rules require more than 3 items in a list for the 59 | // comma to appear before the 'or' 60 | return fmt.Sprintf("expected one of %s, %s or %s", tokens[0], tokens[1], tokens[2]) 61 | 62 | default: 63 | // Oxford-comma separated alternatives list. 64 | tokens = append(tokens[:len(tokens)-1], "or "+tokens[len(tokens)-1]) 65 | return "expected one of " + strings.Join(tokens, ", ") 66 | } 67 | } 68 | 69 | func DescribeToken(tok *token.Token) string { 70 | switch tok.Type { 71 | case token.INVALID: 72 | return fmt.Sprintf("unknown/invalid token %q", tok.Lit) 73 | case token.EOF: 74 | return "end-of-file" 75 | default: 76 | return fmt.Sprintf("%q", tok.Lit) 77 | } 78 | } 79 | 80 | func (e *Error) Error() string { 81 | // identify the line and column of the error in 'gnu' style so it can be understood 82 | // by editors and IDEs; user will need to prefix it with a filename. 83 | text := fmt.Sprintf("%d:%d: error: ", e.ErrorToken.Pos.Line, e.ErrorToken.Pos.Column) 84 | 85 | // See if the error token can provide us with the filename. 86 | switch src := e.ErrorToken.Pos.Context.(type) { 87 | case token.Sourcer: 88 | text = src.Source() + ":" + text 89 | } 90 | 91 | if e.Err != nil { 92 | // Custom error specified, e.g. by << nil, errors.New("missing newline") >> 93 | text += e.Err.Error() 94 | } else { 95 | tokens := make([]string, len(e.ExpectedTokens)) 96 | for idx, token := range e.ExpectedTokens { 97 | if !unicode.IsLetter(rune(token[0])) { 98 | token = strconv.Quote(token) 99 | } 100 | tokens[idx] = token 101 | } 102 | text += DescribeExpected(tokens) 103 | actual := DescribeToken(e.ErrorToken) 104 | text += fmt.Sprintf("; got: %s", actual) 105 | } 106 | 107 | return text 108 | } 109 | -------------------------------------------------------------------------------- /internal/gocc/parser/action.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package parser 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | type action interface { 10 | act() 11 | String() string 12 | } 13 | 14 | type ( 15 | accept bool 16 | shift int // value is next state index 17 | reduce int // value is production index 18 | ) 19 | 20 | func (this accept) act() {} 21 | func (this shift) act() {} 22 | func (this reduce) act() {} 23 | 24 | func (this accept) Equal(that action) bool { 25 | if _, ok := that.(accept); ok { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | func (this reduce) Equal(that action) bool { 32 | that1, ok := that.(reduce) 33 | if !ok { 34 | return false 35 | } 36 | return this == that1 37 | } 38 | 39 | func (this shift) Equal(that action) bool { 40 | that1, ok := that.(shift) 41 | if !ok { 42 | return false 43 | } 44 | return this == that1 45 | } 46 | 47 | func (this accept) String() string { return "accept(0)" } 48 | func (this shift) String() string { return fmt.Sprintf("shift:%d", this) } 49 | func (this reduce) String() string { 50 | return fmt.Sprintf("reduce:%d(%s)", this, productionsTable[this].String) 51 | } 52 | -------------------------------------------------------------------------------- /internal/gocc/parser/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package parser 4 | 5 | // Parser-specific user-defined and entirely-optional context, 6 | // accessible as '$Context' in SDT actions. 7 | type Context interface{} 8 | -------------------------------------------------------------------------------- /internal/gocc/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | var _ = []byte(`package main 4 | 5 | import . "github.com/retroenv/nesgo/pkg/nes" 6 | 7 | const PPU_CTRL = 0x2000 8 | 9 | type inline any 10 | 11 | const ( 12 | A = 0x1000 13 | B = 0b1000 // comment 14 | ) 15 | 16 | var i int8 17 | 18 | var ( 19 | C int8 20 | D int8 // comment 21 | ) 22 | 23 | 24 | type inter any 25 | 26 | // comment1 27 | 28 | /* comment2 */ 29 | 30 | func test() { 31 | i = 1 32 | Inx() 33 | } 34 | 35 | func empty() { 36 | } 37 | 38 | func TestInline(... inline) { 39 | test() 40 | x: 41 | } 42 | 43 | func main() { 44 | Init() // comment1 45 | Lda(1) /* comment2 */ 46 | Sta(PPU_ADDR, 2) 47 | 48 | a: 49 | if Bne() { 50 | goto a 51 | } 52 | return 53 | } 54 | `) 55 | 56 | var _ = `package, main 57 | import, ., github.com/retroenv/nesgo/pkg/nes 58 | const, PPU_CTRL, 0x2000 59 | const, A, 0x1000 60 | const, B, 0b1000 61 | var, i, int8 62 | var, C, int8 63 | var, D, int8 64 | func, test 65 | op, =, i,1 66 | inst, inx, 67 | func, empty 68 | 69 | func, inline, TestInline 70 | call, test 71 | label, x 72 | func, main 73 | call, Init 74 | inst, lda, 1 75 | inst, sta, PPU_ADDR,2 76 | label, a 77 | inst, bne, a 78 | inst, rts, 79 | ` 80 | -------------------------------------------------------------------------------- /internal/gocc/token/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package token 4 | 5 | // Context allows user-defined data to be associated with the 6 | // lexer/scanner to be associated with each token that lexer 7 | // produces. 8 | type Context interface{} 9 | 10 | // Sourcer is a Context interface which presents a Source() method 11 | // identifying e.g the filename for the current code. 12 | type Sourcer interface { 13 | Source() string 14 | } 15 | -------------------------------------------------------------------------------- /internal/gocc/util/litconv.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package util 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // Interface. 13 | 14 | // RuneValue will convert the literal value of a scanned token to a rune. 15 | func RuneValue(lit []byte) rune { 16 | if lit[1] == '\\' { 17 | return escapeCharVal(lit) 18 | } 19 | r, size := utf8.DecodeRune(lit[1:]) 20 | if size != len(lit)-2 { 21 | panic(fmt.Sprintf("Error decoding rune. Lit: %s, rune: %d, size%d\n", lit, r, size)) 22 | } 23 | return r 24 | } 25 | 26 | // UintValue will attempt to parse a byte-slice as a signed base-10 64-bit integer. 27 | func IntValue(lit []byte) (int64, error) { 28 | return strconv.ParseInt(string(lit), 10, 64) 29 | } 30 | 31 | // UintValue will attempt to parse a byte-slice as an unsigned base-10 64-bit integer. 32 | func UintValue(lit []byte) (uint64, error) { 33 | return strconv.ParseUint(string(lit), 10, 64) 34 | } 35 | 36 | // Helpers. 37 | func escapeCharVal(lit []byte) rune { 38 | var i, base, max uint32 39 | offset := 2 40 | switch lit[offset] { 41 | case 'a': 42 | return '\a' 43 | case 'b': 44 | return '\b' 45 | case 'f': 46 | return '\f' 47 | case 'n': 48 | return '\n' 49 | case 'r': 50 | return '\r' 51 | case 't': 52 | return '\t' 53 | case 'v': 54 | return '\v' 55 | case '\\': 56 | return '\\' 57 | case '\'': 58 | return '\'' 59 | case '0', '1', '2', '3', '4', '5', '6', '7': 60 | i, base, max = 3, 8, 255 61 | case 'x': 62 | i, base, max = 2, 16, 255 63 | offset++ 64 | case 'u': 65 | i, base, max = 4, 16, unicode.MaxRune 66 | offset++ 67 | case 'U': 68 | i, base, max = 8, 16, unicode.MaxRune 69 | offset++ 70 | default: 71 | panic(fmt.Sprintf("Error decoding character literal: %s\n", lit)) 72 | } 73 | 74 | var x uint32 75 | for ; i > 0 && offset < len(lit)-1; i-- { 76 | ch, size := utf8.DecodeRune(lit[offset:]) 77 | offset += size 78 | d := uint32(digitVal(ch)) 79 | if d >= base { 80 | panic(fmt.Sprintf("charVal(%s): illegal character (%c) in escape sequence. size=%d, offset=%d", lit, ch, size, offset)) 81 | } 82 | x = x*base + d 83 | } 84 | if x > max || 0xD800 <= x && x < 0xE000 { 85 | panic(fmt.Sprintf("Error decoding escape char value. Lit:%s, offset:%d, escape sequence is invalid Unicode code point\n", lit, offset)) 86 | } 87 | 88 | return rune(x) 89 | } 90 | 91 | func digitVal(ch rune) int { 92 | switch { 93 | case '0' <= ch && ch <= '9': 94 | return int(ch) - '0' 95 | case 'a' <= ch && ch <= 'f': 96 | return int(ch) - 'a' + 10 97 | case 'A' <= ch && ch <= 'F': 98 | return int(ch) - 'A' + 10 99 | } 100 | return 16 // larger than any legal digit val 101 | } 102 | -------------------------------------------------------------------------------- /internal/gocc/util/rune.go: -------------------------------------------------------------------------------- 1 | // Code generated by gocc; DO NOT EDIT. 2 | 3 | package util 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func RuneToString(r rune) string { 10 | if r >= 0x20 && r < 0x7f { 11 | return fmt.Sprintf("'%c'", r) 12 | } 13 | switch r { 14 | case 0x07: 15 | return "'\\a'" 16 | case 0x08: 17 | return "'\\b'" 18 | case 0x0C: 19 | return "'\\f'" 20 | case 0x0A: 21 | return "'\\n'" 22 | case 0x0D: 23 | return "'\\r'" 24 | case 0x09: 25 | return "'\\t'" 26 | case 0x0b: 27 | return "'\\v'" 28 | case 0x5c: 29 | return "'\\\\\\'" 30 | case 0x27: 31 | return "'\\''" 32 | case 0x22: 33 | return "'\\\"'" 34 | } 35 | if r < 0x10000 { 36 | return fmt.Sprintf("\\u%04x", r) 37 | } 38 | return fmt.Sprintf("\\U%08x", r) 39 | } 40 | -------------------------------------------------------------------------------- /internal/testroms/nestest/nestest.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retroenv/nesgo/c089e79eb37104387ab5967ecd1864dc8170fcc4/internal/testroms/nestest/nestest.nes -------------------------------------------------------------------------------- /internal/testroms/nestest/nestest_test.go: -------------------------------------------------------------------------------- 1 | package nestest 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "testing" 8 | 9 | "github.com/retroenv/nesgo/pkg/nes" 10 | "github.com/retroenv/retrogolib/arch/cpu/m6502" 11 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 12 | "github.com/retroenv/retrogolib/assert" 13 | ) 14 | 15 | func TestNestest(t *testing.T) { 16 | file, err := os.Open("nestest.nes") 17 | assert.NoError(t, err) 18 | 19 | cart, err := cartridge.LoadFile(file) 20 | assert.NoError(t, err) 21 | assert.NoError(t, file.Close()) 22 | 23 | var buffer bytes.Buffer 24 | trace := bufio.NewWriter(&buffer) 25 | 26 | m6502.Isc.Name = "isb" 27 | 28 | options := []nes.Option{ 29 | nes.WithEmulator(), 30 | nes.WithCartridge(cart), 31 | nes.WithEntrypoint(0xc000), 32 | nes.WithStopAt(0x0001), 33 | nes.WithDisabledGUI(), 34 | nes.WithTracingTarget(trace), 35 | } 36 | nes.Start(nil, options...) 37 | 38 | assert.NoError(t, trace.Flush()) 39 | 40 | file, err = os.Open("nestest_no_ppu.log") 41 | assert.NoError(t, err) 42 | 43 | nestest := bufio.NewScanner(file) 44 | emulator := bufio.NewScanner(bufio.NewReader(&buffer)) 45 | 46 | for nestest.Scan() { 47 | expected := nestest.Text() 48 | assert.True(t, emulator.Scan()) 49 | 50 | got := emulator.Text() 51 | assert.Equal(t, expected, got) 52 | } 53 | 54 | assert.NoError(t, file.Close()) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/apu/const.go: -------------------------------------------------------------------------------- 1 | // Package apu provides APU (Audio Processing Unit) functionality. 2 | package apu 3 | 4 | const ( 5 | SQ1_VOL = 0x4000 6 | SQ1_SWEEP = 0x4001 7 | SQ1_LO = 0x4002 8 | SQ1_HI = 0x4003 9 | SQ2_VOL = 0x4004 10 | SQ2_SWEEP = 0x4005 11 | SQ2_LO = 0x4006 12 | SQ2_HI = 0x4007 13 | TRI_LINEAR = 0x4008 14 | TRI_LO = 0x400A 15 | TRI_HI = 0x400B 16 | NOISE_VOL = 0x400C 17 | NOISE_LO = 0x400E 18 | NOISE_HI = 0x400F 19 | APU_DMC_CTRL = 0x4010 20 | APU_CHAN_CTRL = 0x4015 21 | APU_FRAME = 0x4017 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/apu/const_translation.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package apu 4 | 5 | import . "github.com/retroenv/retrogolib/addressing" 6 | 7 | // AddressToName maps address constants from address to name. 8 | var AddressToName = map[uint16]AccessModeConstant{ 9 | SQ1_VOL: {Constant: "SQ1_VOL", Mode: WriteAccess}, 10 | SQ1_SWEEP: {Constant: "SQ1_SWEEP", Mode: WriteAccess}, 11 | SQ1_LO: {Constant: "SQ1_LO", Mode: WriteAccess}, 12 | SQ1_HI: {Constant: "SQ1_HI", Mode: WriteAccess}, 13 | SQ2_VOL: {Constant: "SQ2_VOL", Mode: WriteAccess}, 14 | SQ2_SWEEP: {Constant: "SQ2_SWEEP", Mode: WriteAccess}, 15 | SQ2_LO: {Constant: "SQ2_LO", Mode: WriteAccess}, 16 | SQ2_HI: {Constant: "SQ2_HI", Mode: WriteAccess}, 17 | TRI_LINEAR: {Constant: "TRI_LINEAR", Mode: WriteAccess}, 18 | TRI_LO: {Constant: "TRI_LO", Mode: WriteAccess}, 19 | TRI_HI: {Constant: "TRI_HI", Mode: WriteAccess}, 20 | NOISE_VOL: {Constant: "NOISE_VOL", Mode: WriteAccess}, 21 | NOISE_LO: {Constant: "NOISE_LO", Mode: WriteAccess}, 22 | NOISE_HI: {Constant: "NOISE_HI", Mode: WriteAccess}, 23 | APU_DMC_CTRL: {Constant: "APU_DMC_CTRL", Mode: WriteAccess}, 24 | APU_CHAN_CTRL: {Constant: "APU_CHAN_CTRL", Mode: ReadWriteAccess}, 25 | APU_FRAME: {Constant: "APU_FRAME", Mode: WriteAccess}, 26 | } 27 | -------------------------------------------------------------------------------- /pkg/bus/bus.go: -------------------------------------------------------------------------------- 1 | // Package bus provides a system Bus connecting all main system parts. 2 | package bus 3 | 4 | import ( 5 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 6 | ) 7 | 8 | // Bus contains all NES sub system components. 9 | // Since many components access other components, this structure 10 | // allows an easy access and reduces the import dependencies and 11 | // initialization order issues. 12 | type Bus struct { 13 | Cartridge *cartridge.Cartridge // used by Mapper 14 | Controller1 Controller // used by Memory 15 | Controller2 Controller // used by Memory 16 | CPU CPU // used by PPU 17 | Mapper Mapper // used by Memory and PPU 18 | Memory Memory // used by CPU 19 | NameTable NameTable // used by CPU and Mapper 20 | PPU PPU // used by Memory 21 | } 22 | -------------------------------------------------------------------------------- /pkg/bus/controller.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import "github.com/retroenv/nesgo/pkg/controller" 4 | 5 | // Controller represents a hardware controller. 6 | type Controller interface { 7 | Read() uint8 8 | SetButtonState(key controller.Button, pressed bool) 9 | SetStrobeMode(mode uint8) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/bus/cpu.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | // CPUFlags contains the CPU flags. 4 | type CPUFlags struct { 5 | C uint8 6 | Z uint8 7 | I uint8 8 | D uint8 9 | B uint8 10 | V uint8 11 | N uint8 12 | } 13 | 14 | // CPUInterrupts contains the CPU interrupt info. 15 | type CPUInterrupts struct { 16 | NMITriggered bool 17 | NMIRunning bool 18 | IrqTriggered bool 19 | IrqRunning bool 20 | } 21 | 22 | // CPUState contains the current state of the CPU. 23 | type CPUState struct { 24 | A uint8 25 | X uint8 26 | Y uint8 27 | PC uint16 28 | SP uint8 29 | Cycles uint64 30 | Flags CPUFlags 31 | Interrupts CPUInterrupts 32 | } 33 | 34 | // CPU represents the Central Processing Unit. 35 | type CPU interface { 36 | Cycles() uint64 37 | StallCycles(cycles uint16) 38 | State() CPUState 39 | TriggerIrq() 40 | TriggerNMI() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/bus/mapper.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import "github.com/retroenv/retrogolib/arch/nes/cartridge" 4 | 5 | // MapperState contains the current state of the mapper. 6 | type MapperState struct { 7 | ID byte `json:"id"` 8 | Name string `json:"name"` 9 | 10 | ChrWindows []int `json:"chrWindows"` 11 | PrgWindows []int `json:"prgWindows"` 12 | } 13 | 14 | // Mapper represents a mapper memory access interface. 15 | type Mapper interface { 16 | BasicMemory 17 | 18 | MirrorMode() cartridge.MirrorMode 19 | State() MapperState 20 | } 21 | -------------------------------------------------------------------------------- /pkg/bus/memory.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | // BasicMemory represents a basic memory access interface. 4 | type BasicMemory interface { 5 | Read(address uint16) uint8 6 | Write(address uint16, value uint8) 7 | } 8 | 9 | // Memory represents an advanced memory access interface. 10 | type Memory interface { 11 | BasicMemory 12 | 13 | ReadAbsolute(address any, register any) byte 14 | ReadAddressModes(immediate bool, params ...any) byte 15 | ReadWord(address uint16) uint16 16 | ReadWordBug(address uint16) uint16 17 | WriteAddressModes(value byte, params ...any) 18 | WriteWord(address, value uint16) 19 | 20 | LinkRegisters(x *uint8, y *uint8, globalX *uint8, globalY *uint8) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/bus/ppu.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 7 | ) 8 | 9 | // PPU represents the Picture Processing Unit. 10 | type PPU interface { 11 | BasicMemory 12 | 13 | Image() *image.RGBA 14 | Palette() Palette 15 | Step(cycles int) 16 | } 17 | 18 | // Palette represents the PPU palette. 19 | type Palette interface { 20 | BasicMemory 21 | 22 | Data() [32]byte 23 | } 24 | 25 | // NameTable represents a name table interface. 26 | type NameTable interface { 27 | BasicMemory 28 | 29 | Data() [4][]byte 30 | MirrorMode() cartridge.MirrorMode 31 | SetMirrorMode(mirrorMode cartridge.MirrorMode) 32 | SetVRAM(vram []byte) 33 | 34 | Fetch(address uint16) 35 | Value() byte 36 | } 37 | -------------------------------------------------------------------------------- /pkg/ca65/external.go: -------------------------------------------------------------------------------- 1 | // Package ca65 provides helpers to create ca65 assembler compatible asm output. 2 | package ca65 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | assembler = "ca65" 13 | linker = "ld65" 14 | ) 15 | 16 | // Config holds the ROM building configuration. 17 | type Config struct { 18 | PrgBase int 19 | PRGSize int 20 | CHRSize int 21 | } 22 | 23 | // AssembleUsingExternalApp calls the external assembler and linker to generate a .nes 24 | // ROM from the given asm file. 25 | func AssembleUsingExternalApp(asmFile, objectFile, outputFile string, conf Config) error { 26 | if _, err := exec.LookPath(assembler); err != nil { 27 | return fmt.Errorf("%s is not installed", assembler) 28 | } 29 | if _, err := exec.LookPath(linker); err != nil { 30 | return fmt.Errorf("%s is not installed", linker) 31 | } 32 | 33 | cmd := exec.Command(assembler, asmFile, "-o", objectFile) 34 | if out, err := cmd.CombinedOutput(); err != nil { 35 | return fmt.Errorf("assembling file: %s: %w", strings.TrimSpace(string(out)), err) 36 | } 37 | 38 | configFile, err := os.CreateTemp("", "rom"+".*.cfg") 39 | if err != nil { 40 | return fmt.Errorf("creating temp file: %w", err) 41 | } 42 | defer func() { 43 | _ = os.Remove(configFile.Name()) 44 | }() 45 | 46 | mapperConfig := GenerateMapperConfig(conf) 47 | 48 | if err := os.WriteFile(configFile.Name(), []byte(mapperConfig), 0444); err != nil { 49 | return fmt.Errorf("writing linker config: %w", err) 50 | } 51 | 52 | cmd = exec.Command(linker, "-C", configFile.Name(), "-o", outputFile, objectFile) 53 | if out, err := cmd.CombinedOutput(); err != nil { 54 | return fmt.Errorf("linking file: %s: %w", string(out), err) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/ca65/mapper.go: -------------------------------------------------------------------------------- 1 | package ca65 2 | 3 | import "fmt" 4 | 5 | var mapper0Config = ` 6 | MEMORY { 7 | ZP: start = $00, size = $100, type = rw, file = ""; 8 | RAM: start = $0200, size = $600, type = rw, file = ""; 9 | HDR: start = $0000, size = $10, type = ro, file = %%O, fill = yes; 10 | PRG: start = $%04X, size = $%04X, type = ro, file = %%O, fill = yes; 11 | CHR: start = $0000, size = $%04X, type = ro, file = %%O, fill = yes; 12 | } 13 | 14 | SEGMENTS { 15 | ZEROPAGE: load = ZP, type = zp; 16 | OAM: load = RAM, type = bss, start = $200, optional = yes; 17 | BSS: load = RAM, type = bss; 18 | HEADER: load = HDR, type = ro; 19 | CODE: load = PRG, type = ro, start = $%04X; 20 | DPCM: load = PRG, type = ro, start = $C000, optional = yes; 21 | VECTORS: load = PRG, type = ro, start = $%04X; 22 | TILES: load = CHR, type = ro; 23 | } 24 | ` 25 | 26 | // GenerateMapperConfig generates a ca65 linker config dynamically based on the passed ROM settings. 27 | func GenerateMapperConfig(conf Config) string { 28 | prgSize := conf.PRGSize 29 | vectorStart := conf.PrgBase + prgSize - 6 30 | 31 | generatedConfig := fmt.Sprintf(mapper0Config, conf.PrgBase, prgSize, conf.CHRSize, conf.PrgBase, vectorStart) 32 | return generatedConfig 33 | } 34 | -------------------------------------------------------------------------------- /pkg/controller/const.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | const ( 4 | JOYPAD1 = 0x4016 5 | JOYPAD2 = 0x4017 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/controller/const_translation.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package controller 4 | 5 | import . "github.com/retroenv/retrogolib/addressing" 6 | 7 | // AddressToName maps address constants from address to name. 8 | var AddressToName = map[uint16]AccessModeConstant{ 9 | JOYPAD1: {Constant: "JOYPAD1", Mode: ReadWriteAccess}, 10 | JOYPAD2: {Constant: "JOYPAD2", Mode: ReadAccess}, 11 | } 12 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package controller provides hardware controller functionality. 4 | package controller 5 | 6 | import "sync/atomic" 7 | 8 | // Button defines a button on the controller. 9 | type Button uint64 10 | 11 | const ( 12 | A Button = 0b0000_0001 13 | B Button = 0b0000_0010 14 | Select Button = 0b0000_0100 15 | Start Button = 0b0000_1000 16 | Up Button = 0b0001_0000 17 | Down Button = 0b0010_0000 18 | Left Button = 0b0100_0000 19 | Right Button = 0b1000_0000 20 | ) 21 | 22 | // Controller represents a hardware controller. 23 | type Controller struct { 24 | // if strobeMode is set, it resets the pointer to the state to read 25 | // to the A button. The pointer is not advanced on every state read 26 | // until it strobe mode is set off again. 27 | strobeMode bool 28 | // button pressed state as flags. needs to be read atomically as it 29 | // written by the main goroutine that is locked for SDL/OpenGL usage 30 | // and the emulator running in a separate goroutine. 31 | buttons uint64 32 | // index (mask) of next button state to read 33 | index uint8 34 | } 35 | 36 | // New returns a new Controller. 37 | func New() *Controller { 38 | c := &Controller{} 39 | c.reset() 40 | return c 41 | } 42 | 43 | func (c *Controller) reset() { 44 | c.buttons = 0 45 | c.strobeMode = false 46 | c.index = 1 47 | } 48 | 49 | // SetStrobeMode sets the strobe mode flag of the controller. 50 | func (c *Controller) SetStrobeMode(mode uint8) { 51 | if mode&1 == 1 { 52 | c.strobeMode = true 53 | c.index = 1 54 | } else { 55 | c.strobeMode = false 56 | } 57 | } 58 | 59 | // Read returns the current button state. 60 | func (c *Controller) Read() uint8 { 61 | state := atomic.LoadUint64(&c.buttons) 62 | if c.strobeMode { 63 | return uint8(state & uint64(A)) 64 | } 65 | 66 | val := state & uint64(c.index) // nolint:ifshort 67 | c.index <<= 1 68 | if val != 0 { 69 | return 1 70 | } 71 | return 0 72 | } 73 | 74 | // SetButtonState sets the current button state. 75 | func (c *Controller) SetButtonState(key Button, pressed bool) { 76 | state := atomic.LoadUint64(&c.buttons) 77 | if pressed { 78 | state |= uint64(key) 79 | } else { 80 | state &= ^uint64(key) 81 | } 82 | atomic.StoreUint64(&c.buttons, state) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/retrogolib/assert" 7 | ) 8 | 9 | func TestController(t *testing.T) { 10 | c := New() 11 | c.SetButtonState(B, true) 12 | 13 | assert.Equal(t, 0, c.Read()) 14 | assert.Equal(t, 1, c.Read()) 15 | assert.Equal(t, 0, c.Read()) 16 | 17 | c.SetStrobeMode(1) 18 | assert.Equal(t, 0, c.Read()) 19 | assert.Equal(t, 0, c.Read()) 20 | 21 | c.SetStrobeMode(0) 22 | assert.Equal(t, 0, c.Read()) 23 | assert.Equal(t, 1, c.Read()) 24 | 25 | c.SetButtonState(B, false) 26 | c.SetStrobeMode(1) 27 | c.SetStrobeMode(0) 28 | assert.Equal(t, 0, c.Read()) 29 | assert.Equal(t, 0, c.Read()) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/cpu/addressing.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | . "github.com/retroenv/retrogolib/addressing" 8 | "github.com/retroenv/retrogolib/cpu" 9 | ) 10 | 11 | // addressModeFromCall gets the addressing mode from the passed params. 12 | func (c *CPU) addressModeFromCall(instruction *cpu.Instruction, params ...any) Mode { 13 | if len(params) == 0 { 14 | mode := addressModeFromCallNoParam(instruction) 15 | return mode 16 | } 17 | 18 | firstParam := params[0] 19 | var register any 20 | if len(params) > 1 { 21 | register = params[1] 22 | } 23 | 24 | switch address := firstParam.(type) { 25 | case int: 26 | return c.addressModeInt(address, instruction, firstParam, register) 27 | 28 | case uint8: 29 | return ImmediateAddressing 30 | 31 | case *uint8: // variable 32 | return ImmediateAddressing 33 | 34 | case Absolute: 35 | return c.addressModeAbsolute(instruction) 36 | 37 | case Indirect, IndirectResolved: 38 | return c.addressModeIndirect(register) 39 | 40 | case ZeroPage: 41 | return c.addressModeZeroPage(register) 42 | 43 | case Accumulator: 44 | return AccumulatorAddressing 45 | 46 | default: 47 | panic(fmt.Sprintf("unsupported addressing mode type %T", firstParam)) 48 | } 49 | } 50 | 51 | func (c *CPU) addressModeInt(address int, instruction *cpu.Instruction, firstParam, register any) Mode { 52 | if instruction.HasAddressing(ImmediateAddressing) && register == nil && address <= math.MaxUint8 { 53 | return ImmediateAddressing 54 | } 55 | if register == nil { 56 | return AbsoluteAddressing 57 | } 58 | 59 | ptr := register.(*uint8) 60 | switch ptr { 61 | case &c.X: 62 | return AbsoluteXAddressing 63 | case &c.Y: 64 | return AbsoluteYAddressing 65 | default: 66 | panic(fmt.Sprintf("unsupported int parameter %v", firstParam)) 67 | } 68 | } 69 | 70 | func (c *CPU) addressModeAbsolute(instruction *cpu.Instruction) Mode { 71 | // branches in emulation mode 72 | if instruction.HasAddressing(RelativeAddressing) { 73 | return RelativeAddressing 74 | } 75 | 76 | return AbsoluteAddressing 77 | } 78 | 79 | func (c *CPU) addressModeIndirect(register any) Mode { 80 | if register == nil { 81 | return IndirectAddressing 82 | } 83 | 84 | ptr := register.(*uint8) 85 | switch ptr { 86 | case &c.X: 87 | return IndirectXAddressing 88 | case &c.Y: 89 | return IndirectYAddressing 90 | default: 91 | panic(fmt.Sprintf("unsupported indirect parameter %v", register)) 92 | } 93 | } 94 | 95 | func (c *CPU) addressModeZeroPage(register any) Mode { 96 | if register == nil { 97 | return ZeroPageAddressing 98 | } 99 | 100 | ptr := register.(*uint8) 101 | switch ptr { 102 | case &c.X: 103 | return ZeroPageXAddressing 104 | case &c.Y: 105 | return ZeroPageYAddressing 106 | default: 107 | panic(fmt.Sprintf("unsupported zeropage parameter %v", register)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/cpu/cpu.go: -------------------------------------------------------------------------------- 1 | // Package cpu provides CPU (Central Processing Unit) functionality. 2 | package cpu 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | 8 | "github.com/retroenv/nesgo/pkg/bus" 9 | "github.com/retroenv/retrogolib/arch/nes/parameter" 10 | ) 11 | 12 | const ( 13 | StackBase = 0x100 14 | 15 | initialCycles = 7 16 | initialFlags = 0b0010_0100 // I and U flags are 1, the rest 0 17 | InitialStack = 0xFD 18 | ) 19 | 20 | // CPU implements a MOS Technology 6502 CPU. 21 | type CPU struct { 22 | mu sync.RWMutex 23 | 24 | A uint8 // accumulator 25 | X uint8 // x register 26 | Y uint8 // y register 27 | PC uint16 // program counter 28 | SP uint8 // stack pointer 29 | Flags flags 30 | 31 | bus *bus.Bus 32 | 33 | emulator bool 34 | irqAddress uint16 35 | irqHandler *func() 36 | irqRunning bool 37 | nmiAddress uint16 38 | nmiHandler *func() 39 | nmiRunning bool 40 | triggerIrq bool 41 | triggerNmi bool 42 | 43 | cycles uint64 44 | stallCycles uint16 // TODO stall cycles, use a Step() function 45 | 46 | tracing TracingMode 47 | tracingTarget io.Writer 48 | TraceStep TraceStep 49 | paramConverter parameter.Converter 50 | lastFunction string 51 | } 52 | 53 | // New creates a new CPU. 54 | func New(bus *bus.Bus, nmiHandler, irqHandler *func(), emulator bool) *CPU { 55 | c := &CPU{ 56 | SP: InitialStack, 57 | bus: bus, 58 | emulator: emulator, 59 | irqHandler: irqHandler, 60 | nmiHandler: nmiHandler, 61 | cycles: initialCycles, 62 | paramConverter: parameter.New(), 63 | } 64 | 65 | // read interrupt handler addresses 66 | c.nmiAddress = bus.Memory.ReadWord(0xFFFA) 67 | c.PC = bus.Memory.ReadWord(0xFFFC) 68 | c.irqAddress = bus.Memory.ReadWord(0xFFFE) 69 | 70 | c.setFlags(initialFlags) 71 | return c 72 | } 73 | 74 | // SetTracing sets the CPU tracing options. 75 | func (c *CPU) SetTracing(mode TracingMode, target io.Writer) { 76 | c.tracing = mode 77 | c.tracingTarget = target 78 | } 79 | 80 | // ResetCycles sets the cycle counter to 0. 81 | // This is useful for counting used CPU cycles for a function. 82 | func (c *CPU) ResetCycles() { 83 | c.cycles = 0 84 | } 85 | 86 | // Cycles returns the amount of CPU cycles executed since system start. 87 | func (c *CPU) Cycles() uint64 { 88 | return c.cycles 89 | } 90 | 91 | // StallCycles stalls the CPU for the given amount of cycles. This is used for DMA transfer in the PPU. 92 | func (c *CPU) StallCycles(cycles uint16) { 93 | c.stallCycles = cycles 94 | } 95 | 96 | // TriggerIrq causes a interrupt request to occur on the next cycle. 97 | func (c *CPU) TriggerIrq() { 98 | c.triggerIrq = true 99 | } 100 | 101 | // TriggerNMI causes a non-maskable interrupt to occur on the next cycle. 102 | func (c *CPU) TriggerNMI() { 103 | c.triggerNmi = true 104 | } 105 | 106 | // writeLock takes the mutex write lock and returns a function to write unlock to allow an easy use for 107 | // the lock/unlock mechanism in the form of defer c.writeLock()() 108 | func (c *CPU) writeLock() func() { 109 | c.mu.Lock() 110 | return c.writeUnlock 111 | } 112 | 113 | func (c *CPU) writeUnlock() { 114 | c.mu.Unlock() 115 | } 116 | -------------------------------------------------------------------------------- /pkg/cpu/debugger.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import "github.com/retroenv/nesgo/pkg/bus" 4 | 5 | // State returns the current state of the CPU. 6 | func (c *CPU) State() bus.CPUState { 7 | c.mu.RLock() 8 | defer c.mu.RUnlock() 9 | 10 | state := bus.CPUState{ 11 | A: c.A, 12 | X: c.X, 13 | Y: c.Y, 14 | PC: c.PC, 15 | SP: c.SP, 16 | Cycles: c.cycles, 17 | Flags: bus.CPUFlags{ 18 | C: c.Flags.C, 19 | Z: c.Flags.Z, 20 | I: c.Flags.I, 21 | D: c.Flags.D, 22 | B: c.Flags.B, 23 | V: c.Flags.V, 24 | N: c.Flags.N, 25 | }, 26 | Interrupts: bus.CPUInterrupts{ 27 | NMITriggered: c.triggerNmi, 28 | NMIRunning: c.nmiRunning, 29 | IrqTriggered: c.triggerIrq, 30 | IrqRunning: c.irqRunning, 31 | }, 32 | } 33 | return state 34 | } 35 | -------------------------------------------------------------------------------- /pkg/cpu/flags.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | // Bit No. 7 6 5 4 3 2 1 0 4 | // Flag S V B D I Z C 5 | type flags struct { 6 | C uint8 // carry flag 7 | Z uint8 // zero flag 8 | I uint8 // interrupt disable flag 9 | D uint8 // decimal mode flag 10 | B uint8 // break command flag 11 | U uint8 // unused flag 12 | V uint8 // overflow flag 13 | N uint8 // negative flag 14 | } 15 | 16 | func (c *CPU) setFlags(flags uint8) { 17 | c.Flags.C = (flags >> 0) & 1 18 | c.Flags.Z = (flags >> 1) & 1 19 | c.Flags.I = (flags >> 2) & 1 20 | c.Flags.D = (flags >> 3) & 1 21 | c.Flags.B = (flags >> 4) & 1 22 | c.Flags.U = (flags >> 5) & 1 23 | c.Flags.V = (flags >> 6) & 1 24 | c.Flags.N = (flags >> 7) & 1 25 | } 26 | 27 | // GetFlags returns the current state of flags as byte. 28 | func (c *CPU) GetFlags() uint8 { 29 | var f byte 30 | f |= c.Flags.C << 0 31 | f |= c.Flags.Z << 1 32 | f |= c.Flags.I << 2 33 | f |= c.Flags.D << 3 34 | f |= c.Flags.B << 4 35 | f |= c.Flags.U << 5 36 | f |= c.Flags.V << 6 37 | f |= c.Flags.N << 7 38 | return f 39 | } 40 | 41 | // setZ - set the zero flag if the argument is zero. 42 | func (c *CPU) setZ(value uint8) { 43 | if value == 0 { 44 | c.Flags.Z = 1 45 | } else { 46 | c.Flags.Z = 0 47 | } 48 | } 49 | 50 | // setN - set the negative flag if the argument is negative (high bit is set). 51 | func (c *CPU) setN(value uint8) { 52 | if value&0x80 != 0 { 53 | c.Flags.N = 1 54 | } else { 55 | c.Flags.N = 0 56 | } 57 | } 58 | 59 | // setV - set the overflow flag. 60 | func (c *CPU) setV(set bool) { 61 | if set { 62 | c.Flags.V = 1 63 | } else { 64 | c.Flags.V = 0 65 | } 66 | } 67 | 68 | func (c *CPU) setZN(value uint8) { 69 | c.setZ(value) 70 | c.setN(value) 71 | } 72 | 73 | func (c *CPU) compare(a, b byte) { 74 | c.setZN(a - b) 75 | if a >= b { 76 | c.Flags.C = 1 77 | } else { 78 | c.Flags.C = 0 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/cpu/helper.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package cpu 4 | 5 | import ( 6 | . "github.com/retroenv/retrogolib/addressing" 7 | ) 8 | 9 | // execute branch jump if the branching op result is true. 10 | func (c *CPU) branch(branchTo bool, param any) { 11 | // disable trace while calling the go mode branch code 12 | // TODO refactor to avoid this 13 | trace := c.tracing 14 | c.tracing = NoTracing 15 | 16 | if branchTo { 17 | addr := param.(Absolute) 18 | 19 | c.PC = uint16(addr) 20 | c.cycles++ 21 | } 22 | 23 | c.tracing = trace 24 | } 25 | 26 | // hasAccumulatorParam returns whether the passed or missing parameter 27 | // indicates usage of the accumulator register. 28 | func hasAccumulatorParam(params ...any) bool { 29 | if params == nil { 30 | return true 31 | } 32 | param := params[0] 33 | _, ok := param.(Accumulator) 34 | return ok 35 | } 36 | 37 | // push a value to the stack and update the stack pointer. 38 | func (c *CPU) push(value byte) { 39 | c.bus.Memory.Write(uint16(StackBase+int(c.SP)), value) 40 | c.SP-- 41 | } 42 | 43 | // Push16 a word to the stack and update the stack pointer. 44 | func (c *CPU) Push16(value uint16) { 45 | high := byte(value >> 8) 46 | low := byte(value) 47 | c.push(high) 48 | c.push(low) 49 | } 50 | 51 | // Pop pops a byte from the stack and update the stack pointer. 52 | func (c *CPU) Pop() byte { 53 | c.SP++ 54 | return c.bus.Memory.Read(uint16(StackBase + int(c.SP))) 55 | } 56 | 57 | // Pop16 pops a word from the stack and updates the stack pointer. 58 | func (c *CPU) Pop16() uint16 { 59 | low := uint16(c.Pop()) 60 | high := uint16(c.Pop()) 61 | return high<<8 | low 62 | } 63 | -------------------------------------------------------------------------------- /pkg/cpu/instruction.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import "github.com/retroenv/retrogolib/arch/cpu/m6502" 4 | 5 | // LinkInstructionFuncs links cpu instruction emulation functions to the CPU instance. 6 | // Defining it directly in the instruction instances like ParamFunc: (CPU*).Adc does not work 7 | // due to an initialization loop of Opcodes refers to adc refers to CPU.Adc refers to 8 | // instructionHook refers to Opcodes. 9 | // nolint: funlen 10 | func LinkInstructionFuncs(c *CPU) { 11 | m6502.Adc.ParamFunc = c.Adc 12 | m6502.And.ParamFunc = c.And 13 | m6502.Asl.ParamFunc = c.Asl 14 | m6502.Bcc.ParamFunc = c.BccInternal 15 | m6502.Bcs.ParamFunc = c.BcsInternal 16 | m6502.Beq.ParamFunc = c.BeqInternal 17 | m6502.Bit.ParamFunc = c.Bit 18 | m6502.Bmi.ParamFunc = c.BmiInternal 19 | m6502.Bne.ParamFunc = c.BneInternal 20 | m6502.Bpl.ParamFunc = c.BplInternal 21 | m6502.Brk.NoParamFunc = c.Brk 22 | m6502.Bvc.ParamFunc = c.BvcInternal 23 | m6502.Bvs.ParamFunc = c.BvsInternal 24 | m6502.Clc.NoParamFunc = c.Clc 25 | m6502.Cld.NoParamFunc = c.Cld 26 | m6502.Cli.NoParamFunc = c.Cli 27 | m6502.Clv.NoParamFunc = c.Clv 28 | m6502.Cmp.ParamFunc = c.Cmp 29 | m6502.Cpx.ParamFunc = c.Cpx 30 | m6502.Cpy.ParamFunc = c.Cpy 31 | m6502.Dec.ParamFunc = c.Dec 32 | m6502.Dex.NoParamFunc = c.Dex 33 | m6502.Dey.NoParamFunc = c.Dey 34 | m6502.Eor.ParamFunc = c.Eor 35 | m6502.Inc.ParamFunc = c.Inc 36 | m6502.Inx.NoParamFunc = c.Inx 37 | m6502.Iny.NoParamFunc = c.Iny 38 | m6502.Jmp.ParamFunc = c.Jmp 39 | m6502.Jsr.ParamFunc = c.Jsr 40 | m6502.Lda.ParamFunc = c.Lda 41 | m6502.Ldx.ParamFunc = c.Ldx 42 | m6502.Ldy.ParamFunc = c.Ldy 43 | m6502.Lsr.ParamFunc = c.Lsr 44 | m6502.Nop.NoParamFunc = c.Nop 45 | m6502.Ora.ParamFunc = c.Ora 46 | m6502.Pha.NoParamFunc = c.Pha 47 | m6502.Php.NoParamFunc = c.Php 48 | m6502.Pla.NoParamFunc = c.Pla 49 | m6502.Plp.NoParamFunc = c.Plp 50 | m6502.Rol.ParamFunc = c.Rol 51 | m6502.Ror.ParamFunc = c.Ror 52 | m6502.Rti.NoParamFunc = c.Rti 53 | m6502.Rts.NoParamFunc = c.Rts 54 | m6502.Sbc.ParamFunc = c.Sbc 55 | m6502.Sec.NoParamFunc = c.Sec 56 | m6502.Sed.NoParamFunc = c.Sed 57 | m6502.Sei.NoParamFunc = c.Sei 58 | m6502.Sta.ParamFunc = c.Sta 59 | m6502.Stx.ParamFunc = c.Stx 60 | m6502.Sty.ParamFunc = c.Sty 61 | m6502.Tax.NoParamFunc = c.Tax 62 | m6502.Tay.NoParamFunc = c.Tay 63 | m6502.Tsx.NoParamFunc = c.Tsx 64 | m6502.Txa.NoParamFunc = c.Txa 65 | m6502.Txs.NoParamFunc = c.Txs 66 | m6502.Tya.NoParamFunc = c.Tya 67 | 68 | linkUnofficialInstructionFuncs(c) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/cpu/interrupt.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | // CheckInterrupts checks for triggered interrupts and executes them. 4 | func (c *CPU) CheckInterrupts() { 5 | if c.triggerNmi { 6 | c.nmi() 7 | } 8 | if c.triggerIrq { 9 | c.irq() 10 | } 11 | } 12 | 13 | func (c *CPU) nmi() { 14 | c.mu.Lock() 15 | c.triggerNmi = false 16 | c.nmiRunning = true 17 | c.mu.Unlock() 18 | 19 | c.executeInterrupt(c.nmiHandler, c.nmiAddress) 20 | } 21 | 22 | func (c *CPU) irq() { 23 | c.mu.Lock() 24 | c.triggerIrq = false 25 | c.irqRunning = true 26 | c.mu.Unlock() 27 | 28 | c.executeInterrupt(c.irqHandler, c.irqAddress) 29 | } 30 | 31 | func (c *CPU) executeInterrupt(goFun *func(), funAddress uint16) { 32 | c.Push16(c.PC) 33 | c.phpInternal() 34 | 35 | if *goFun != nil { 36 | c.Flags.I = 1 37 | c.cycles += 7 38 | f := *goFun 39 | f() 40 | return 41 | } 42 | 43 | if funAddress != 0 { 44 | c.Flags.I = 1 45 | c.cycles += 7 46 | c.PC = funAddress 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/cpu/timing.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package cpu 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/retroenv/retrogolib/arch/cpu/m6502" 9 | "github.com/retroenv/retrogolib/cpu" 10 | ) 11 | 12 | // instructionHook is a hook that is executed before a CPU instruction is executed. 13 | // It allows for accounting of the instruction timing and trace logging. 14 | // Params can be of length 0 to 2. 15 | // At the end of the function the write lock is taken and a unlocker function returned. 16 | func (c *CPU) instructionHook(instruction *cpu.Instruction, params ...any) func() { 17 | if !c.emulator { 18 | // trigger interrupt checking here as the system is not looping through the instructions in go mode 19 | c.CheckInterrupts() 20 | } 21 | 22 | startCycles := c.cycles 23 | 24 | if c.tracing == NoTracing { 25 | addressing := c.addressModeFromCall(instruction, params...) 26 | if !instruction.HasAddressing(addressing) { 27 | panic(fmt.Sprintf("unexpected addressing mode type %T", addressing)) 28 | } 29 | 30 | opcode := instruction.Addressing[addressing].Opcode 31 | opcodeInfo := m6502.Opcodes[opcode] 32 | c.cycles += uint64(opcodeInfo.Timing) 33 | } else { 34 | if err := c.trace(instruction, params...); err != nil { 35 | panic(err) 36 | } 37 | c.cycles += uint64(c.TraceStep.Timing) 38 | 39 | if c.TraceStep.PageCrossed && c.TraceStep.PageCrossCycle { 40 | c.cycles++ 41 | } 42 | } 43 | 44 | // this executes the ppu steps before the instruction 45 | cpuCycles := c.cycles - startCycles 46 | ppuCycles := cpuCycles * 3 47 | c.bus.PPU.Step(int(ppuCycles)) 48 | 49 | return c.writeLock() 50 | } 51 | 52 | // AccountBranchingPageCrossCycle accounts for a branch page crossing extra CPU cycle. 53 | func (c *CPU) AccountBranchingPageCrossCycle(ins *cpu.Instruction) { 54 | if _, ok := m6502.BranchingInstructions[ins.Name]; !ok { 55 | return 56 | } 57 | if ins.Name != m6502.Jmp.Name && ins.Name != m6502.Jsr.Name { 58 | c.cycles++ 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/cpu/unofficial.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // This file contains support for unofficial CPU instructions. 4 | // https://www.nesdev.org/wiki/Programming_with_unofficial_opcodes 5 | 6 | package cpu 7 | 8 | import ( 9 | "github.com/retroenv/retrogolib/arch/cpu/m6502" 10 | ) 11 | 12 | func linkUnofficialInstructionFuncs(c *CPU) { 13 | m6502.Dcp.ParamFunc = c.Dcp 14 | m6502.Isc.ParamFunc = c.Isc 15 | m6502.Lax.ParamFunc = c.Lax 16 | m6502.NopUnofficial.ParamFunc = c.NopUnofficial 17 | m6502.Rla.ParamFunc = c.Rla 18 | m6502.Rra.ParamFunc = c.Rra 19 | m6502.Sax.ParamFunc = c.Sax 20 | m6502.SbcUnofficial.ParamFunc = c.SbcUnofficial 21 | m6502.Slo.ParamFunc = c.Slo 22 | m6502.Sre.ParamFunc = c.Sre 23 | } 24 | -------------------------------------------------------------------------------- /pkg/gamegenie/gamegenie.go: -------------------------------------------------------------------------------- 1 | // Package gamegenie implements NES Game Genie code and decode support. 2 | package gamegenie 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/retroenv/retrogolib/arch/nes" 9 | ) 10 | 11 | // Patch defines a patch to apply to a NES ROM. 12 | type Patch struct { 13 | Address uint16 14 | Data byte 15 | Compare byte 16 | HasCompare bool 17 | } 18 | 19 | // Decode decodes a game genie code into a patch. 20 | func Decode(code string) (Patch, error) { 21 | length := len(code) 22 | if length != 6 && length != 8 { 23 | return Patch{}, fmt.Errorf("invalid code length %d", length) 24 | } 25 | 26 | code = strings.ToUpper(code) 27 | n := make([]uint16, length) 28 | for i := range code { 29 | c := code[i] 30 | value, ok := translationCharToValue[c] 31 | if !ok { 32 | return Patch{}, fmt.Errorf("invalid character %v", c) 33 | } 34 | n[i] = uint16(value) 35 | } 36 | 37 | address := nes.CodeBaseAddress + ((n[3] & 7) << 12) | 38 | ((n[5] & 7) << 8) | ((n[4] & 8) << 8) | 39 | ((n[2] & 7) << 4) | ((n[1] & 8) << 4) | 40 | (n[4] & 7) | (n[3] & 8) 41 | 42 | data := ((n[1] & 7) << 4) | ((n[0] & 8) << 4) | (n[0] & 7) | (n[length-1] & 8) 43 | 44 | patch := Patch{ 45 | Address: address, 46 | Data: byte(data), 47 | } 48 | 49 | if length == 8 { 50 | patch.HasCompare = true 51 | patch.Compare = byte(((n[7] & 7) << 4) | ((n[6] & 8) << 4) | (n[6] & 7) | (n[5] & 8)) 52 | } 53 | 54 | return patch, nil 55 | } 56 | 57 | // Encode encodes a NES ROM patch into a game genie code. 58 | func Encode(patch Patch) (string, error) { 59 | if patch.Address < nes.CodeBaseAddress { 60 | return "", fmt.Errorf("address $%04X is not supported", patch.Address) 61 | } 62 | 63 | n := []uint16{ 64 | uint16(((patch.Data >> 4) & 8) | (patch.Data & 7)), 65 | ((patch.Address >> 4) & 8) | ((uint16(patch.Data) >> 4) & 7), 66 | 8 | ((patch.Address >> 4) & 7), 67 | (patch.Address & 8) | ((patch.Address >> 12) & 7), 68 | ((patch.Address >> 8) & 8) | (patch.Address & 7), 69 | } 70 | 71 | if patch.HasCompare { 72 | n = append(n, 73 | (uint16(patch.Compare)&8)|((patch.Address>>8)&7), 74 | uint16(((patch.Compare>>4)&8)|(patch.Compare&7)), 75 | uint16((patch.Data&8)|((patch.Compare>>4)&7)), 76 | ) 77 | } else { 78 | n = append(n, (uint16(patch.Data)&8)|((patch.Address>>8)&7)) 79 | } 80 | 81 | buf := strings.Builder{} 82 | for _, value := range n { 83 | character := translationValueToChar[value] 84 | buf.WriteByte(character) 85 | } 86 | return buf.String(), nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/gamegenie/gamegenie_test.go: -------------------------------------------------------------------------------- 1 | package gamegenie 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/retrogolib/assert" 7 | ) 8 | 9 | var testCases = []struct { 10 | Code string 11 | ExpectedAddress uint16 12 | ExpectedValue uint8 13 | ExpectedCompare uint8 14 | ExpectedError bool 15 | }{ 16 | { 17 | // Capcom's Ghosts 'n Goblins start your player with a really funky weapon 18 | Code: "GOSSIP", 19 | ExpectedAddress: 0xD1DD, 20 | ExpectedValue: 0x14, 21 | }, 22 | // TODO: fix 23 | // { 24 | // // Super Mario Bros 1 swim in any level 25 | // Code: "PIGOAP", 26 | // ExpectedAddress: 0x9148, 27 | // ExpectedValue: 0x51, 28 | // }, 29 | { 30 | // Dr. Mario clear a row or column with only 3 colors in a line, rather than 4 31 | Code: "ZEXPYGLA", 32 | ExpectedAddress: 0x94A7, 33 | ExpectedValue: 0x02, 34 | ExpectedCompare: 0x03, 35 | }, 36 | { 37 | Code: "TEST", 38 | ExpectedError: true, 39 | }, 40 | } 41 | 42 | func TestDecode(t *testing.T) { 43 | t.Parallel() 44 | 45 | for _, test := range testCases { 46 | test := test 47 | t.Run(test.Code, func(t *testing.T) { 48 | t.Parallel() 49 | 50 | patch, err := Decode(test.Code) 51 | 52 | if test.ExpectedError { 53 | assert.True(t, err != nil) 54 | return 55 | } 56 | assert.NoError(t, err) 57 | 58 | assert.Equal(t, test.ExpectedAddress, patch.Address) 59 | assert.Equal(t, test.ExpectedValue, patch.Data) 60 | 61 | if len(test.Code) == 8 { 62 | assert.Equal(t, test.ExpectedCompare, patch.Compare) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestEncode(t *testing.T) { 69 | t.Parallel() 70 | 71 | for _, test := range testCases { 72 | test := test 73 | t.Run(test.Code, func(t *testing.T) { 74 | t.Parallel() 75 | 76 | if test.ExpectedError { 77 | return 78 | } 79 | 80 | patch := Patch{ 81 | Address: test.ExpectedAddress, 82 | Data: test.ExpectedValue, 83 | Compare: test.ExpectedCompare, 84 | } 85 | if len(test.Code) == 8 { 86 | patch.HasCompare = true 87 | } 88 | 89 | code, err := Encode(patch) 90 | assert.NoError(t, err) 91 | assert.Equal(t, test.Code, code) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/gamegenie/translation.go: -------------------------------------------------------------------------------- 1 | package gamegenie 2 | 3 | var translationCharToValue = map[byte]byte{ 4 | 'A': 0x0, 5 | 'P': 0x1, 6 | 'Z': 0x2, 7 | 'L': 0x3, 8 | 'G': 0x4, 9 | 'I': 0x5, 10 | 'T': 0x6, 11 | 'Y': 0x7, 12 | 'E': 0x8, 13 | 'O': 0x9, 14 | 'X': 0xA, 15 | 'U': 0xB, 16 | 'K': 0xC, 17 | 'S': 0xD, 18 | 'V': 0xE, 19 | 'N': 0xF, 20 | } 21 | 22 | var translationValueToChar = []byte{ 23 | 'A', 24 | 'P', 25 | 'Z', 26 | 'L', 27 | 'G', 28 | 'I', 29 | 'T', 30 | 'Y', 31 | 'E', 32 | 'O', 33 | 'X', 34 | 'U', 35 | 'K', 36 | 'S', 37 | 'V', 38 | 'N', 39 | } 40 | -------------------------------------------------------------------------------- /pkg/mapper/mapper.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package mapper provides hardware mapper support. 4 | // It maps CHR and PRG chips into the NES address space. 5 | package mapper 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/retroenv/nesgo/pkg/bus" 11 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 12 | "github.com/retroenv/nesgo/pkg/mapper/mapperdb" 13 | ) 14 | 15 | type mapperInitializer func(base mapperdb.Base) bus.Mapper 16 | 17 | var mappers = map[byte]mapperInitializer{ 18 | 0: mapperdb.NewNROM, 19 | 1: mapperdb.NewMMC1, 20 | 2: mapperdb.NewUxROMOr, 21 | 3: mapperdb.NewCNROM, 22 | 7: mapperdb.NewAxROM, 23 | 30: mapperdb.NewUNROM512, 24 | 94: mapperdb.NewUN1ROM, 25 | 111: mapperdb.NewGTROM, 26 | 180: mapperdb.NewUxROMAnd, 27 | } 28 | 29 | // New creates a new mapper for the mapper defined by the cartridge. 30 | func New(bus *bus.Bus) (bus.Mapper, error) { 31 | mapperNumber := bus.Cartridge.Mapper 32 | initializer, ok := mappers[mapperNumber] 33 | if !ok { 34 | return nil, fmt.Errorf("mapper %d is not supported", mapperNumber) 35 | } 36 | 37 | base := mapperbase.New(bus) 38 | mapper := initializer(base) 39 | return mapper, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/mapper/mapperbase/bank.go: -------------------------------------------------------------------------------- 1 | package mapperbase 2 | 3 | type bank struct { 4 | data []byte 5 | length int 6 | } 7 | 8 | // setDefaultBankSizes sets the default CHR and PRG sizes based on the set window size. 9 | func (b *Base) setDefaultBankSizes() { 10 | b.setDefaultChrBankSizes() 11 | b.setDefaultPrgBankSizes() 12 | } 13 | 14 | // setBanks sets the bank data based on each bank's length. This needs to be called after the bank lengths 15 | // have been set. 16 | func (b *Base) setBanks() { 17 | b.setChrBanks() 18 | b.setPrgBanks() 19 | b.createNameTableBanks() 20 | } 21 | 22 | // setWindows sets the CHR and PRG windows to banks based on a static window size. 23 | func (b *Base) setWindows() { 24 | windows := chrMemSize / b.chrWindowSize 25 | b.chrWindows = make([]int, windows) 26 | bank := 0 27 | for i := 0; i < windows; i++ { 28 | b.chrWindows[i] = bank 29 | 30 | if bank+1 < len(b.chrBanks) { 31 | bank++ 32 | } else { 33 | bank = 0 34 | } 35 | } 36 | 37 | windows = prgMemSize / b.prgWindowSize 38 | b.prgWindows = make([]int, windows) 39 | bank = 0 40 | for i := 0; i < windows; i++ { 41 | b.prgWindows[i] = bank 42 | 43 | if bank+1 < len(b.prgBanks) { 44 | bank++ 45 | } else { 46 | bank = 0 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/mapper/mapperbase/chr.go: -------------------------------------------------------------------------------- 1 | package mapperbase 2 | 3 | // setDefaultChrBankSizes creates the banks with default lengths set based on the CHR and CHR window size. 4 | // Some mapper and modes can have a mix of sizes, for example two 2KB banks and four 1KB banks for MMC6. 5 | // In case of mixed sizing the bank sizes need to be initialized manually. 6 | func (b *Base) setDefaultChrBankSizes() { 7 | chrSize := len(b.bus.Cartridge.CHR) 8 | if chrSize == 0 { 9 | chrSize = len(b.chrRAM) 10 | } 11 | 12 | banks := chrSize / b.chrWindowSize 13 | b.chrBanks = make([]bank, banks) 14 | 15 | for i := 0; i < banks; i++ { 16 | bank := &b.chrBanks[i] 17 | bank.length = b.chrWindowSize 18 | } 19 | } 20 | 21 | // setChrBanks sets the bank data based on each bank's length. This needs to be called after the bank lengths 22 | // have been set, either by calling setDefaultChrBankSizes() or setting it manually. 23 | func (b *Base) setChrBanks() { 24 | chr := b.bus.Cartridge.CHR 25 | if len(chr) == 0 { 26 | chr = b.chrRAM 27 | } 28 | 29 | startOffset := 0 30 | 31 | for i := 0; i < len(b.chrBanks); i++ { 32 | bank := &b.chrBanks[i] 33 | endOffset := startOffset + bank.length 34 | bank.data = chr[startOffset:endOffset] 35 | startOffset += bank.length 36 | } 37 | } 38 | 39 | // ChrBankCount returns the amount of CHR banks. 40 | func (b *Base) ChrBankCount() int { 41 | return len(b.chrBanks) 42 | } 43 | 44 | // SetChrWindow sets a CHR window to a specific bank. 45 | func (b *Base) SetChrWindow(window, bank int) { 46 | if bank < 0 { 47 | bank = len(b.chrBanks) + bank 48 | } 49 | bank %= len(b.chrBanks) 50 | 51 | b.mu.Lock() 52 | b.chrWindows[window] = bank 53 | b.mu.Unlock() 54 | } 55 | 56 | // SetChrWindowSize sets the CHR window size. 57 | func (b *Base) SetChrWindowSize(size int) { 58 | b.chrWindowSize = size 59 | } 60 | 61 | // SetChrRAM enables the usage of CHR RAM and sets the RAM buffer. 62 | func (b *Base) SetChrRAM(ram []byte) { 63 | b.chrRAM = ram 64 | } 65 | -------------------------------------------------------------------------------- /pkg/mapper/mapperbase/hook.go: -------------------------------------------------------------------------------- 1 | package mapperbase 2 | 3 | // Hook defines a hook type that can be configured after creation. 4 | type Hook interface { 5 | SetProxyOnly(proxy bool) 6 | } 7 | 8 | type hook struct { 9 | startAddress uint16 10 | endAddress uint16 11 | 12 | onlyProxy bool // whether to continue mapper memory function execution after hook call 13 | } 14 | 15 | type readHook struct { 16 | hook 17 | 18 | hookFunc func(address uint16) uint8 19 | } 20 | 21 | type writeHook struct { 22 | hook 23 | 24 | hookFunc func(address uint16, value uint8) 25 | } 26 | 27 | func (h *hook) SetProxyOnly(proxy bool) { 28 | h.onlyProxy = proxy 29 | } 30 | 31 | // AddReadHook adds an address range read hook that gets called when a read from given range is made. 32 | func (b *Base) AddReadHook(startAddress, endAddress uint16, hookFunc func(address uint16) uint8) Hook { 33 | hook := readHook{ 34 | hook: hook{ 35 | startAddress: startAddress, 36 | endAddress: endAddress, 37 | }, 38 | hookFunc: hookFunc, 39 | } 40 | b.readHooks = append(b.readHooks, hook) 41 | return &hook.hook 42 | } 43 | 44 | // AddWriteHook adds an address range write hook that gets called when a write into the given range is made. 45 | func (b *Base) AddWriteHook(startAddress, endAddress uint16, hookFunc func(address uint16, value uint8)) Hook { 46 | hook := writeHook{ 47 | hook: hook{ 48 | startAddress: startAddress, 49 | endAddress: endAddress, 50 | }, 51 | hookFunc: hookFunc, 52 | } 53 | b.writeHooks = append(b.writeHooks, hook) 54 | return &hook.hook 55 | } 56 | -------------------------------------------------------------------------------- /pkg/mapper/mapperbase/nametable.go: -------------------------------------------------------------------------------- 1 | package mapperbase 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 7 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 8 | ) 9 | 10 | // MirrorModeTranslation maps a 8bit index to a nametable mirror mode. 11 | type MirrorModeTranslation map[uint8]cartridge.MirrorMode 12 | 13 | // createNameTableBanks creates the VRAM banks. 14 | func (b *Base) createNameTableBanks() { 15 | b.nameTableBanks = make([]bank, b.nameTableCount) 16 | 17 | for i := 0; i < b.nameTableCount; i++ { 18 | bank := &b.nameTableBanks[i] 19 | bank.length = nametable.VramSize 20 | bank.data = make([]byte, bank.length) 21 | } 22 | 23 | b.SetNameTableWindow(0) 24 | } 25 | 26 | // SetNameTableCount sets amount of nametables. 27 | func (b *Base) SetNameTableCount(count int) { 28 | b.nameTableCount = count 29 | } 30 | 31 | // SetNameTableWindow sets the nametable window to a specific bank. 32 | func (b *Base) SetNameTableWindow(bank int) { 33 | bank %= len(b.nameTableBanks) 34 | nameTable := &b.nameTableBanks[bank] 35 | b.bus.NameTable.SetVRAM(nameTable.data) 36 | } 37 | 38 | // NameTable returns the nametable buffer of a specific bank. Used in tests. 39 | func (b *Base) NameTable(bank int) []byte { 40 | bank %= len(b.nameTableBanks) 41 | nameTable := &b.nameTableBanks[bank] 42 | return nameTable.data 43 | } 44 | 45 | // SetNameTableMirrorMode sets the nametable mirror mode. 46 | func (b *Base) SetNameTableMirrorMode(mirrorMode cartridge.MirrorMode) { 47 | b.bus.NameTable.SetMirrorMode(mirrorMode) 48 | } 49 | 50 | // MirrorMode returns the set mirror mode. 51 | func (b *Base) MirrorMode() cartridge.MirrorMode { 52 | return b.bus.NameTable.MirrorMode() 53 | } 54 | 55 | // SetMirrorModeTranslation set the mirror mode translation map. 56 | func (b *Base) SetMirrorModeTranslation(translation MirrorModeTranslation) { 57 | b.mirrorModeTranslation = translation 58 | } 59 | 60 | // SetNameTableMirrorModeIndex sets the nametable mirror mode based on the value of the mapper based 61 | // translation map from index to mirror mode. 62 | func (b *Base) SetNameTableMirrorModeIndex(index uint8) { 63 | mode, ok := b.mirrorModeTranslation[index] 64 | if !ok { 65 | panic(fmt.Sprintf("invalid nametable mirror mode index %d", index)) 66 | } 67 | b.bus.NameTable.SetMirrorMode(mode) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/mapper/mapperbase/prg.go: -------------------------------------------------------------------------------- 1 | package mapperbase 2 | 3 | // setDefaultPrgBankSizes creates the banks with default lengths set based on the PRG and PRG window size. 4 | // Some mapper and modes can have a mix of sizes, for example One 16KB bank and two 8KB banks for MMC5. 5 | // In case of mixed sizing the bank sizes need to be initialized manually. 6 | func (b *Base) setDefaultPrgBankSizes() { 7 | prgSize := len(b.bus.Cartridge.PRG) 8 | banks := prgSize / b.prgWindowSize 9 | b.prgBanks = make([]bank, banks) 10 | 11 | for i := 0; i < banks; i++ { 12 | bank := &b.prgBanks[i] 13 | bank.length = b.prgWindowSize 14 | } 15 | } 16 | 17 | // setPrgBanks sets the bank data based on each bank's length. This needs to be called after the bank lengths 18 | // have been set, either by calling setDefaultPrgBankSizes() or setting it manually. 19 | func (b *Base) setPrgBanks() { 20 | prg := b.bus.Cartridge.PRG 21 | startOffset := 0 22 | 23 | for i := 0; i < len(b.prgBanks); i++ { 24 | bank := &b.prgBanks[i] 25 | endOffset := startOffset + bank.length 26 | bank.data = prg[startOffset:endOffset] 27 | startOffset += bank.length 28 | } 29 | } 30 | 31 | // PrgBankCount returns the amount of PRG banks. 32 | func (b *Base) PrgBankCount() int { 33 | return len(b.prgBanks) 34 | } 35 | 36 | // SetPrgWindow sets a PRG window to a specific bank. 37 | func (b *Base) SetPrgWindow(window, bank int) { 38 | if bank < 0 { 39 | bank = len(b.prgBanks) + bank 40 | } 41 | bank %= len(b.prgBanks) 42 | 43 | b.mu.Lock() 44 | b.prgWindows[window] = bank 45 | b.mu.Unlock() 46 | } 47 | 48 | // SetPrgWindowSize sets the PRG window size. 49 | func (b *Base) SetPrgWindowSize(size int) { 50 | b.prgWindowSize = size 51 | } 52 | 53 | // SetPrgRAM enables the usage of PRG RAM and sets the RAM buffer. 54 | func (b *Base) SetPrgRAM(ram []byte) { 55 | b.prgRAM = ram 56 | } 57 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/axrom.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "github.com/retroenv/nesgo/pkg/bus" 5 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 6 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 7 | ) 8 | 9 | /* 10 | Boards: AMROM, ANROM, AN1ROM, AOROM, others 11 | PRG ROM capacity: 256K 12 | PRG ROM window: 32K 13 | CHR capacity: 8K 14 | */ 15 | 16 | type mapperAxROM struct { 17 | Base 18 | } 19 | 20 | // NewAxROM returns a new mapper instance. 21 | func NewAxROM(base Base) bus.Mapper { 22 | m := &mapperAxROM{ 23 | Base: base, 24 | } 25 | m.SetName("AxROM") 26 | m.SetPrgWindowSize(0x8000) // 32K 27 | m.Initialize() 28 | 29 | translation := mapperbase.MirrorModeTranslation{ 30 | 0: cartridge.MirrorSingle0, 31 | 1: cartridge.MirrorSingle1, 32 | } 33 | m.SetMirrorModeTranslation(translation) 34 | 35 | m.AddWriteHook(0x8000, 0xFFFF, m.setPrgWindow) 36 | return m 37 | } 38 | 39 | func (m *mapperAxROM) setPrgWindow(address uint16, value uint8) { 40 | value &= 0b0000_0111 41 | m.SetPrgWindow(0, int(value)) // select 32 KB PRG ROM bank for CPU $8000-$FFFF 42 | 43 | mirrorMode := (value >> 4) & 1 44 | m.SetNameTableMirrorModeIndex(mirrorMode) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/axrom_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperAxROM(t *testing.T) { 14 | prg := make([]byte, 0x8000*2) 15 | 16 | base := mapperbase.New(&bus.Bus{ 17 | Cartridge: &cartridge.Cartridge{ 18 | CHR: make([]byte, 0x2000), 19 | PRG: prg, 20 | }, 21 | NameTable: nametable.New(cartridge.MirrorHorizontal), 22 | }) 23 | m := NewAxROM(base) 24 | 25 | prg[0x0010] = 0x03 // bank 0 26 | prg[0x8010] = 0x04 // bank 1 27 | assert.Equal(t, 0x03, m.Read(0x8010)) 28 | 29 | m.Write(0x8000, 1) // select bank 1 30 | assert.Equal(t, 0x04, m.Read(0x8010)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/base.go: -------------------------------------------------------------------------------- 1 | // Package mapperdb contains all mapper implementations. 2 | package mapperdb 3 | 4 | import ( 5 | "github.com/retroenv/nesgo/pkg/bus" 6 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 7 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 8 | ) 9 | 10 | // Base defines the base mapper interface that contains helper functions for shared functionality. 11 | type Base interface { 12 | bus.Mapper 13 | 14 | ChrBankCount() int 15 | SetChrRAM(ram []byte) 16 | SetChrWindow(window, bank int) 17 | SetChrWindowSize(size int) 18 | 19 | PrgBankCount() int 20 | SetPrgRAM(ram []byte) 21 | SetPrgWindow(window, bank int) 22 | SetPrgWindowSize(size int) 23 | 24 | NameTable(bank int) []byte 25 | SetMirrorModeTranslation(translation mapperbase.MirrorModeTranslation) 26 | SetNameTableCount(count int) 27 | SetNameTableMirrorMode(mirrorMode cartridge.MirrorMode) 28 | SetNameTableMirrorModeIndex(index uint8) 29 | SetNameTableWindow(bank int) 30 | 31 | AddReadHook(startAddress, endAddress uint16, hookFunc func(address uint16) uint8) mapperbase.Hook 32 | AddWriteHook(startAddress, endAddress uint16, hookFunc func(address uint16, value uint8)) mapperbase.Hook 33 | Cartridge() *cartridge.Cartridge 34 | Initialize() 35 | SetName(name string) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/cnrom.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | /* 4 | Boards: CNROM "and similar" 5 | PRG ROM capacity: 16K or 32K 6 | CHR capacity: 32K (2M oversize version) 7 | CHR window: 8K 8 | */ 9 | 10 | import ( 11 | "github.com/retroenv/nesgo/pkg/bus" 12 | ) 13 | 14 | type mapperCNROM struct { 15 | Base 16 | } 17 | 18 | // NewCNROM returns a new mapper instance. 19 | func NewCNROM(base Base) bus.Mapper { 20 | m := &mapperCNROM{ 21 | Base: base, 22 | } 23 | m.SetName("CNROM") 24 | m.Initialize() 25 | 26 | m.AddWriteHook(0x8000, 0xFFFF, m.setChrWindow) 27 | return m 28 | } 29 | 30 | func (m *mapperCNROM) setChrWindow(address uint16, value uint8) { 31 | m.SetChrWindow(0, int(value)) // select 8 KB CHR ROM bank for PPU $0000-$1FFF 32 | } 33 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/cnrom_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperCNROM(t *testing.T) { 14 | chr := make([]byte, 0x6000) 15 | 16 | base := mapperbase.New(&bus.Bus{ 17 | Cartridge: &cartridge.Cartridge{ 18 | CHR: chr, 19 | PRG: make([]byte, 0x4000), 20 | }, 21 | NameTable: nametable.New(cartridge.MirrorHorizontal), 22 | }) 23 | m := NewCNROM(base) 24 | 25 | chr[0x0010] = 0x03 // bank 0 26 | chr[0x2010] = 0x04 // bank 1 27 | chr[0x4010] = 0x05 // bank 2 28 | 29 | assert.Equal(t, 0x03, m.Read(0x0010)) 30 | 31 | m.Write(0x8000, 1) // select bank 1 32 | assert.Equal(t, 0x04, m.Read(0x0010)) 33 | 34 | m.Write(0x8000, 2) // select bank 2 35 | assert.Equal(t, 0x05, m.Read(0x0010)) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/gtrom.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | /* 4 | Boards: GTROM 5 | PRG ROM capacity: 512K 6 | PRG ROM window: 32K 7 | CHR capacity: 16K 8 | CHR window: 8K 9 | 10 | 32K CHR RAM used as two 8K CHR RAM and two 8K nametables 11 | */ 12 | 13 | import ( 14 | "github.com/retroenv/nesgo/pkg/bus" 15 | ) 16 | 17 | type mapperGTROM struct { 18 | Base 19 | } 20 | 21 | // NewGTROM returns a new mapper instance. 22 | func NewGTROM(base Base) bus.Mapper { 23 | m := &mapperGTROM{ 24 | Base: base, 25 | } 26 | m.SetName("Cheapocabra (GTROM)") 27 | m.SetPrgWindowSize(0x8000) // 32K 28 | m.SetNameTableCount(2) 29 | m.SetChrRAM(make([]byte, 0x4000)) // 16K 30 | m.Initialize() 31 | 32 | m.AddReadHook(0x5000, 0x5FFF, m.getControl) 33 | m.AddReadHook(0x7000, 0x7FFF, m.getControl) 34 | m.AddWriteHook(0x5000, 0x5FFF, m.setBanks) 35 | m.AddWriteHook(0x7000, 0x7FFF, m.setBanks) 36 | 37 | return m 38 | } 39 | 40 | func (m *mapperGTROM) getControl(address uint16) uint8 { 41 | return 0 // TODO should return open bus value 42 | } 43 | 44 | func (m *mapperGTROM) setBanks(address uint16, value uint8) { 45 | prgBank := value & 0b0000_1111 46 | 47 | m.SetPrgWindow(0, int(prgBank)) // select 32 KB PRG ROM bank for CPU $8000-$FFFF 48 | 49 | chrBank := int(value>>4) & 1 50 | m.SetChrWindow(0, chrBank) 51 | 52 | nameTableBank := int(value>>5) & 1 53 | m.SetNameTableWindow(nameTableBank) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/gtrom_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperGTROM(t *testing.T) { 14 | prg := make([]byte, 0x8000*2) 15 | 16 | nameTable := nametable.New(cartridge.Mirror4) 17 | base := mapperbase.New(&bus.Bus{ 18 | Cartridge: &cartridge.Cartridge{ 19 | PRG: prg, 20 | }, 21 | NameTable: nameTable, 22 | }) 23 | m := NewGTROM(base) 24 | 25 | chr := make([]byte, 0x4000) 26 | base.SetChrRAM(chr) 27 | base.Initialize() 28 | 29 | prg[0x7010] = 0x03 // bank 0 30 | prg[0xF010] = 0x04 // bank 1 31 | assert.Equal(t, 0x03, m.Read(0xF010)) 32 | 33 | m.Write(0x5000, 1) // select bank 1 34 | 35 | assert.Equal(t, 0x04, m.Read(0xF010)) 36 | 37 | chr[0x1010] = 0x03 // bank 0 38 | chr[0x3010] = 0x04 // bank 1 39 | assert.Equal(t, 0x03, m.Read(0x1010)) 40 | 41 | m.Write(0x5000, 1<<4) // select bank 1 42 | assert.Equal(t, 0x04, m.Read(0x1010)) 43 | 44 | data := base.NameTable(0) 45 | data[0x0100] = 0x05 // bank 0 46 | data = base.NameTable(1) 47 | data[0x0100] = 0x06 // bank 1 48 | 49 | assert.Equal(t, 0x05, nameTable.Read(0x2100)) 50 | m.Write(0x5000, 1<<5) // select bank 1 51 | assert.Equal(t, 0x06, nameTable.Read(0x2100)) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/mmc1_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperMMC1(t *testing.T) { 14 | chr := make([]byte, 0x1000*3) // 4K banks 15 | prg := make([]byte, 0x4000*3) // 16K banks 16 | 17 | base := mapperbase.New(&bus.Bus{ 18 | Cartridge: &cartridge.Cartridge{ 19 | CHR: chr, 20 | PRG: prg, 21 | }, 22 | NameTable: nametable.New(cartridge.MirrorHorizontal), 23 | }) 24 | m := NewMMC1(base) 25 | 26 | chr[0x0000] = 0x01 27 | chr[0x2000] = 0x02 28 | prg[0x0000] = 0x03 29 | prg[0x8000] = 0x04 30 | 31 | m.Write(0x8000, 0) 32 | m.Write(0x8000, 1) // select bank 2 33 | m.Write(0x8000, 0) 34 | m.Write(0x8000, 0) 35 | m.Write(0xE000, 0) // set prg bank 36 | 37 | m.Write(0x8000, 0) 38 | m.Write(0x8000, 1) // select bank 2 39 | m.Write(0x8000, 0) 40 | m.Write(0x8000, 0) 41 | m.Write(0xC000, 0) // set chr 1 bank 42 | 43 | m.Write(0x8000, 1) // mirror mode 1 44 | m.Write(0x8000, 0) 45 | m.Write(0x8000, 0) 46 | m.Write(0x8000, 1) // prg mode 2 47 | m.Write(0x9000, 1) // chr mode 1, set control 48 | 49 | assert.Equal(t, 0x01, m.Read(0x0000)) 50 | assert.Equal(t, 0x02, m.Read(0x1000)) 51 | assert.Equal(t, 0x03, m.Read(0x8000)) 52 | assert.Equal(t, 0x04, m.Read(0xC000)) 53 | 54 | mode := m.MirrorMode() 55 | assert.Equal(t, cartridge.MirrorSingle1, mode) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/nrom.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | /* 4 | Boards: NROM, HROM*, RROM, RTROM, SROM, STROM 5 | PRG ROM capacity: 16K or 32K 6 | CHR capacity: 8K 7 | */ 8 | 9 | import ( 10 | "github.com/retroenv/nesgo/pkg/bus" 11 | ) 12 | 13 | type mapperNROM struct { 14 | Base 15 | } 16 | 17 | // NewNROM returns a new mapper instance. 18 | func NewNROM(base Base) bus.Mapper { 19 | m := &mapperNROM{ 20 | Base: base, 21 | } 22 | m.SetName("NROM") 23 | m.Initialize() 24 | return m 25 | } 26 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/nrom_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperNROMPrg16k(t *testing.T) { 14 | chr := make([]byte, 0x2000) 15 | prg := make([]byte, 0x4000) 16 | 17 | base := mapperbase.New(&bus.Bus{ 18 | Cartridge: &cartridge.Cartridge{ 19 | CHR: chr, 20 | PRG: prg, 21 | }, 22 | NameTable: nametable.New(cartridge.MirrorHorizontal), 23 | }) 24 | m := NewNROM(base) 25 | 26 | chr[0x0001] = 0x02 // bank 0 27 | assert.Equal(t, 0x02, m.Read(0x0001)) 28 | 29 | prg[0x0010] = 0x03 // bank 0 30 | assert.Equal(t, 0x03, m.Read(0x8010)) 31 | assert.Equal(t, 0x03, m.Read(0xC010)) 32 | } 33 | 34 | func TestMapperNROMPrg32k(t *testing.T) { 35 | chr := make([]byte, 0x2000) 36 | prg := make([]byte, 0x8000) 37 | 38 | base := mapperbase.New(&bus.Bus{ 39 | Cartridge: &cartridge.Cartridge{ 40 | CHR: chr, 41 | PRG: prg, 42 | }, 43 | NameTable: nametable.New(cartridge.MirrorHorizontal), 44 | }) 45 | m := NewNROM(base) 46 | 47 | chr[0x0001] = 0x02 // bank 0 48 | assert.Equal(t, 0x02, m.Read(0x0001)) 49 | 50 | prg[0x0010] = 0x03 // bank 0 51 | prg[0x4010] = 0x04 // bank 1 52 | assert.Equal(t, 0x03, m.Read(0x8010)) 53 | assert.Equal(t, 0x04, m.Read(0xC010)) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/unrom512.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "github.com/retroenv/nesgo/pkg/bus" 5 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 6 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 7 | ) 8 | 9 | /* 10 | Boards: UNROM-512-8, UNROM-512-16, UNROM-512-32, INL-D-RAM, UNROM-512-F 11 | PRG ROM capacity: 256K/512K 12 | PRG ROM window: 16K + 16K fixed 13 | CHR capacity: 32K 14 | CHR window: 8K 15 | */ 16 | 17 | type mapperUNROM512 struct { 18 | Base 19 | } 20 | 21 | // NewUNROM512 returns a new mapper instance. 22 | func NewUNROM512(base Base) bus.Mapper { 23 | m := &mapperUNROM512{ 24 | Base: base, 25 | } 26 | m.SetName("UNROM 512") 27 | m.SetChrRAM(make([]byte, 0x8000)) // 32K 28 | m.Initialize() 29 | 30 | m.AddWriteHook(0x8000, 0xFFFF, m.setBanks) 31 | 32 | translation := mapperbase.MirrorModeTranslation{ 33 | 0: cartridge.MirrorHorizontal, 34 | 1: cartridge.MirrorVertical, 35 | 2: cartridge.MirrorSingle0, 36 | 3: cartridge.Mirror4, 37 | } 38 | m.SetMirrorModeTranslation(translation) 39 | 40 | cart := m.Cartridge() 41 | m.SetNameTableMirrorModeIndex(uint8(cart.Mirror)) 42 | 43 | m.SetPrgWindow(1, -1) 44 | return m 45 | } 46 | 47 | func (m *mapperUNROM512) setBanks(address uint16, value uint8) { 48 | prgBank := value & 0b0001_1111 49 | 50 | m.SetPrgWindow(0, int(prgBank)) // select 16 KB PRG ROM bank at $8000 51 | 52 | chrBank := int(value>>5) & 0b0000_0011 53 | m.SetChrWindow(0, chrBank) 54 | 55 | screen := int(value>>7) & 1 56 | if screen == 0 { 57 | m.SetNameTableMirrorMode(cartridge.MirrorSingle0) 58 | } else { 59 | m.SetNameTableMirrorMode(cartridge.MirrorSingle1) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/unrom512_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperUNROM512(t *testing.T) { 14 | prg := make([]byte, 0x8000*2) 15 | 16 | base := mapperbase.New(&bus.Bus{ 17 | Cartridge: &cartridge.Cartridge{ 18 | PRG: prg, 19 | }, 20 | NameTable: nametable.New(cartridge.MirrorHorizontal), 21 | }) 22 | m := NewUNROM512(base) 23 | 24 | chr := make([]byte, 0x8000) 25 | base.SetChrRAM(chr) 26 | base.Initialize() 27 | 28 | chr[0x1010] = 0x03 // bank 0 29 | chr[0x3010] = 0x04 // bank 1 30 | assert.Equal(t, 0x03, m.Read(0x1010)) 31 | 32 | prg[0x0010] = 0x03 // bank 0 33 | prg[0x8010] = 0x04 // bank 1 34 | assert.Equal(t, 0x03, m.Read(0x8010)) 35 | 36 | m.Write(0x8000, 0b1010_0010) // select mirror mode 1, chr bank 1, prg bank 2 37 | assert.Equal(t, 0x04, m.Read(0x8010)) 38 | 39 | assert.Equal(t, cartridge.MirrorSingle1, m.MirrorMode()) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/uxrom.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import "github.com/retroenv/nesgo/pkg/bus" 4 | 5 | /* 6 | Boards: UNROM, UOROM 7 | PRG ROM capacity: 256K/4096K 8 | PRG ROM window: 16K + 16K fixed 9 | CHR capacity: 8K 10 | */ 11 | 12 | type mapperUxROM struct { 13 | Base 14 | 15 | valueShift int 16 | windowIndex int 17 | } 18 | 19 | // NewUxROMOr returns a new mapper instance with OR logic (74HC32) configuration. 20 | func NewUxROMOr(base Base) bus.Mapper { 21 | m := newMapperUxROM(base) 22 | m.SetName("UxROM") 23 | 24 | // $8000-$BFFF: 16 KB switchable PRG ROM bank 25 | // $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank 26 | m.SetPrgWindow(1, -1) // $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank 27 | return m 28 | } 29 | 30 | // NewUN1ROM returns a new mapper instance with OR logic (74HC32) configuration and a value shifter of 2. 31 | func NewUN1ROM(base Base) bus.Mapper { 32 | m := newMapperUxROM(base) 33 | m.SetName("UN1ROM") 34 | 35 | // $8000-$BFFF: 16 KB switchable PRG ROM bank 36 | // $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank 37 | m.SetPrgWindow(1, -1) // $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank 38 | m.valueShift = 2 // very similar to UxROM, but the register is shifted by two bits 39 | return m 40 | } 41 | 42 | // NewUxROMAnd returns a new mapper instance with AND logic (74HC08) configuration. 43 | func NewUxROMAnd(base Base) bus.Mapper { 44 | m := newMapperUxROM(base) 45 | m.SetName("UxROM") 46 | 47 | // $8000-$BFFF: 16 KB PRG ROM bank, fixed to the first bank 48 | // $C000-$FFFF: 16 KB switchable PRG ROM bank 49 | m.windowIndex = 1 50 | return m 51 | } 52 | 53 | func newMapperUxROM(base Base) *mapperUxROM { 54 | m := &mapperUxROM{ 55 | Base: base, 56 | } 57 | m.Initialize() 58 | 59 | m.AddWriteHook(0x8000, 0xFFFF, m.setPrgWindow) 60 | return m 61 | } 62 | 63 | func (m *mapperUxROM) setPrgWindow(address uint16, value uint8) { 64 | value >>= m.valueShift 65 | value &= 0b0000_0111 // UNROM uses bits 2-0; UOROM/UN1ROM uses bits 3-0 66 | m.SetPrgWindow(m.windowIndex, int(value)) // select 16 KB PRG ROM bank 67 | } 68 | -------------------------------------------------------------------------------- /pkg/mapper/mapperdb/uxrom_test.go: -------------------------------------------------------------------------------- 1 | package mapperdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper/mapperbase" 8 | "github.com/retroenv/nesgo/pkg/ppu/nametable" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | "github.com/retroenv/retrogolib/assert" 11 | ) 12 | 13 | func TestMapperUxROMOr(t *testing.T) { 14 | prg := make([]byte, 0xC000) 15 | 16 | base := mapperbase.New(&bus.Bus{ 17 | Cartridge: &cartridge.Cartridge{ 18 | CHR: make([]byte, 0x2000), 19 | PRG: prg, 20 | }, 21 | NameTable: nametable.New(cartridge.MirrorHorizontal), 22 | }) 23 | m := NewUxROMOr(base) 24 | 25 | prg[0x0010] = 0x03 // bank 0 26 | prg[0x4010] = 0x04 // bank 1 27 | prg[0x8010] = 0x05 // bank 2 28 | assert.Equal(t, 0x03, m.Read(0x8010)) 29 | assert.Equal(t, 0x05, m.Read(0xC010)) 30 | 31 | m.Write(0x8000, 1) // select bank 1 32 | assert.Equal(t, 0x04, m.Read(0x8010)) 33 | } 34 | 35 | func TestMapperUxROMAnd(t *testing.T) { 36 | prg := make([]byte, 0xC000) 37 | 38 | base := mapperbase.New(&bus.Bus{ 39 | Cartridge: &cartridge.Cartridge{ 40 | CHR: make([]byte, 0x2000), 41 | PRG: prg, 42 | }, 43 | NameTable: nametable.New(cartridge.MirrorHorizontal), 44 | }) 45 | m := NewUxROMAnd(base) 46 | 47 | prg[0x0010] = 0x03 // bank 0 48 | prg[0x4010] = 0x04 // bank 1 49 | prg[0x8010] = 0x05 // bank 2 50 | assert.Equal(t, 0x03, m.Read(0x8010)) 51 | assert.Equal(t, 0x04, m.Read(0xC010)) 52 | 53 | m.Write(0x8000, 2) // select bank 2 54 | assert.Equal(t, 0x05, m.Read(0xC010)) 55 | } 56 | 57 | func TestMapperUN1ROM(t *testing.T) { 58 | prg := make([]byte, 0xC000) 59 | 60 | base := mapperbase.New(&bus.Bus{ 61 | Cartridge: &cartridge.Cartridge{ 62 | CHR: make([]byte, 0x2000), 63 | PRG: prg, 64 | }, 65 | NameTable: nametable.New(cartridge.MirrorHorizontal), 66 | }) 67 | m := NewUN1ROM(base) 68 | 69 | prg[0x0010] = 0x03 // bank 0 70 | prg[0x4010] = 0x04 // bank 1 71 | prg[0x8010] = 0x05 // bank 2 72 | assert.Equal(t, 0x03, m.Read(0x8010)) 73 | assert.Equal(t, 0x05, m.Read(0xC010)) 74 | 75 | m.Write(0x8000, 1<<2) // select bank 1 76 | assert.Equal(t, 0x04, m.Read(0x8010)) 77 | } 78 | -------------------------------------------------------------------------------- /pkg/mapper/mock.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "github.com/retroenv/nesgo/pkg/bus" 5 | "github.com/retroenv/nesgo/pkg/memory" 6 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 7 | ) 8 | 9 | // MockMapper implements a mock mapper for use in tests. 10 | type MockMapper struct { 11 | *memory.Memory 12 | } 13 | 14 | // NewMockMapper returns a new mock mapper. 15 | func NewMockMapper(bus *bus.Bus) bus.Mapper { 16 | return &MockMapper{ 17 | Memory: memory.New(bus), 18 | } 19 | } 20 | 21 | // State returns the current state of the mapper. 22 | func (m *MockMapper) State() bus.MapperState { 23 | return bus.MapperState{} 24 | } 25 | 26 | // MirrorMode returns the set mirror mode. 27 | func (m *MockMapper) MirrorMode() cartridge.MirrorMode { 28 | return cartridge.MirrorHorizontal 29 | } 30 | -------------------------------------------------------------------------------- /pkg/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/retroenv/retrogolib/addressing" 7 | "github.com/retroenv/retrogolib/assert" 8 | ) 9 | 10 | func TestMemoryImmediate(t *testing.T) { 11 | t.Parallel() 12 | m := New(nil) 13 | 14 | i := new(uint8) 15 | m.WriteAddressModes(1, i) 16 | assert.Equal(t, 1, *i) 17 | 18 | assert.Equal(t, 1, m.ReadAddressModes(true, i)) 19 | assert.Equal(t, 1, m.ReadAddressModes(true, 1)) 20 | } 21 | 22 | func TestMemoryAbsoluteInt(t *testing.T) { 23 | t.Parallel() 24 | m := New(nil) 25 | 26 | m.WriteAddressModes(1, 2) 27 | assert.Equal(t, 1, m.Read(2)) 28 | assert.Equal(t, 1, m.ReadAddressModes(false, 2)) 29 | 30 | m.WriteAddressModes(1, Absolute(3)) 31 | assert.Equal(t, 1, m.Read(2)) 32 | assert.Equal(t, 1, m.ReadAddressModes(false, Absolute(3))) 33 | } 34 | 35 | func TestMemoryAbsoluteIndirect(t *testing.T) { 36 | t.Parallel() 37 | m := New(nil) 38 | x := new(uint8) 39 | y := new(uint8) 40 | m.LinkRegisters(x, y, x, y) 41 | 42 | m.Write(3, 0x00) 43 | m.Write(4, 0x10) 44 | *x = 1 45 | m.WriteAddressModes(1, Indirect(2), x) 46 | assert.Equal(t, 1, m.Read(0x1000)) 47 | 48 | m.Write(8, 0x00) 49 | m.Write(9, 0x18) 50 | *y = 1 51 | m.WriteAddressModes(1, Indirect(8), y) 52 | assert.Equal(t, 1, m.Read(0x1800)) 53 | } 54 | 55 | func TestReadWord(t *testing.T) { 56 | m := New(nil) 57 | m.Write(0, 1) 58 | m.Write(1, 2) 59 | assert.Equal(t, 0x201, m.ReadWord(0)) 60 | } 61 | 62 | func TestReadWordBug(t *testing.T) { 63 | m := New(nil) 64 | m.Write(0x2ff, 1) 65 | m.Write(0x200, 2) 66 | assert.Equal(t, 0x201, m.ReadWordBug(0x02FF)) 67 | } 68 | 69 | func TestWriteWord(t *testing.T) { 70 | m := New(nil) 71 | m.WriteWord(0, 0x201) 72 | assert.Equal(t, 0x201, m.ReadWord(0)) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/memory/ram.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package memory 4 | 5 | import ( 6 | "sync" 7 | ) 8 | 9 | // RAM implements the Random-access memory. 10 | type RAM struct { 11 | mu sync.RWMutex // protects data 12 | offset uint16 13 | size uint16 14 | data []byte 15 | } 16 | 17 | // NewRAM returns a new ram. 18 | func NewRAM(offset, size uint16) *RAM { 19 | r := &RAM{ 20 | offset: offset, 21 | size: size, 22 | } 23 | r.Reset() 24 | return r 25 | } 26 | 27 | // Reset resets the RAM content. 28 | func (r *RAM) Reset() { 29 | r.mu.Lock() 30 | r.data = make([]byte, r.size) 31 | r.mu.Unlock() 32 | } 33 | 34 | // Read a byte from a memory address. 35 | func (r *RAM) Read(address uint16) byte { 36 | r.mu.RLock() 37 | b := r.data[address-r.offset] 38 | r.mu.RUnlock() 39 | return b 40 | } 41 | 42 | // Write a byte to a memory address. 43 | func (r *RAM) Write(address uint16, value byte) { 44 | r.mu.Lock() 45 | r.data[address-r.offset] = value 46 | r.mu.Unlock() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/nes/compiler.go: -------------------------------------------------------------------------------- 1 | package nes 2 | 3 | // Inline can be used to declare the function to be inlined by the compiler. 4 | // This should be used as last parameter in a function, as variadic parameter 5 | // so that any caller does not need to pass any extra argument, for example: 6 | // func Name(_ ...Inline) 7 | type Inline any 8 | 9 | // VariableInit is a placeholder for a variable initialization function 10 | // that nesgo uses to initialize variables on program startup. If the function 11 | // is not called from the code it will be called automatically from the first 12 | // instruction of the reset handler code. The location of this call can be 13 | // customized by placing a call to this function anywhere into the program 14 | // code. 15 | func VariableInit() {} 16 | -------------------------------------------------------------------------------- /pkg/nes/debugger/cpu.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package debugger 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | ) 9 | 10 | type cpuFlags struct { 11 | C hexByte `json:"c"` 12 | Z hexByte `json:"z"` 13 | I hexByte `json:"i"` 14 | D hexByte `json:"d"` 15 | B hexByte `json:"b"` 16 | V hexByte `json:"v"` 17 | N hexByte `json:"n"` 18 | } 19 | 20 | type cpuState struct { 21 | A hexByte `json:"a"` 22 | X hexByte `json:"x"` 23 | Y hexByte `json:"y"` 24 | PC hexWord `json:"pc"` 25 | SP hexByte `json:"sp"` 26 | Cycles hexQword `json:"cycles"` 27 | Flags cpuFlags `json:"flags"` 28 | Interrupts cpuInterrupts `json:"interrupts"` 29 | } 30 | 31 | type cpuInterrupts struct { 32 | NMI cpuInterruptState `json:"nmi"` 33 | IRQ cpuInterruptState `json:"irq"` 34 | } 35 | 36 | type cpuInterruptState struct { 37 | Running bool `json:"running"` 38 | Triggered bool `json:"triggered"` 39 | } 40 | 41 | func (d *Debugger) cpuState(w http.ResponseWriter, r *http.Request) { 42 | state := d.bus.CPU.State() 43 | 44 | res := cpuState{ 45 | A: hexByte(state.A), 46 | X: hexByte(state.X), 47 | Y: hexByte(state.Y), 48 | PC: hexWord(state.PC), 49 | SP: hexByte(state.SP), 50 | Cycles: hexQword(state.Cycles), 51 | Flags: cpuFlags{ 52 | C: hexByte(state.Flags.C), 53 | Z: hexByte(state.Flags.Z), 54 | I: hexByte(state.Flags.I), 55 | D: hexByte(state.Flags.D), 56 | B: hexByte(state.Flags.B), 57 | V: hexByte(state.Flags.V), 58 | N: hexByte(state.Flags.N), 59 | }, 60 | Interrupts: cpuInterrupts{ 61 | NMI: cpuInterruptState{ 62 | Running: state.Interrupts.NMIRunning, 63 | Triggered: state.Interrupts.NMITriggered, 64 | }, 65 | IRQ: cpuInterruptState{ 66 | Running: state.Interrupts.IrqRunning, 67 | Triggered: state.Interrupts.IrqTriggered, 68 | }, 69 | }, 70 | } 71 | 72 | _ = json.NewEncoder(w).Encode(res) 73 | } 74 | 75 | func (d *Debugger) cpuPause(w http.ResponseWriter, r *http.Request) { 76 | // TODO implement 77 | } 78 | -------------------------------------------------------------------------------- /pkg/nes/debugger/debugger.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package debugger provides a Debugger webserver. 4 | package debugger 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/retroenv/nesgo/pkg/bus" 14 | ) 15 | 16 | const defaultWebserverTimeout = 5 * time.Second 17 | 18 | // Debugger implements a Debugger webserver. 19 | type Debugger struct { 20 | bus *bus.Bus 21 | server *http.Server 22 | } 23 | 24 | // New creates a new debugger webserver. 25 | func New(listenAddress string, bus *bus.Bus) *Debugger { 26 | d := &Debugger{ 27 | bus: bus, 28 | } 29 | 30 | mux := http.NewServeMux() 31 | 32 | mux.HandleFunc("/cpu", d.cpuState) 33 | mux.HandleFunc("/cpu/pause", d.cpuPause) 34 | 35 | mux.HandleFunc("/mapper", d.mapperState) 36 | 37 | mux.HandleFunc("/ppu/palette", d.ppuPalette) 38 | mux.HandleFunc("/ppu/mirrormode", d.ppuMirrorMode) 39 | mux.HandleFunc("/ppu/nametables", d.ppuNameTables) 40 | 41 | d.server = &http.Server{ 42 | Addr: listenAddress, 43 | Handler: mux, 44 | ReadTimeout: defaultWebserverTimeout, 45 | WriteTimeout: defaultWebserverTimeout, 46 | } 47 | 48 | return d 49 | } 50 | 51 | // Start the debugger webserver, this needs to be called in a goroutine. 52 | func (d *Debugger) Start(ctx context.Context) { 53 | d.server.BaseContext = func(_ net.Listener) context.Context { 54 | return ctx 55 | } 56 | 57 | if err := d.server.ListenAndServe(); err != nil { 58 | panic(fmt.Errorf("listening on %s: %w", d.server.Addr, err)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/nes/debugger/helper.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package debugger 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // hexArray implements a byte array alias that JSON marshals to a hex array. 12 | type hexArray []byte 13 | 14 | func (h hexArray) MarshalJSON() ([]byte, error) { 15 | parts := make([]string, len(h)) 16 | for i, b := range h { 17 | parts[i] = fmt.Sprintf("%02X", b) 18 | } 19 | 20 | return json.Marshal(parts) 21 | } 22 | 23 | // hexArrayCombined implements a byte array alias that JSON marshals to a hex string. 24 | type hexArrayCombined []byte 25 | 26 | func (h hexArrayCombined) MarshalJSON() ([]byte, error) { 27 | buf := strings.Builder{} 28 | 29 | for _, b := range h { 30 | s := fmt.Sprintf("%02X", b) 31 | buf.WriteString(s) 32 | } 33 | 34 | return json.Marshal(buf.String()) 35 | } 36 | 37 | // hexByte implements byte alias that JSON marshals to a hex string. 38 | type hexByte uint8 39 | 40 | func (h hexByte) MarshalJSON() ([]byte, error) { 41 | s := fmt.Sprintf("%02X", h) 42 | return json.Marshal(s) 43 | } 44 | 45 | // hexWord implements word alias that JSON marshals to a hex string. 46 | type hexWord uint16 47 | 48 | func (h hexWord) MarshalJSON() ([]byte, error) { 49 | s := fmt.Sprintf("%04X", h) 50 | return json.Marshal(s) 51 | } 52 | 53 | // hexDword implements qword alias that JSON marshals to a hex string. 54 | type hexQword uint64 55 | 56 | func (h hexQword) MarshalJSON() ([]byte, error) { 57 | s := fmt.Sprintf("%08X", h) 58 | return json.Marshal(s) 59 | } 60 | 61 | // nolint: unparam 62 | func bytesToSliceArrayCombined(data []byte, rows, width int) []hexArrayCombined { 63 | var result []hexArrayCombined 64 | 65 | for row := 0; row < rows; row++ { 66 | offset := row * width 67 | result = append(result, data[offset:offset+width]) 68 | } 69 | 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /pkg/nes/debugger/mapper.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package debugger 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | ) 9 | 10 | func (d *Debugger) mapperState(w http.ResponseWriter, r *http.Request) { 11 | state := d.bus.Mapper.State() 12 | 13 | _ = json.NewEncoder(w).Encode(state) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/nes/debugger/ppu.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package debugger 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | ) 11 | 12 | type ppuPaletteBackground struct { 13 | Color hexArray `json:"color"` 14 | Palette0 hexArray `json:"palette0"` 15 | Palette1 hexArray `json:"palette1"` 16 | Palette2 hexArray `json:"palette2"` 17 | } 18 | 19 | type ppuPaletteSprite struct { 20 | Palette0 hexArray `json:"palette0"` 21 | Palette1 hexArray `json:"palette1"` 22 | Palette2 hexArray `json:"palette2"` 23 | Palette3 hexArray `json:"palette3"` 24 | } 25 | 26 | type ppuPalette struct { 27 | Background ppuPaletteBackground `json:"background"` 28 | Sprite ppuPaletteSprite `json:"sprite"` 29 | } 30 | 31 | func (d *Debugger) ppuPalette(w http.ResponseWriter, r *http.Request) { 32 | palette := d.bus.PPU.Palette() 33 | data := palette.Data() 34 | 35 | res := ppuPalette{ 36 | Background: ppuPaletteBackground{ 37 | Color: data[0:1], 38 | Palette0: data[1:4], 39 | Palette1: data[4:7], 40 | Palette2: data[7:10], 41 | }, 42 | Sprite: ppuPaletteSprite{ 43 | Palette0: data[10:13], 44 | Palette1: data[13:16], 45 | Palette2: data[16:19], 46 | Palette3: data[19:22], 47 | }, 48 | } 49 | 50 | _ = json.NewEncoder(w).Encode(res) 51 | } 52 | 53 | type ppuNameTables struct { 54 | NameTable0 []hexArrayCombined `json:"nametable0"` 55 | NameTable1 []hexArrayCombined `json:"nametable1"` 56 | NameTable2 []hexArrayCombined `json:"nametable2"` 57 | NameTable3 []hexArrayCombined `json:"nametable3"` 58 | } 59 | 60 | func (d *Debugger) ppuNameTables(w http.ResponseWriter, r *http.Request) { 61 | tables := d.bus.NameTable.Data() 62 | 63 | tableLen := 30 * 32 64 | res := ppuNameTables{ 65 | NameTable0: bytesToSliceArrayCombined(tables[0][:tableLen], 30, 32), 66 | NameTable1: bytesToSliceArrayCombined(tables[1][:tableLen], 30, 32), 67 | NameTable2: bytesToSliceArrayCombined(tables[2][:tableLen], 30, 32), 68 | NameTable3: bytesToSliceArrayCombined(tables[3][:tableLen], 30, 32), 69 | } 70 | 71 | _ = json.NewEncoder(w).Encode(res) 72 | } 73 | 74 | type ppuMirrorMode struct { 75 | MirrorMode cartridge.MirrorMode `json:"mirrorMode"` 76 | } 77 | 78 | func (d *Debugger) ppuMirrorMode(w http.ResponseWriter, r *http.Request) { 79 | mirrorMode := d.bus.NameTable.MirrorMode() 80 | 81 | res := ppuMirrorMode{ 82 | MirrorMode: mirrorMode, 83 | } 84 | 85 | _ = json.NewEncoder(w).Encode(res) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/nes/input.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package nes 4 | 5 | import ( 6 | "github.com/retroenv/nesgo/pkg/controller" 7 | "github.com/retroenv/retrogolib/input" 8 | ) 9 | 10 | var controllerMapping = map[input.Key]controller.Button{ 11 | input.Up: controller.Up, 12 | input.Down: controller.Down, 13 | input.Left: controller.Left, 14 | input.Right: controller.Right, 15 | input.Z: controller.A, 16 | input.X: controller.B, 17 | input.Enter: controller.Start, 18 | input.Backspace: controller.Select, 19 | } 20 | 21 | // KeyDown gets called when a key down event is registered. 22 | func (sys *System) KeyDown(key input.Key) { 23 | controllerKey, ok := controllerMapping[key] 24 | if !ok { 25 | return 26 | } 27 | sys.Bus.Controller1.SetButtonState(controllerKey, true) 28 | } 29 | 30 | // KeyUp gets called when a key up event is registered. 31 | func (sys *System) KeyUp(key input.Key) { 32 | controllerKey, ok := controllerMapping[key] 33 | if !ok { 34 | return 35 | } 36 | sys.Bus.Controller1.SetButtonState(controllerKey, false) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/nes/nogui.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package nes 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/retroenv/nesgo/pkg/ppu" 9 | "github.com/retroenv/retrogolib/gui" 10 | ) 11 | 12 | func setupNoGui(_ gui.Backend) (guiRender func() (bool, error), guiCleanup func(), err error) { 13 | render := func() (bool, error) { 14 | time.Sleep(time.Second / ppu.FPS) 15 | return true, nil 16 | } 17 | cleanup := func() {} 18 | 19 | return render, cleanup, nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/nes/option.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package nes 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/retroenv/nesgo/pkg/cpu" 9 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 10 | ) 11 | 12 | // Options contains options for the nesgo system. 13 | type Options struct { 14 | entrypoint int 15 | stopAt int 16 | 17 | debug bool 18 | debugAddress string 19 | 20 | emulator bool 21 | noGui bool 22 | cartridge *cartridge.Cartridge 23 | 24 | tracing cpu.TracingMode 25 | tracingTarget io.Writer 26 | 27 | nmiHandler func() 28 | irqHandler func() 29 | } 30 | 31 | // Option defines a Start parameter. 32 | type Option func(*Options) 33 | 34 | // NewOptions creates a new options instance from the passed options. 35 | func NewOptions(optionList ...Option) *Options { 36 | opts := &Options{ 37 | entrypoint: -1, 38 | stopAt: -1, 39 | } 40 | for _, option := range optionList { 41 | option(opts) 42 | } 43 | 44 | if opts.emulator && opts.tracing != cpu.NoTracing { 45 | opts.tracing = cpu.EmulatorTracing 46 | } 47 | 48 | return opts 49 | } 50 | 51 | // WithCartridge sets a cartridge to load. 52 | func WithCartridge(cart *cartridge.Cartridge) func(*Options) { 53 | return func(options *Options) { 54 | options.cartridge = cart 55 | } 56 | } 57 | 58 | // WithEmulator sets the emulator mode. 59 | func WithEmulator() func(*Options) { 60 | return func(options *Options) { 61 | options.emulator = true 62 | } 63 | } 64 | 65 | // WithIrqHandler sets an Irq Handler for the program. 66 | func WithIrqHandler(f func()) func(*Options) { 67 | return func(options *Options) { 68 | options.irqHandler = f 69 | } 70 | } 71 | 72 | // WithNmiHandler sets a Nmi Handler for the program. 73 | func WithNmiHandler(f func()) func(*Options) { 74 | return func(options *Options) { 75 | options.nmiHandler = f 76 | } 77 | } 78 | 79 | // WithDebug enables the debugging mode and webserver. 80 | func WithDebug(debugAddress string) func(*Options) { 81 | return func(options *Options) { 82 | options.debug = true 83 | options.debugAddress = debugAddress 84 | } 85 | } 86 | 87 | // WithTracing enables tracing for the program. 88 | func WithTracing() func(*Options) { 89 | return func(options *Options) { 90 | options.tracing = cpu.GoTracing 91 | } 92 | } 93 | 94 | // WithTracingTarget set the tracing target io writer. 95 | func WithTracingTarget(target io.Writer) func(*Options) { 96 | return func(options *Options) { 97 | options.tracing = cpu.GoTracing 98 | options.tracingTarget = target 99 | } 100 | } 101 | 102 | // WithEntrypoint enables tracing for the program. 103 | func WithEntrypoint(address int) func(*Options) { 104 | return func(options *Options) { 105 | options.entrypoint = address 106 | } 107 | } 108 | 109 | // WithStopAt stops execution of the program at a specific address. 110 | func WithStopAt(address int) func(*Options) { 111 | return func(options *Options) { 112 | options.stopAt = address 113 | } 114 | } 115 | 116 | // WithDisabledGUI disabled the GUI. 117 | func WithDisabledGUI() func(*Options) { 118 | return func(options *Options) { 119 | options.noGui = true 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/nes/start.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package nes 4 | 5 | import ( 6 | "github.com/retroenv/nesgo/pkg/nes/debugger" 7 | "github.com/retroenv/retrogolib/app" 8 | "github.com/retroenv/retrogolib/gui" 9 | ) 10 | 11 | // Start is the main entrypoint for a NES program that starts the execution. 12 | // Different options can be passed. 13 | // Following callback function that will be called by NES when different events occur: 14 | // resetHandler: called when the system gets turned on or reset 15 | // nmiHandler: occurs when the PPU starts preparing the next frame of 16 | // 17 | // graphics, 60 times per second 18 | // 19 | // irqHandler: can be triggered by the NES sound processor or from 20 | // 21 | // certain types of cartridge hardware. 22 | func Start(resetHandlerParam func(), options ...Option) { 23 | opts := NewOptions(options...) 24 | sys := NewSystem(opts) 25 | if opts.entrypoint >= 0 { 26 | sys.PC = uint16(opts.entrypoint) 27 | } 28 | 29 | sys.LinkAliases() 30 | 31 | sys.CPU.SetTracing(opts.tracing, opts.tracingTarget) 32 | 33 | if opts.emulator { 34 | sys.ResetHandler = func() { 35 | sys.runEmulatorSteps(opts.stopAt) 36 | } 37 | } else { 38 | sys.ResetHandler = resetHandlerParam 39 | sys.CPU.SetResetHandlerTraceInfo(resetHandlerParam) 40 | } 41 | 42 | ctx := app.Context() 43 | var debugServer *debugger.Debugger 44 | if opts.debug { 45 | debugServer = debugger.New(opts.debugAddress, sys.Bus) 46 | go debugServer.Start(ctx) 47 | } 48 | 49 | guiStarter := setupNoGui 50 | if gui.Setup != nil && !opts.noGui { 51 | guiStarter = gui.Setup 52 | } 53 | if err := sys.runRenderer(ctx, opts, guiStarter); err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/nes/variables.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package nes 4 | 5 | // NewInt8 creates a new int8 variable that can be used with 6 | // instructions like Sta(). This function returns a pointer to 7 | // allow the emulator Go mode to differentiate between a 8 | // passed address or variable. 9 | func NewInt8(value int8) *int8 { 10 | i := new(int8) 11 | *i = value 12 | return i 13 | } 14 | 15 | // NewUint8 creates a new uint8 variable that can be used with 16 | // instructions like Sta(). This function returns a pointer to 17 | // allow the emulator Go mode to differentiate between a 18 | // passed address or variable. 19 | func NewUint8(value uint8) *uint8 { 20 | i := new(uint8) 21 | *i = value 22 | return i 23 | } 24 | 25 | // NewUint16 creates a new uint16 variable that can be used with 26 | // instructions like Sta(). This function returns a pointer to 27 | // allow the emulator Go mode to differentiate between a 28 | // passed address or variable. 29 | func NewUint16(value uint16) *uint16 { 30 | i := new(uint16) 31 | *i = value 32 | return i 33 | } 34 | -------------------------------------------------------------------------------- /pkg/neslib/controller.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import . "github.com/retroenv/nesgo/pkg/nes" 4 | 5 | // ReadJoypad returns all joypad bits in the A register. 6 | // index defines the joypad, must be set to 0 or 1. 7 | func ReadJoypad(index uint8) { 8 | Ldy(index) 9 | Lda(1) 10 | Sta(JOYPAD1, Y) // set strobe bit 11 | Lsr(A) // now A is 0 12 | Sta(JOYPAD1, Y) // clear strobe bit 13 | Ldx(8) // read 8 bits 14 | for Bne() { // repeat while X is 0 15 | Pha() // save A (result) 16 | Lda(JOYPAD1, Y) // load controller state 17 | Lsr(A) // bit 0 -> carry 18 | Pla() // restore A (result) 19 | Rol(A) // carry -> bit 0 of result 20 | Dex() // X = X - 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/neslib/doc.go: -------------------------------------------------------------------------------- 1 | // Package neslib provides helper functions for writing NES programs in Golang. 2 | // This package needs to be imported using the dot notation. 3 | package neslib 4 | -------------------------------------------------------------------------------- /pkg/neslib/init.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import . "github.com/retroenv/nesgo/pkg/nes" 4 | 5 | // Init is the NES setup start routine. 6 | func Init(_ ...Inline) { 7 | Sei() 8 | Cld() 9 | Ldx(0xff) 10 | Txs() 11 | Inx() 12 | Stx(PPU_MASK) // disable rendering 13 | Stx(APU_DMC_CTRL) // disable DMC interrupts 14 | Stx(PPU_CTRL) // disable NMI interrupts 15 | Bit(PPU_STATUS) // clear VBL flag 16 | Bit(APU_CHAN_CTRL) // ack DMC IRQ bit 7 17 | Lda(0x40) 18 | Sta(APU_FRAME) // disable APU Frame IRQ 19 | Lda(0x0F) 20 | Sta(APU_CHAN_CTRL) // disable DMC, enable/init other channels. 21 | } 22 | 23 | // ClearRAM clears all RAM, except for last 2 bytes of CPU stack. 24 | func ClearRAM() { 25 | Lda(0) // A = 0 26 | Tax() // X = 0 27 | 28 | for { // loop 256 times 29 | Sta(0, X) // clear 0x0-0xff 30 | 31 | Cpx(0xfe) // last 2 bytes of stack? 32 | if !Bcs() { // don't clear it 33 | Sta(0x100, X) // clear 0x100-0x1fd 34 | } 35 | 36 | Sta(0x200, X) // clear 0x200-0x2ff 37 | Sta(0x300, X) // clear 0x300-0x3ff 38 | Sta(0x400, X) // clear 0x400-0x4ff 39 | Sta(0x500, X) // clear 0x500-0x5ff 40 | Sta(0x600, X) // clear 0x600-0x6ff 41 | Sta(0x700, X) // clear 0x700-0x7ff 42 | Inx() // X = X + 1 43 | 44 | if !Bne() { 45 | break 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/neslib/init_test.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/retroenv/nesgo/pkg/nes" 7 | "github.com/retroenv/retrogolib/assert" 8 | ) 9 | 10 | func TestClearRAM(t *testing.T) { 11 | sys := NewSystem(nil) 12 | sys.LinkAliases() 13 | 14 | sys.Bus.Memory.Write(0x7FF, 1) 15 | 16 | value := sys.Bus.Memory.Read(0x7FF) 17 | assert.Equal(t, 1, value) 18 | 19 | ClearRAM() 20 | 21 | value = sys.Bus.Memory.Read(0x7FF) 22 | assert.Equal(t, 0, value) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/neslib/math.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import . "github.com/retroenv/nesgo/pkg/nes" 4 | 5 | // DivSigned16 divides the signed number in A by 16. 6 | func DivSigned16(_ ...Inline) { 7 | Lsr() 8 | Lsr() 9 | Lsr() 10 | Lsr() 11 | Clc() 12 | Adc(0x78) 13 | Eor(0x78) 14 | } 15 | 16 | // DivSigned8 divides the signed number in A by 8. 17 | func DivSigned8(_ ...Inline) { 18 | Lsr() 19 | Lsr() 20 | Lsr() 21 | Clc() 22 | Adc(0x70) 23 | Eor(0x70) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/neslib/math_test.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/retroenv/nesgo/pkg/nes" 7 | "github.com/retroenv/retrogolib/assert" 8 | ) 9 | 10 | func TestDivSigned16(t *testing.T) { 11 | sys := NewSystem(nil) 12 | sys.LinkAliases() 13 | sys.ResetCycles() 14 | 15 | *A = 0b1000_0001 // -127 16 | DivSigned16() 17 | assert.Equal(t, 0b1111_1000, *A) // -8 18 | assert.Equal(t, 14, sys.Cycles()) // -16 19 | 20 | *A = 0b0010_1010 // 42 21 | DivSigned16() 22 | assert.Equal(t, 0b0000_0010, *A) // 2 23 | } 24 | 25 | func TestDivSigned8(t *testing.T) { 26 | sys := NewSystem(nil) 27 | sys.LinkAliases() 28 | sys.ResetCycles() 29 | 30 | *A = 0b1000_0001 // -127 31 | DivSigned8() 32 | assert.Equal(t, 0b1111_0000, *A) // -16 33 | assert.Equal(t, 12, sys.Cycles()) // -16 34 | 35 | *A = 0b0010_1010 // 42 36 | DivSigned8() 37 | assert.Equal(t, 0b0000_0101, *A) // 5 38 | } 39 | -------------------------------------------------------------------------------- /pkg/neslib/ppu.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import . "github.com/retroenv/nesgo/pkg/nes" 4 | 5 | // WaitSync waits for vertical sync to start. 6 | func WaitSync() { 7 | for Bpl() { 8 | Bit(PPU_STATUS) 9 | } 10 | } 11 | 12 | // StartPPUTransfer starts the PPU transfer to the passed address. 13 | func StartPPUTransfer(address uint16, _ ...Inline) { 14 | Ldx(PPU_STATUS) 15 | Ldx(uint8(address >> 8)) 16 | Stx(PPU_ADDR) 17 | Ldx(uint8(address)) 18 | Stx(PPU_ADDR) 19 | } 20 | 21 | // PPUTransfer transfers a constant to the PPU. 22 | func PPUTransfer(data any, _ ...Inline) { 23 | Lda(data) 24 | Sta(PPU_DATA) 25 | } 26 | 27 | // PPUMask sets the PPU mask. 28 | func PPUMask(flags uint8, _ ...Inline) { 29 | Lda(flags) 30 | Sta(PPU_MASK) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/neslib/rand.go: -------------------------------------------------------------------------------- 1 | package neslib 2 | 3 | import . "github.com/retroenv/nesgo/pkg/nes" 4 | 5 | // NextRandom returns a random number in A register. 6 | func NextRandom() { 7 | Lsr(A) 8 | if Bcc() { 9 | goto NoEor 10 | } 11 | Eor(0xd4) 12 | NoEor: 13 | } 14 | 15 | // PrevRandom returns a random number in A register. 16 | func PrevRandom() { 17 | Asl(A) 18 | if Bcc() { 19 | goto NoEor 20 | } 21 | Eor(0xa9) 22 | NoEor: 23 | } 24 | -------------------------------------------------------------------------------- /pkg/ppu/addressing/addressing.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package addressing handles PPU addressing of X/Y coordinates and nametables. 4 | package addressing 5 | 6 | // Addressing handles PPU addressing of X/Y coordinates and nametables. 7 | type Addressing struct { 8 | latch bool // address latch toggle for high/low byte 9 | vram register 10 | temp register 11 | } 12 | 13 | // New returns a new addressing manager. 14 | func New() *Addressing { 15 | return &Addressing{} 16 | } 17 | 18 | // SetAddress sets the address using the temp address register and the latch as switch 19 | // to differentiate between the high and low bytes. 20 | func (a *Addressing) SetAddress(value byte) { 21 | if a.latch { 22 | address := a.temp.address() & 0xFF00 23 | address |= uint16(value) 24 | a.temp.set(address) 25 | a.vram = a.temp 26 | } else { 27 | address := a.temp.address() & 0x00FF 28 | address |= uint16(value) << 8 29 | a.temp.set(address) 30 | } 31 | 32 | a.latch = !a.latch 33 | } 34 | 35 | // SetScroll sets the scroll values, X or Y depending on the latch toggle. 36 | func (a *Addressing) SetScroll(value byte) { 37 | if a.latch { 38 | a.temp.FineY = uint16(value) & 0x07 39 | a.temp.CoarseY = uint16(value) >> 3 40 | } else { 41 | a.temp.CoarseX = uint16(value) >> 3 42 | } 43 | 44 | a.latch = !a.latch 45 | } 46 | 47 | // SetTempNameTables sets the temp register nametable from the passed PPU control byte. 48 | func (a *Addressing) SetTempNameTables(nameTableX, nameTableY byte) { 49 | a.temp.NameTableX = uint16(nameTableX) 50 | a.temp.NameTableY = uint16(nameTableY) 51 | } 52 | 53 | // ClearLatch clears the address latch toggle. 54 | func (a *Addressing) ClearLatch() { 55 | a.latch = false 56 | } 57 | 58 | // Latch returns the address latch toggle. 59 | func (a *Addressing) Latch() bool { 60 | return a.latch 61 | } 62 | 63 | // Address returns the current vram address. 64 | func (a *Addressing) Address() uint16 { 65 | return a.vram.address() 66 | } 67 | 68 | // FineY returns FineY of the vram. 69 | func (a *Addressing) FineY() uint16 { 70 | return a.vram.FineY 71 | } 72 | 73 | // Increment the vram address by the given pixel count. 74 | func (a *Addressing) Increment(value byte) { 75 | a.vram.increment(value) 76 | } 77 | 78 | // IncrementX increments coarse x and wraps the nameTables horizontally. 79 | func (a *Addressing) IncrementX() { 80 | a.vram.incrementX() 81 | } 82 | 83 | // IncrementY increments fine Y, overflowing to coarse Y and wraps the nameTables vertically. 84 | func (a *Addressing) IncrementY() { 85 | a.vram.incrementY() 86 | } 87 | 88 | // CopyX copies the temp X coordinates from temp to vram register. 89 | func (a *Addressing) CopyX() { 90 | a.vram.NameTableX = a.temp.NameTableX 91 | a.vram.CoarseX = a.temp.CoarseX 92 | } 93 | 94 | // CopyY copies the temp Y coordinates from temp to vram register. 95 | func (a *Addressing) CopyY() { 96 | a.vram.NameTableY = a.temp.NameTableY 97 | a.vram.CoarseY = a.temp.CoarseY 98 | a.vram.FineY = a.temp.FineY 99 | } 100 | -------------------------------------------------------------------------------- /pkg/ppu/addressing/register.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package addressing 4 | 5 | // register represents an internal PPU register that has the address decoded to fields. 6 | // It is also known as loopy register. The data could be stored more efficient as a 7 | // single uint16 but the decomposed structure is chosen for ease of development and readability of code. 8 | type register struct { 9 | CoarseX uint16 // 0000 0000 0001 1111 10 | CoarseY uint16 // 0000 0011 1110 0000 11 | NameTableX uint16 // 0000 0100 0000 0000 12 | NameTableY uint16 // 0000 1000 0000 0000 13 | FineY uint16 // 0111 0000 0000 0000 14 | Unused uint16 // 1000 0000 0000 0000 15 | } 16 | 17 | // address returns the final address calculated from the internal fields. 18 | func (r *register) address() uint16 { 19 | address := r.CoarseX 20 | address |= r.CoarseY << 5 21 | address |= r.NameTableX << 10 22 | address |= r.NameTableY << 11 23 | address |= r.FineY << 12 24 | address |= r.Unused << 15 25 | return address 26 | } 27 | 28 | // set the internal decoded fields from an address. 29 | func (r *register) set(address uint16) { 30 | r.CoarseX = address & 0b0001_1111 31 | r.CoarseY = (address >> 5) & 0b0001_1111 32 | r.NameTableX = (address >> 10) & 1 33 | r.NameTableY = (address >> 11) & 1 34 | r.FineY = (address >> 12) & 0b0000_0111 35 | r.Unused = (address >> 15) & 1 36 | } 37 | 38 | // increment the address by the given pixel count. 39 | func (r *register) increment(value byte) { 40 | r.set(r.address() + uint16(value)) 41 | } 42 | 43 | // incrementX increments coarse x and wraps the nameTables horizontally. 44 | func (r *register) incrementX() { 45 | if r.CoarseX < 31 { 46 | r.CoarseX++ 47 | return 48 | } 49 | 50 | r.CoarseX = 0 51 | r.NameTableX ^= 1 // switch horizontal nameTable 52 | } 53 | 54 | // incrementY increments fine Y, overflowing to coarse Y and wraps the nameTables vertically. 55 | func (r *register) incrementY() { 56 | if r.FineY < 7 { 57 | r.FineY++ 58 | return 59 | } 60 | 61 | r.FineY = 0 62 | 63 | switch r.CoarseY { 64 | case 29: 65 | // row 29 is the last row of tiles in a nameTable 66 | r.CoarseY = 0 67 | r.NameTableY ^= 1 // switch vertical nameTable 68 | 69 | case 31: 70 | // coarse Y can be set out of bounds (> 29), which will cause the PPU to read the attribute data stored 71 | // there as tile data. if coarse Y is incremented from 31, it will wrap to 0, but the nameTable will not switch. 72 | r.CoarseY = 0 73 | 74 | default: 75 | r.CoarseY++ 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/ppu/addressing/register_test.go: -------------------------------------------------------------------------------- 1 | package addressing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/retrogolib/assert" 7 | ) 8 | 9 | func TestRegisterIncrementX(t *testing.T) { 10 | t.Parallel() 11 | 12 | r := ®ister{} 13 | 14 | assert.Equal(t, 0, r.CoarseX) 15 | r.incrementX() 16 | assert.Equal(t, 1, r.CoarseX) 17 | 18 | r.CoarseX = 31 19 | r.incrementX() 20 | assert.Equal(t, 0, r.CoarseX) 21 | assert.Equal(t, 1, r.NameTableX) 22 | 23 | r.CoarseX = 31 24 | r.incrementX() 25 | assert.Equal(t, 0, r.CoarseX) 26 | assert.Equal(t, 0, r.NameTableX) 27 | } 28 | 29 | func TestRegisterIncrementY(t *testing.T) { 30 | t.Parallel() 31 | 32 | r := ®ister{} 33 | 34 | assert.Equal(t, 0, r.FineY) 35 | r.incrementY() 36 | assert.Equal(t, 1, r.FineY) 37 | 38 | r.FineY = 7 39 | r.incrementY() 40 | assert.Equal(t, 0, r.FineY) 41 | assert.Equal(t, 1, r.CoarseY) 42 | assert.Equal(t, 0, r.NameTableY) 43 | 44 | r.FineY = 7 45 | r.CoarseY = 29 46 | r.incrementY() 47 | assert.Equal(t, 0, r.FineY) 48 | assert.Equal(t, 0, r.CoarseY) 49 | assert.Equal(t, 1, r.NameTableY) 50 | 51 | r.FineY = 7 52 | r.CoarseY = 31 53 | r.incrementY() 54 | assert.Equal(t, 0, r.FineY) 55 | assert.Equal(t, 0, r.CoarseY) 56 | assert.Equal(t, 1, r.NameTableY) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/ppu/colors.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package ppu 4 | 5 | import "image/color" 6 | 7 | var colors = [64]color.RGBA{ 8 | {0x58, 0x58, 0x58, 0xFF}, 9 | {0x00, 0x23, 0x7C, 0xFF}, 10 | {0x0D, 0x10, 0x99, 0xFF}, 11 | {0x30, 0x00, 0x92, 0xFF}, 12 | {0x4F, 0x00, 0x6C, 0xFF}, 13 | {0x60, 0x00, 0x35, 0xFF}, 14 | {0x5C, 0x05, 0x00, 0xFF}, 15 | {0x46, 0x18, 0x00, 0xFF}, 16 | {0x27, 0x2D, 0x00, 0xFF}, 17 | {0x09, 0x3E, 0x00, 0xFF}, 18 | {0x00, 0x45, 0x00, 0xFF}, 19 | {0x00, 0x41, 0x06, 0xFF}, 20 | {0x00, 0x35, 0x45, 0xFF}, 21 | {0x00, 0x00, 0x00, 0xFF}, 22 | {0x00, 0x00, 0x00, 0xFF}, 23 | {0x00, 0x00, 0x00, 0xFF}, 24 | {0xA1, 0xA1, 0xA1, 0xFF}, 25 | {0x0B, 0x53, 0xD7, 0xFF}, 26 | {0x33, 0x37, 0xFE, 0xFF}, 27 | {0x66, 0x21, 0xF7, 0xFF}, 28 | {0x95, 0x15, 0xBE, 0xFF}, 29 | {0xAC, 0x16, 0x6E, 0xFF}, 30 | {0xA6, 0x27, 0x21, 0xFF}, 31 | {0x86, 0x43, 0x00, 0xFF}, 32 | {0x59, 0x62, 0x00, 0xFF}, 33 | {0x2D, 0x7A, 0x00, 0xFF}, 34 | {0x0C, 0x85, 0x00, 0xFF}, 35 | {0x00, 0x7F, 0x2A, 0xFF}, 36 | {0x00, 0x6D, 0x85, 0xFF}, 37 | {0x00, 0x00, 0x00, 0xFF}, 38 | {0x00, 0x00, 0x00, 0xFF}, 39 | {0x00, 0x00, 0x00, 0xFF}, 40 | {0xFF, 0xFF, 0xFF, 0xFF}, 41 | {0x51, 0xA5, 0xFE, 0xFF}, 42 | {0x80, 0x84, 0xFE, 0xFF}, 43 | {0xBC, 0x6A, 0xFE, 0xFF}, 44 | {0xF1, 0x5B, 0xFE, 0xFF}, 45 | {0xFE, 0x5E, 0xC4, 0xFF}, 46 | {0xFE, 0x72, 0x69, 0xFF}, 47 | {0xE1, 0x93, 0x21, 0xFF}, 48 | {0xAD, 0xB6, 0x00, 0xFF}, 49 | {0x79, 0xD3, 0x00, 0xFF}, 50 | {0x51, 0xDF, 0x21, 0xFF}, 51 | {0x3A, 0xD9, 0x74, 0xFF}, 52 | {0x39, 0xC3, 0xDF, 0xFF}, 53 | {0x42, 0x42, 0x42, 0xFF}, 54 | {0x00, 0x00, 0x00, 0xFF}, 55 | {0x00, 0x00, 0x00, 0xFF}, 56 | {0xFF, 0xFF, 0xFF, 0xFF}, 57 | {0xB5, 0xD9, 0xFE, 0xFF}, 58 | {0xCA, 0xCA, 0xFE, 0xFF}, 59 | {0xE3, 0xBE, 0xFE, 0xFF}, 60 | {0xF9, 0xB8, 0xFE, 0xFF}, 61 | {0xFE, 0xBA, 0xE7, 0xFF}, 62 | {0xFE, 0xC3, 0xBC, 0xFF}, 63 | {0xF4, 0xD1, 0x99, 0xFF}, 64 | {0xDE, 0xE0, 0x86, 0xFF}, 65 | {0xC6, 0xEC, 0x87, 0xFF}, 66 | {0xB2, 0xF2, 0x9D, 0xFF}, 67 | {0xA7, 0xF0, 0xC3, 0xFF}, 68 | {0xA8, 0xE7, 0xF0, 0xFF}, 69 | {0xAC, 0xAC, 0xAC, 0xFF}, 70 | {0x00, 0x00, 0x00, 0xFF}, 71 | {0x00, 0x00, 0x00, 0xFF}, 72 | } 73 | -------------------------------------------------------------------------------- /pkg/ppu/const.go: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | import ( 4 | "github.com/retroenv/nesgo/pkg/ppu/control" 5 | "github.com/retroenv/nesgo/pkg/ppu/mask" 6 | ) 7 | 8 | const ( 9 | PPU_CTRL = 0x2000 10 | PPU_MASK = 0x2001 11 | PPU_STATUS = 0x2002 12 | OAM_ADDR = 0x2003 13 | OAM_DATA = 0x2004 14 | PPU_SCROLL = 0x2005 15 | PPU_ADDR = 0x2006 16 | PPU_DATA = 0x2007 17 | 18 | PALETTE_START = 0x3f00 19 | 20 | OAM_DMA = 0x4014 21 | 22 | // PPU_CTRL flags 23 | CTRL_NMI = control.CTRL_NMI // Execute Non-Maskable Interrupt on VBlank 24 | CTRL_MASTERSLAVE = control.CTRL_MASTERSLAVE // Master/Slave select 25 | CTRL_8x8 = control.CTRL_8x8 // Use 8x8 Sprites 26 | CTRL_8x16 = control.CTRL_8x16 // Use 8x16 Sprites 27 | CTRL_BG_0000 = control.CTRL_BG_0000 // Background Pattern Table at 0x0000 in VRAM 28 | CTRL_BG_1000 = control.CTRL_BG_1000 // Background Pattern Table at 0x1000 in VRAM 29 | CTRL_SPR_0000 = control.CTRL_SPR_0000 // Sprite Pattern Table at 0x0000 in VRAM 30 | CTRL_SPR_1000 = control.CTRL_SPR_1000 // Sprite Pattern Table at 0x1000 in VRAM 31 | CTRL_INC_1 = control.CTRL_INC_1 // Increment PPU Address by 1 (Horizontal rendering) 32 | CTRL_INC_32 = control.CTRL_INC_32 // Increment PPU Address by 32 (Vertical rendering) 33 | CTRL_NT_2000 = control.CTRL_NT_2000 // Name Table Address at 0x2000 34 | CTRL_NT_2400 = control.CTRL_NT_2400 // Name Table Address at 0x2400 35 | CTRL_NT_2800 = control.CTRL_NT_2800 // Name Table Address at 0x2800 36 | CTRL_NT_2C00 = control.CTRL_NT_2C00 // Name Table Address at 0x2C00 37 | 38 | // PPU_MASK flags 39 | MASK_COLOR = mask.MASK_COLOR // Display in Color 40 | MASK_MONO = mask.MASK_MONO // Display in Monochrome 41 | MASK_BG_CLIP = mask.MASK_BG_CLIP // Background clipped on left column 42 | MASK_SPR_CLIP = mask.MASK_SPR_CLIP // Sprites clipped on left column 43 | MASK_BG = mask.MASK_BG // Backgrounds Visible 44 | MASK_SPR = mask.MASK_SPR // Sprites Visible 45 | MASK_TINT_RED = mask.MASK_TINT_RED // Red Background 46 | MASK_TINT_BLUE = mask.MASK_TINT_BLUE // Blue Background 47 | MASK_TINT_GREEN = mask.MASK_TINT_GREEN // Green Background 48 | ) 49 | -------------------------------------------------------------------------------- /pkg/ppu/const_translation.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package ppu 4 | 5 | import . "github.com/retroenv/retrogolib/addressing" 6 | 7 | // AddressToName maps address constants from address to name. 8 | var AddressToName = map[uint16]AccessModeConstant{ 9 | PPU_CTRL: {Constant: "PPU_CTRL", Mode: WriteAccess}, 10 | PPU_MASK: {Constant: "PPU_MASK", Mode: WriteAccess}, 11 | PPU_STATUS: {Constant: "PPU_STATUS", Mode: ReadAccess}, 12 | OAM_ADDR: {Constant: "OAM_ADDR", Mode: WriteAccess}, 13 | OAM_DATA: {Constant: "OAM_DATA", Mode: ReadWriteAccess}, 14 | PPU_SCROLL: {Constant: "PPU_SCROLL", Mode: WriteAccess}, 15 | PPU_ADDR: {Constant: "PPU_ADDR", Mode: WriteAccess}, 16 | PPU_DATA: {Constant: "PPU_DATA", Mode: ReadWriteAccess}, 17 | 18 | PALETTE_START: {Constant: "PALETTE_START", Mode: ReadWriteAccess}, 19 | 20 | OAM_DMA: {Constant: "OAM_DMA", Mode: WriteAccess}, 21 | } 22 | -------------------------------------------------------------------------------- /pkg/ppu/control/const.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | const ( 4 | // PPU_CTRL flags 5 | CTRL_NMI = 0b1000_0000 // Execute Non-Maskable Interrupt on VBlank 6 | CTRL_MASTERSLAVE = 0b0100_0000 // Master/Slave select 7 | CTRL_8x8 = 0b0000_0000 // Use 8x8 Sprites 8 | CTRL_8x16 = 0b0010_0000 // Use 8x16 Sprites 9 | CTRL_BG_0000 = 0b0000_0000 // Background Pattern Table at 0x0000 in VRAM 10 | CTRL_BG_1000 = 0b0001_0000 // Background Pattern Table at 0x1000 in VRAM 11 | CTRL_SPR_0000 = 0b0000_0000 // Sprite Pattern Table at 0x0000 in VRAM 12 | CTRL_SPR_1000 = 0b0000_1000 // Sprite Pattern Table at 0x1000 in VRAM 13 | CTRL_INC_1 = 0b0000_0000 // Increment PPU Address by 1 (Horizontal rendering) 14 | CTRL_INC_32 = 0b0000_0100 // Increment PPU Address by 32 (Vertical rendering) 15 | CTRL_NT_2000 = 0b0000_0000 // Name Table Address at 0x2000 16 | CTRL_NT_2400 = 0b0000_0001 // Name Table Address at 0x2400 17 | CTRL_NT_2800 = 0b0000_0010 // Name Table Address at 0x2800 18 | CTRL_NT_2C00 = 0b0000_0011 // Name Table Address at 0x2C00 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/ppu/control/control.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package control contains the PPU control manager. 4 | package control 5 | 6 | type addressing interface { 7 | SetTempNameTables(nameTableX, nameTableY byte) 8 | } 9 | 10 | type nmi interface { 11 | SetEnabled(enabled bool) 12 | } 13 | 14 | type sprites interface { 15 | SetSpritePatternTable(address uint16) 16 | SetSpriteSize(size int) 17 | } 18 | 19 | type tiles interface { 20 | SetBackgroundPatternTable(backgroundPatternTable uint16) 21 | } 22 | 23 | // Control implements a PPU control manager. 24 | type Control struct { 25 | addressing addressing 26 | nmi nmi 27 | sprites sprites 28 | tiles tiles 29 | 30 | value byte // cached value since the fields are never modified directly 31 | 32 | BaseNameTable uint16 33 | VRAMIncrement uint8 // 0: add 1, going across; 1: add 32, going down 34 | SpritePatternTable uint16 35 | BackgroundPatternTable uint16 36 | SpriteSize uint8 // 0: 8x8 pixels; 1: 8x16 pixels 37 | MasterSlave uint8 38 | } 39 | 40 | // New returns a new mask manager. 41 | func New(addressing addressing, nmi nmi, sprites sprites, tiles tiles) *Control { 42 | return &Control{ 43 | addressing: addressing, 44 | nmi: nmi, 45 | sprites: sprites, 46 | tiles: tiles, 47 | } 48 | } 49 | 50 | // Set and extract the mask fields from given byte value. 51 | func (c *Control) Set(value byte) { 52 | c.value = value 53 | 54 | c.BaseNameTable = (uint16(value&CTRL_NT_2C00) << 10) + 0x2000 55 | 56 | increment := (value & CTRL_INC_32) >> 2 57 | if increment == 0 { 58 | c.VRAMIncrement = 1 59 | } else { 60 | c.VRAMIncrement = 32 61 | } 62 | 63 | c.SpritePatternTable = uint16(value&CTRL_SPR_1000) >> 3 64 | c.sprites.SetSpritePatternTable(c.SpritePatternTable) 65 | 66 | c.BackgroundPatternTable = uint16(value&CTRL_BG_1000) >> 4 67 | c.tiles.SetBackgroundPatternTable(c.BackgroundPatternTable) 68 | 69 | c.SpriteSize = value & CTRL_8x16 >> 5 70 | if c.SpriteSize == 0 { 71 | c.sprites.SetSpriteSize(8) 72 | } else { 73 | c.sprites.SetSpriteSize(16) 74 | } 75 | 76 | c.MasterSlave = value & CTRL_MASTERSLAVE >> 6 77 | 78 | c.nmi.SetEnabled(value&CTRL_NMI != 0) 79 | 80 | nameTableX := value & CTRL_NT_2400 81 | nameTableY := value & CTRL_NT_2800 >> 1 82 | c.addressing.SetTempNameTables(nameTableX, nameTableY) 83 | } 84 | 85 | // Value returns the control fields encoded as byte. 86 | func (c *Control) Value() byte { 87 | return c.value 88 | } 89 | -------------------------------------------------------------------------------- /pkg/ppu/mask/const.go: -------------------------------------------------------------------------------- 1 | package mask 2 | 3 | const ( 4 | // PPU_MASK flags 5 | MASK_COLOR = 0b0000_0000 // Display in Color 6 | MASK_MONO = 0b0000_0001 // Display in Monochrome 7 | MASK_BG_CLIP = 0b0000_0010 // Background clipped on left column 8 | MASK_SPR_CLIP = 0b0000_0100 // Sprites clipped on left column 9 | MASK_BG = 0b0000_1000 // Backgrounds Visible 10 | MASK_SPR = 0b0001_0000 // Sprites Visible 11 | MASK_TINT_RED = 0b0010_0000 // Red Background 12 | MASK_TINT_BLUE = 0b0100_0000 // Blue Background 13 | MASK_TINT_GREEN = 0b1000_0000 // Green Background 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/ppu/mask/mask.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package mask contains the PPU mask. 4 | package mask 5 | 6 | // Mask implements a PPU mask fields manager. 7 | type Mask struct { 8 | value byte // cached value since the fields are never modified directly 9 | 10 | Grayscale bool 11 | renderBackgroundLeft bool 12 | renderSpritesLeft bool 13 | renderBackground bool 14 | renderSprites bool 15 | EnhanceRed bool 16 | EnhanceGreen bool 17 | EnhanceBlue bool 18 | } 19 | 20 | // New returns a new mask manager. 21 | func New() *Mask { 22 | return &Mask{} 23 | } 24 | 25 | // Set and extract the mask fields from given byte value. 26 | func (m *Mask) Set(value byte) { 27 | m.value = value 28 | 29 | m.Grayscale = value&MASK_MONO != 0 30 | m.renderBackgroundLeft = value&MASK_BG_CLIP != 0 31 | m.renderSpritesLeft = value&MASK_SPR_CLIP != 0 32 | m.renderBackground = value&MASK_BG != 0 33 | m.renderSprites = value&MASK_SPR != 0 34 | m.EnhanceRed = value&MASK_TINT_RED != 0 35 | m.EnhanceGreen = value&MASK_TINT_GREEN != 0 36 | m.EnhanceBlue = value&MASK_TINT_BLUE != 0 37 | } 38 | 39 | // Value returns the mask fields encoded as byte. 40 | func (m *Mask) Value() byte { 41 | return m.value 42 | } 43 | 44 | // RenderBackground returns a flag whether the background should be rendered. 45 | func (m *Mask) RenderBackground() bool { 46 | return m.renderBackground 47 | } 48 | 49 | // RenderSprites returns a flag whether the sprites should be rendered. 50 | func (m *Mask) RenderSprites() bool { 51 | return m.renderSprites 52 | } 53 | 54 | // RenderBackgroundLeft returns a flag whether the background should be rendered in leftmost 8 pixels of screen. 55 | func (m *Mask) RenderBackgroundLeft() bool { 56 | return m.renderBackgroundLeft 57 | } 58 | 59 | // RenderSpritesLeft returns a flag whether the sprites should be rendered in leftmost 8 pixels of screen. 60 | func (m *Mask) RenderSpritesLeft() bool { 61 | return m.renderSpritesLeft 62 | } 63 | -------------------------------------------------------------------------------- /pkg/ppu/memory/memory.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package memory provides PPU memory access. 4 | package memory 5 | 6 | import ( 7 | "github.com/retroenv/nesgo/pkg/bus" 8 | ) 9 | 10 | // Memory implements PPU memory support. 11 | type Memory struct { 12 | mapper bus.Mapper 13 | nametable bus.NameTable 14 | palette bus.BasicMemory 15 | } 16 | 17 | // New returns a new memory manager. 18 | func New(mapper bus.Mapper, nametable bus.NameTable, palette bus.BasicMemory) *Memory { 19 | return &Memory{ 20 | mapper: mapper, 21 | nametable: nametable, 22 | palette: palette, 23 | } 24 | } 25 | 26 | // Read from a PPU memory address. 27 | func (m *Memory) Read(address uint16) uint8 { 28 | address &= 0x3FFF // valid addresses are $0000-$3FFF; higher addresses will be mirrored down 29 | 30 | switch { 31 | case address < 0x2000: 32 | return m.mapper.Read(address) 33 | 34 | case address < 0x3F00: 35 | return m.nametable.Read(address) 36 | 37 | default: // >= 0x3F00 38 | return m.palette.Read(address) 39 | } 40 | } 41 | 42 | // Write to a PPU memory address. 43 | func (m *Memory) Write(address uint16, value uint8) { 44 | address &= 0x3FFF // valid addresses are $0000-$3FFF; higher addresses will be mirrored down 45 | 46 | switch { 47 | case address < 0x2000: 48 | m.mapper.Write(address, value) 49 | 50 | case address < 0x3F00: 51 | m.nametable.Write(address, value) 52 | 53 | default: // >= 0x3F00 54 | m.palette.Write(address, value) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/ppu/nametable/nametable_test.go: -------------------------------------------------------------------------------- 1 | package nametable 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 7 | "github.com/retroenv/retrogolib/assert" 8 | ) 9 | 10 | func TestNameTable(t *testing.T) { 11 | t.Parallel() 12 | 13 | n := New(cartridge.MirrorHorizontal) 14 | n.SetVRAM(make([]byte, VramSize)) 15 | 16 | n.vram[0] = 1 17 | 18 | value := n.Read(0x2400) 19 | assert.Equal(t, 1, value) 20 | 21 | n.mirrorMode = cartridge.MirrorVertical 22 | value = n.Read(0x2400) 23 | assert.Equal(t, 0, value) 24 | 25 | n.Fetch(0x2000) 26 | value = n.Value() 27 | assert.Equal(t, 1, value) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/ppu/nmi/nmi.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package nmi contains the PPU NMI manager. 4 | package nmi 5 | 6 | import "github.com/retroenv/nesgo/pkg/bus" 7 | 8 | // Nmi implements a PPU NMI manager. 9 | type Nmi struct { 10 | enabled bool 11 | occurred bool 12 | } 13 | 14 | // New returns a new mask manager. 15 | func New() *Nmi { 16 | return &Nmi{} 17 | } 18 | 19 | // Occurred returns whether an NMI occurred. 20 | func (n *Nmi) Occurred() bool { 21 | return n.occurred 22 | } 23 | 24 | // SetOccurred sets the occurred flag. 25 | func (n *Nmi) SetOccurred(occurred bool) { 26 | n.occurred = occurred 27 | } 28 | 29 | // Enabled returns whether NMI triggering is enabled. 30 | func (n *Nmi) Enabled() bool { 31 | return n.enabled 32 | } 33 | 34 | // SetEnabled sets whether NMI is enabled. 35 | func (n *Nmi) SetEnabled(enabled bool) { 36 | n.enabled = enabled 37 | } 38 | 39 | // Trigger an NMI if it occurred and is enabled. 40 | func (n *Nmi) Trigger(cpu bus.CPU) { 41 | if n.enabled && n.occurred { 42 | cpu.TriggerNMI() 43 | n.occurred = false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/ppu/palette/palette.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package palette handles PPU palette support. 4 | package palette 5 | 6 | import "sync" 7 | 8 | const size = 32 9 | 10 | // Palette implements PPU palette support. 11 | type Palette struct { 12 | mu sync.RWMutex 13 | data [size]byte // contains color indexes 14 | } 15 | 16 | // New returns a new palette manager. 17 | func New() *Palette { 18 | return &Palette{} 19 | } 20 | 21 | // Read a value from the palette address. 22 | func (p *Palette) Read(address uint16) byte { 23 | base := mirroredPaletteAddressToBase(address) 24 | p.mu.RLock() 25 | value := p.data[base] 26 | p.mu.RUnlock() 27 | return value 28 | } 29 | 30 | // Write a value to a palette address. 31 | func (p *Palette) Write(address uint16, value byte) { 32 | base := mirroredPaletteAddressToBase(address) 33 | p.mu.Lock() 34 | p.data[base] = value 35 | p.mu.Unlock() 36 | } 37 | 38 | // Data returns the palette data as byte array. 39 | func (p *Palette) Data() [32]byte { 40 | p.mu.RLock() 41 | data := p.data 42 | p.mu.RUnlock() 43 | return data 44 | } 45 | 46 | func mirroredPaletteAddressToBase(address uint16) uint16 { 47 | // $3F20-$3FFF are mirrors of $3F00-$3F1F 48 | address %= size 49 | 50 | // $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C 51 | if address >= 0x10 && address%4 == 0 { 52 | address -= 0x10 53 | } 54 | return address 55 | } 56 | -------------------------------------------------------------------------------- /pkg/ppu/palette/palette_test.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package palette 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/retroenv/retrogolib/assert" 9 | ) 10 | 11 | func TestPalette(t *testing.T) { 12 | t.Parallel() 13 | 14 | p := &Palette{} 15 | p.Write(0, 1) 16 | value := p.Read(0) 17 | assert.Equal(t, 1, value) 18 | 19 | p.Write(0x21, 1) 20 | value = p.Read(1) 21 | assert.Equal(t, 1, value) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/ppu/ppu.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package ppu provides PPU (Picture Processing Unit) functionality. 4 | package ppu 5 | 6 | import ( 7 | "github.com/retroenv/nesgo/pkg/bus" 8 | "github.com/retroenv/nesgo/pkg/ppu/addressing" 9 | "github.com/retroenv/nesgo/pkg/ppu/control" 10 | "github.com/retroenv/nesgo/pkg/ppu/mask" 11 | "github.com/retroenv/nesgo/pkg/ppu/memory" 12 | "github.com/retroenv/nesgo/pkg/ppu/nmi" 13 | "github.com/retroenv/nesgo/pkg/ppu/palette" 14 | "github.com/retroenv/nesgo/pkg/ppu/renderstate" 15 | "github.com/retroenv/nesgo/pkg/ppu/screen" 16 | "github.com/retroenv/nesgo/pkg/ppu/sprites" 17 | "github.com/retroenv/nesgo/pkg/ppu/status" 18 | "github.com/retroenv/nesgo/pkg/ppu/tiles" 19 | ) 20 | 21 | const ( 22 | FPS = 60 23 | Height = screen.Height 24 | Width = screen.Width 25 | ) 26 | 27 | // PPU represents the Picture Processing Unit. 28 | type PPU struct { 29 | bus *bus.Bus 30 | 31 | fineX uint16 32 | dataReadBuffer byte 33 | 34 | addressing *addressing.Addressing 35 | control *control.Control 36 | mask *mask.Mask 37 | memory *memory.Memory 38 | nmi *nmi.Nmi 39 | palette *palette.Palette 40 | renderState *renderstate.RenderState 41 | screen *screen.Screen 42 | sprites *sprites.Sprites 43 | status *status.Status 44 | tiles *tiles.Tiles 45 | } 46 | 47 | // New returns a new PPU. 48 | func New(bus *bus.Bus) *PPU { 49 | p := &PPU{ 50 | bus: bus, 51 | } 52 | p.reset() 53 | return p 54 | } 55 | 56 | func (p *PPU) reset() { 57 | p.fineX = 0 58 | p.dataReadBuffer = 0 59 | 60 | p.addressing = addressing.New() 61 | p.mask = mask.New() 62 | p.nmi = nmi.New() 63 | p.palette = palette.New() 64 | p.renderState = renderstate.New() 65 | p.screen = screen.New() 66 | p.status = status.New() 67 | 68 | p.memory = memory.New(p.bus.Mapper, p.bus.NameTable, p.palette) 69 | p.sprites = sprites.New(p.bus.CPU, p.bus.Mapper, p.bus.Memory, p.renderState, p.status) 70 | 71 | p.tiles = tiles.New(p.addressing, p.memory, p.bus.NameTable) 72 | 73 | p.control = control.New(p.addressing, p.nmi, p.sprites, p.tiles) 74 | } 75 | 76 | func (p *PPU) readData() byte { 77 | address := p.addressing.Address() 78 | address &= 0x3FFF // valid addresses are $0000-$3FFF; higher addresses will be mirrored down 79 | 80 | // when reading data, the contents of an internal read buffer is returned and the buffer 81 | // gets updated with the newly read data 82 | data := p.dataReadBuffer 83 | 84 | p.dataReadBuffer = p.memory.Read(address) 85 | 86 | if address >= 0x3F00 { 87 | // Palette data reads are unbuffered, $3F00-$3FFF are Palette RAM indexes and mirrors of it 88 | data = p.dataReadBuffer 89 | } 90 | 91 | // TODO handle special case of reading during rendering 92 | p.addressing.Increment(p.control.VRAMIncrement) 93 | return data 94 | } 95 | 96 | func (p *PPU) getStatus() byte { 97 | p.addressing.ClearLatch() 98 | 99 | p.status.SetVerticalBlank(p.nmi.Occurred()) 100 | p.nmi.SetOccurred(false) 101 | 102 | value := p.status.Value() 103 | return value 104 | } 105 | 106 | // Palette returns the palette. 107 | func (p *PPU) Palette() bus.Palette { 108 | return p.palette 109 | } 110 | -------------------------------------------------------------------------------- /pkg/ppu/ppu_test.go: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/retroenv/nesgo/pkg/bus" 7 | "github.com/retroenv/nesgo/pkg/mapper" 8 | "github.com/retroenv/retrogolib/arch/nes/cartridge" 9 | "github.com/retroenv/retrogolib/assert" 10 | ) 11 | 12 | // TestSetControl verifies that the control byte gets handled correctly. 13 | func TestSetControl(t *testing.T) { 14 | t.Parallel() 15 | 16 | sys := &bus.Bus{ 17 | Cartridge: cartridge.New(), 18 | } 19 | sys.Mapper = mapper.NewMockMapper(sys) 20 | p := New(sys) 21 | 22 | p.Write(PPU_CTRL, 0b1111_1111) 23 | 24 | assert.Equal(t, 0x2C00, p.control.BaseNameTable) 25 | assert.Equal(t, 32, p.control.VRAMIncrement) 26 | assert.Equal(t, 0x01, p.control.SpritePatternTable) 27 | assert.Equal(t, 0x01, p.control.BackgroundPatternTable) 28 | assert.Equal(t, 0x01, p.control.SpriteSize) 29 | assert.Equal(t, 0x01, p.control.MasterSlave) 30 | assert.True(t, p.nmi.Enabled()) 31 | } 32 | 33 | // TestSetMask verifies that the mask byte gets handled correctly. 34 | func TestSetMask(t *testing.T) { 35 | t.Parallel() 36 | 37 | sys := &bus.Bus{ 38 | Cartridge: cartridge.New(), 39 | } 40 | sys.Mapper = mapper.NewMockMapper(sys) 41 | p := New(sys) 42 | 43 | p.Write(PPU_MASK, 0b1111_1111) 44 | 45 | assert.True(t, p.mask.Grayscale) 46 | assert.True(t, p.mask.RenderBackgroundLeft()) 47 | assert.True(t, p.mask.RenderSpritesLeft()) 48 | assert.True(t, p.mask.RenderBackground()) 49 | assert.True(t, p.mask.RenderSprites()) 50 | assert.True(t, p.mask.EnhanceRed) 51 | assert.True(t, p.mask.EnhanceGreen) 52 | assert.True(t, p.mask.EnhanceBlue) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/ppu/register.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package ppu 4 | 5 | import "fmt" 6 | 7 | // Read from a PPU memory register address. 8 | func (p *PPU) Read(address uint16) uint8 { 9 | base := mirroredRegisterAddressToBase(address) 10 | 11 | switch base { 12 | case PPU_CTRL: 13 | return p.control.Value() 14 | 15 | case PPU_MASK: 16 | return p.mask.Value() 17 | 18 | case PPU_STATUS: 19 | return p.getStatus() 20 | 21 | case OAM_DATA: 22 | return p.sprites.Read() 23 | 24 | case PPU_DATA: 25 | return p.readData() 26 | 27 | default: 28 | panic(fmt.Sprintf("unhandled ppu read at address: 0x%04X", address)) 29 | } 30 | } 31 | 32 | // Write to a PPU memory register address. 33 | func (p *PPU) Write(address uint16, value uint8) { 34 | base := mirroredRegisterAddressToBase(address) 35 | 36 | switch base { 37 | case PPU_CTRL: 38 | p.control.Set(value) 39 | 40 | case PPU_MASK: 41 | p.mask.Set(value) 42 | 43 | case OAM_ADDR: 44 | p.sprites.SetAddress(value) 45 | 46 | case OAM_DATA: 47 | p.sprites.Write(value) 48 | 49 | case PPU_SCROLL: 50 | if !p.addressing.Latch() { 51 | p.fineX = uint16(value) & 0x07 52 | } 53 | p.addressing.SetScroll(value) 54 | 55 | case PPU_ADDR: 56 | p.addressing.SetAddress(value) 57 | 58 | case PPU_DATA: 59 | address := p.addressing.Address() 60 | p.memory.Write(address, value) 61 | p.addressing.Increment(p.control.VRAMIncrement) 62 | 63 | case OAM_DMA: 64 | p.sprites.WriteDMA(value) 65 | 66 | default: 67 | panic(fmt.Sprintf("unhandled ppu write at address: 0x%04X", address)) 68 | } 69 | } 70 | 71 | // mirroredRegisterAddressToBase converts the mirrored addresses to the base address. 72 | // PPU registers are mirrored in every 8 bytes from $2008 through $3FFF. 73 | func mirroredRegisterAddressToBase(address uint16) uint16 { 74 | if address == OAM_DMA { 75 | return address 76 | } 77 | 78 | base := 0x2000 + address&0b0000_0111 79 | return base 80 | } 81 | -------------------------------------------------------------------------------- /pkg/ppu/renderstate/renderstate.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package renderstate handles PPU render handling of cycles, scan lines and frames. 4 | package renderstate 5 | 6 | type mask interface { 7 | RenderBackground() bool 8 | RenderSprites() bool 9 | } 10 | 11 | // RenderState implements a PPU render state manager. 12 | type RenderState struct { 13 | cycle int // 0-340, 0=idle,1-336=tile data fetching,337-340=nameTable fetching 14 | scanLine int // 0-261, 0-239=visible, 240=post-render, 241-260=vertical blank, 261=pre-render 15 | frame uint64 16 | } 17 | 18 | // New returns a new render state manager. 19 | func New() *RenderState { 20 | return &RenderState{ 21 | cycle: 340, 22 | scanLine: 240, 23 | } 24 | } 25 | 26 | // Tick updates cycle, scanLine and frame counters. 27 | func (r *RenderState) Tick(mask mask) { 28 | if mask.RenderBackground() || mask.RenderSprites() { 29 | // for odd frames, the cycle at the end of the scanline is skipped 30 | if r.scanLine == 261 && r.cycle == 339 && r.frame%2 == 1 { 31 | r.nextFrame() 32 | return 33 | } 34 | } 35 | 36 | r.cycle++ 37 | if r.cycle <= 340 { 38 | return 39 | } 40 | r.cycle = 0 41 | 42 | r.scanLine++ 43 | if r.scanLine <= 261 { 44 | return 45 | } 46 | r.nextFrame() 47 | } 48 | 49 | func (r *RenderState) nextFrame() { 50 | r.cycle = 0 51 | r.scanLine = 0 52 | r.frame++ 53 | } 54 | 55 | // Cycle returns the current cycle, possible values are 0-340. 56 | func (r *RenderState) Cycle() int { 57 | return r.cycle 58 | } 59 | 60 | // ScanLine returns the current scanline, possible values are 0-261. 61 | func (r *RenderState) ScanLine() int { 62 | return r.scanLine 63 | } 64 | -------------------------------------------------------------------------------- /pkg/ppu/screen/screen.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package screen handles PPU screen drawing. 4 | package screen 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | ) 10 | 11 | const ( 12 | Width = 256 13 | Height = 240 14 | ) 15 | 16 | // Screen implements PPU screen drawing support. 17 | type Screen struct { 18 | back *image.RGBA // rendering in progress image 19 | front *image.RGBA // currently visible image 20 | } 21 | 22 | // New returns a new screen manager. 23 | func New() *Screen { 24 | return &Screen{ 25 | back: image.NewRGBA(image.Rect(0, 0, Width, Height)), 26 | front: image.NewRGBA(image.Rect(0, 0, Width, Height)), 27 | } 28 | } 29 | 30 | // SetPixel sets a pixel in the rendering image. 31 | func (s *Screen) SetPixel(x, y int, color color.RGBA) { 32 | s.back.SetRGBA(x, y, color) 33 | } 34 | 35 | // Image returns the rendered image to display. 36 | func (s *Screen) Image() *image.RGBA { 37 | return s.front 38 | } 39 | 40 | // FinishRendering finishes rendering by switching the visible image with the rendered one. 41 | func (s *Screen) FinishRendering() { 42 | s.front, s.back = s.back, s.front 43 | } 44 | -------------------------------------------------------------------------------- /pkg/ppu/sprites/sprite.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | package sprites 4 | 5 | // Sprite defines a sprite that can be drawn on screen and moved. 6 | type Sprite struct { 7 | y byte // y position of top of sprite, sprite data is delayed by one scanline 8 | index byte // Tile index number 9 | attributes byte 10 | x byte // x position of left side of sprite. 11 | } 12 | 13 | func (s *Sprite) field(index byte) byte { 14 | switch index { 15 | case 0: 16 | return s.y 17 | 18 | case 1: 19 | return s.index 20 | 21 | case 2: 22 | return s.attributes 23 | 24 | default: 25 | return s.x 26 | } 27 | } 28 | 29 | func (s *Sprite) setField(index, value byte) { 30 | switch index { 31 | case 0: 32 | s.y = value 33 | 34 | case 1: 35 | s.index = value 36 | 37 | case 2: 38 | s.attributes = value 39 | 40 | default: 41 | s.x = value 42 | } 43 | } 44 | 45 | // priority returns whether the sprite is drawn in front of the background. 46 | func (s *Sprite) priority() bool { 47 | priority := (s.attributes >> 5) & 1 48 | return priority == 0 49 | } 50 | 51 | func (s *Sprite) flipHorizontally() bool { 52 | flip := (s.attributes >> 6) & 1 53 | return flip == 1 54 | } 55 | 56 | func (s *Sprite) flipVertically() bool { 57 | flip := (s.attributes >> 7) & 1 58 | return flip == 1 59 | } 60 | -------------------------------------------------------------------------------- /pkg/ppu/status/status.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package status handles PPU status fields. 4 | package status 5 | 6 | // Status implements a PPU status fields manager. 7 | type Status struct { 8 | openBus byte // 0001 1111 9 | spriteOverflow bool // 0010 0000 10 | spriteZeroHit bool // 0100 0000 11 | verticalBlank bool // 1000 0000 12 | } 13 | 14 | // New returns a new status manager. 15 | func New() *Status { 16 | return &Status{} 17 | } 18 | 19 | // Value returns the status fields encoded as byte. 20 | func (s *Status) Value() byte { 21 | value := s.openBus // TODO implement support for open bus value reading 22 | if s.spriteOverflow { 23 | value |= 1 << 5 24 | } 25 | if s.spriteZeroHit { 26 | value |= 1 << 6 27 | } 28 | if s.verticalBlank { 29 | value |= 1 << 7 30 | } 31 | return value 32 | } 33 | 34 | // SetSpriteOverflow sets the sprite overflow flag. 35 | func (s *Status) SetSpriteOverflow(value bool) { 36 | s.spriteOverflow = value 37 | } 38 | 39 | // SetSpriteZeroHit sets the sprite zero hit flag. 40 | func (s *Status) SetSpriteZeroHit(value bool) { 41 | s.spriteZeroHit = value 42 | } 43 | 44 | // SetVerticalBlank sets the vertical blank flag. 45 | func (s *Status) SetVerticalBlank(value bool) { 46 | s.verticalBlank = value 47 | } 48 | -------------------------------------------------------------------------------- /pkg/ppu/tiles/tiles.go: -------------------------------------------------------------------------------- 1 | //go:build !nesgo 2 | 3 | // Package tiles handles PPU tiles support. 4 | package tiles 5 | 6 | import ( 7 | "github.com/retroenv/nesgo/pkg/bus" 8 | ) 9 | 10 | type addressing interface { 11 | Address() uint16 12 | FineY() uint16 13 | } 14 | 15 | // Tiles implements PPU tiles support. 16 | type Tiles struct { 17 | addressing addressing 18 | memory bus.BasicMemory 19 | nameTable bus.NameTable 20 | 21 | attribute byte 22 | backgroundPatternTable uint16 23 | lowByte byte 24 | highByte byte 25 | data uint64 26 | } 27 | 28 | // New returns a new tiles manager. 29 | func New(addressing addressing, memory bus.BasicMemory, nameTable bus.NameTable) *Tiles { 30 | return &Tiles{ 31 | addressing: addressing, 32 | memory: memory, 33 | nameTable: nameTable, 34 | } 35 | } 36 | 37 | // FetchCycle runs a fetch cycle for tile data. Based on the current cycle, different data is fetched. 38 | func (t *Tiles) FetchCycle(cycle int) { 39 | t.data <<= 4 40 | 41 | switch cycle % 8 { 42 | case 0: 43 | t.storeTileData() 44 | 45 | case 1: 46 | t.nameTable.Fetch(t.addressing.Address()) 47 | 48 | case 3: 49 | t.fetchAttributeTableByte() 50 | 51 | case 5: 52 | address := t.tileAddress() 53 | t.lowByte = t.memory.Read(address) 54 | 55 | case 7: 56 | address := t.tileAddress() 57 | t.highByte = t.memory.Read(address + 8) 58 | } 59 | } 60 | 61 | // SetBackgroundPatternTable sets the temp register nametable from the passed PPU control byte. 62 | func (t *Tiles) SetBackgroundPatternTable(table uint16) { 63 | t.backgroundPatternTable = table * 0x1000 64 | } 65 | 66 | // BackgroundPixel returns the background pixel for the given X coordinate. 67 | func (t *Tiles) BackgroundPixel(fineX uint16) byte { 68 | data := uint32(t.data >> 32) 69 | shift := (7 - fineX) * 4 70 | data >>= shift 71 | data &= 0x0F 72 | return byte(data) 73 | } 74 | 75 | func (t *Tiles) fetchAttributeTableByte() { 76 | address := t.addressing.Address() 77 | shift := ((address >> 4) & 4) | (address & 2) 78 | address = 0x23C0 | (address & 0x0C00) | ((address >> 4) & 0x38) | ((address >> 2) & 0x07) 79 | 80 | value := t.memory.Read(address) 81 | t.attribute = ((value >> shift) & 3) << 2 82 | } 83 | 84 | func (t *Tiles) storeTileData() { 85 | var data uint32 86 | for i := 0; i < 8; i++ { 87 | a := t.attribute 88 | p1 := (t.lowByte & 0x80) >> 7 89 | p2 := (t.highByte & 0x80) >> 6 90 | t.lowByte <<= 1 91 | t.highByte <<= 1 92 | data <<= 4 93 | data |= uint32(a | p1 | p2) 94 | } 95 | t.data |= uint64(data) 96 | } 97 | 98 | func (t *Tiles) tileAddress() uint16 { 99 | tile := t.nameTable.Value() 100 | address := t.backgroundPatternTable + uint16(tile)*16 + t.addressing.FineY() 101 | return address 102 | } 103 | --------------------------------------------------------------------------------