├── drivers ├── cartfs │ ├── testdata │ │ ├── hello.txt │ │ ├── i │ │ │ ├── j │ │ │ │ └── k │ │ │ │ │ └── k8s.txt │ │ │ └── i18n.txt │ │ ├── glass.txt │ │ ├── ken.txt │ │ ├── .hidden │ │ │ ├── more │ │ │ │ └── tip.txt │ │ │ ├── .more │ │ │ │ └── tip.txt │ │ │ ├── _more │ │ │ │ └── tip.txt │ │ │ └── fortune.txt │ │ ├── _hidden │ │ │ └── fortune.txt │ │ ├── -not-hidden │ │ │ └── fortune.txt │ │ └── ascii.txt │ ├── concurrency.txt │ ├── fs_other.go │ ├── cartfs_test.go │ ├── fs_n64.go │ └── file.go ├── rspq │ ├── rsp_crash.ucode │ ├── rsp_queue.ucode │ ├── mixer │ │ ├── rsp_mixer.ucode │ │ ├── testdata │ │ │ ├── sfx_alarm_loop3.pcm_s16be │ │ │ ├── sfx_wpn_cannon2.pcm_s16be │ │ │ └── sfx_wpn_machinegun_loop1.pcm_s16be │ │ ├── fixed.go │ │ └── mixer_test.go │ ├── testdata │ │ └── rsp_vec.ucode │ ├── commands.go │ ├── rspq_test.go │ ├── state.go │ └── vec_test.go ├── draw │ ├── testdata │ │ ├── n64.png │ │ ├── n64_s.png │ │ └── gradient.png │ ├── cpu.go │ └── benchmark_test.go ├── controller │ ├── pakfs │ │ ├── testdata │ │ │ └── clktmr.mpk │ │ ├── dir.go │ │ └── charmap.go │ ├── poll.go │ ├── joybus_test.go │ └── controller.go ├── drivers.go ├── carts │ ├── carts.go │ ├── everdrive64 │ │ ├── regs.go │ │ └── usb.go │ ├── summercart64 │ │ ├── config.go │ │ ├── usb_test.go │ │ ├── usb.go │ │ └── regs.go │ └── isviewer │ │ └── isviewer.go ├── display │ └── display.go └── console │ └── console.go ├── tools ├── rom │ ├── ipl3_compat.z64 │ ├── uf2.go │ └── main.go ├── pakfs │ ├── fuse_stub.go │ ├── main.go │ └── fuse.go ├── toolexec │ ├── embed.go │ ├── archive.go │ └── cartfs.go ├── ucode │ └── main.go ├── n64go │ └── main.go └── texture │ └── main.go ├── fonts ├── gomono12 │ ├── 0000_00ff.pos │ ├── 0000_00ff.tex │ ├── 2000_26ff.pos │ ├── 2000_26ff.tex │ └── subfonts.go ├── basicfont12 │ ├── 0000_00ff.pos │ ├── 0000_00ff.tex │ └── subfonts.go ├── goregular12 │ ├── 0000_00ff.pos │ ├── 0000_00ff.tex │ ├── 2000_26ff.pos │ ├── 2000_26ff.tex │ └── subfonts.go ├── gen.go ├── benchmark_test.go ├── face.go └── subfont.go ├── go.env ├── .github └── workflows │ ├── ares_headless.sh │ └── ci.yml ├── machine ├── init.go ├── exception.s ├── machine.go ├── asm_mips64.h ├── exception.go ├── syswriter.go └── rt0.s ├── debug ├── assert.go └── assert_release.go ├── rcp ├── cpu │ ├── cache_stub.s │ ├── cache_mips64.s │ ├── const.go │ ├── pinner.go │ └── cache_test.go ├── doc.go ├── fixed │ ├── int11_5_fixed.go │ ├── int6_10_fixed.go │ ├── uint14_2_fixed.go │ ├── integer.go │ ├── mkfixed.go │ └── fixed.go ├── rsp │ ├── rsp.go │ ├── ucode │ │ └── ucode.go │ ├── dma.go │ ├── rsp_test.go │ └── regs.go ├── periph │ ├── regs.go │ ├── device.go │ ├── mmio.go │ └── dma.go ├── serial │ ├── regs.go │ └── command.go ├── rdp │ ├── rdp_test.go │ ├── combiner.go │ └── regs.go ├── tasker_test.go ├── mmio_test.go ├── interrupt.go ├── video │ └── interrupt.go ├── regs.go ├── mmio.go ├── texture │ └── fileformat.go ├── syscall_test.go └── sync.go ├── go.mod ├── LICENSE ├── testing └── testmain.go ├── README.md └── go.sum /drivers/cartfs/testdata/hello.txt: -------------------------------------------------------------------------------- 1 | hello, world 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/i/j/k/k8s.txt: -------------------------------------------------------------------------------- 1 | kubernetes 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/i/i18n.txt: -------------------------------------------------------------------------------- 1 | internationalization 2 | -------------------------------------------------------------------------------- /drivers/cartfs/concurrency.txt: -------------------------------------------------------------------------------- 1 | Concurrency is not parallelism. 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/glass.txt: -------------------------------------------------------------------------------- 1 | I can eat glass and it doesn't hurt me. 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/ken.txt: -------------------------------------------------------------------------------- 1 | If a program is too slow, it must have a loop. 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/.hidden/more/tip.txt: -------------------------------------------------------------------------------- 1 | #define struct union /* Great space saver */ 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/.hidden/.more/tip.txt: -------------------------------------------------------------------------------- 1 | #define struct union /* Great space saver */ 2 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/.hidden/_more/tip.txt: -------------------------------------------------------------------------------- 1 | #define struct union /* Great space saver */ 2 | -------------------------------------------------------------------------------- /tools/rom/ipl3_compat.z64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/tools/rom/ipl3_compat.z64 -------------------------------------------------------------------------------- /drivers/rspq/rsp_crash.ucode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/rsp_crash.ucode -------------------------------------------------------------------------------- /drivers/rspq/rsp_queue.ucode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/rsp_queue.ucode -------------------------------------------------------------------------------- /fonts/gomono12/0000_00ff.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/gomono12/0000_00ff.pos -------------------------------------------------------------------------------- /fonts/gomono12/0000_00ff.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/gomono12/0000_00ff.tex -------------------------------------------------------------------------------- /fonts/gomono12/2000_26ff.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/gomono12/2000_26ff.pos -------------------------------------------------------------------------------- /fonts/gomono12/2000_26ff.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/gomono12/2000_26ff.tex -------------------------------------------------------------------------------- /drivers/draw/testdata/n64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/draw/testdata/n64.png -------------------------------------------------------------------------------- /drivers/draw/testdata/n64_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/draw/testdata/n64_s.png -------------------------------------------------------------------------------- /fonts/basicfont12/0000_00ff.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/basicfont12/0000_00ff.pos -------------------------------------------------------------------------------- /fonts/basicfont12/0000_00ff.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/basicfont12/0000_00ff.tex -------------------------------------------------------------------------------- /fonts/goregular12/0000_00ff.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/goregular12/0000_00ff.pos -------------------------------------------------------------------------------- /fonts/goregular12/0000_00ff.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/goregular12/0000_00ff.tex -------------------------------------------------------------------------------- /fonts/goregular12/2000_26ff.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/goregular12/2000_26ff.pos -------------------------------------------------------------------------------- /fonts/goregular12/2000_26ff.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/fonts/goregular12/2000_26ff.tex -------------------------------------------------------------------------------- /drivers/cartfs/testdata/.hidden/fortune.txt: -------------------------------------------------------------------------------- 1 | WARNING: terminal is not fully functional 2 | - (press RETURN) 3 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/_hidden/fortune.txt: -------------------------------------------------------------------------------- 1 | WARNING: terminal is not fully functional 2 | - (press RETURN) 3 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/-not-hidden/fortune.txt: -------------------------------------------------------------------------------- 1 | WARNING: terminal is not fully functional 2 | - (press RETURN) 3 | -------------------------------------------------------------------------------- /drivers/draw/testdata/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/draw/testdata/gradient.png -------------------------------------------------------------------------------- /drivers/rspq/mixer/rsp_mixer.ucode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/mixer/rsp_mixer.ucode -------------------------------------------------------------------------------- /drivers/rspq/testdata/rsp_vec.ucode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/testdata/rsp_vec.ucode -------------------------------------------------------------------------------- /drivers/controller/pakfs/testdata/clktmr.mpk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/controller/pakfs/testdata/clktmr.mpk -------------------------------------------------------------------------------- /drivers/rspq/mixer/testdata/sfx_alarm_loop3.pcm_s16be: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/mixer/testdata/sfx_alarm_loop3.pcm_s16be -------------------------------------------------------------------------------- /drivers/rspq/mixer/testdata/sfx_wpn_cannon2.pcm_s16be: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/mixer/testdata/sfx_wpn_cannon2.pcm_s16be -------------------------------------------------------------------------------- /go.env: -------------------------------------------------------------------------------- 1 | GOTOOLCHAIN=go1.24.4-embedded 2 | GOOS=noos 3 | GOARCH=mips64 4 | GOFLAGS='-exec=n64go rom -run' '-toolexec=n64go toolexec' '-tags=n64' '-trimpath' 5 | -------------------------------------------------------------------------------- /drivers/rspq/mixer/testdata/sfx_wpn_machinegun_loop1.pcm_s16be: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clktmr/n64/HEAD/drivers/rspq/mixer/testdata/sfx_wpn_machinegun_loop1.pcm_s16be -------------------------------------------------------------------------------- /.github/workflows/ares_headless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | xvfb-run -s "-terminate" ares \ 3 | --setting Input/Driver=None \ 4 | --setting Video/Driver=None \ 5 | --setting Audio/Driver=None \ 6 | "$@" 7 | -------------------------------------------------------------------------------- /machine/init.go: -------------------------------------------------------------------------------- 1 | //go:build n64 2 | 3 | package machine 4 | 5 | import ( 6 | "embedded/arch/r4000/systim" 7 | 8 | "github.com/clktmr/n64/rcp/cpu" 9 | ) 10 | 11 | func init() { 12 | systim.Setup(cpu.ClockSpeed) 13 | } 14 | -------------------------------------------------------------------------------- /tools/pakfs/fuse_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !(linux || darwin) 2 | 3 | package pakfs 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | ) 9 | 10 | func mount(image, dir string) error { 11 | return fmt.Errorf("not supported on %s", runtime.GOOS) 12 | } 13 | -------------------------------------------------------------------------------- /debug/assert.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package debug 4 | 5 | const Enabled = true 6 | 7 | func Assert(b bool, message string) { 8 | if !b { 9 | panic(message) 10 | } 11 | } 12 | 13 | func AssertErrNil(err error) { 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fonts/gen.go: -------------------------------------------------------------------------------- 1 | package fonts 2 | 3 | //go:generate go tool n64go font basicfont 4 | //go:generate go tool n64go font gomono 5 | //go:generate go tool n64go font -start 0x2000 -end 0x26ff gomono 6 | //go:generate go tool n64go font goregular 7 | //go:generate go tool n64go font -start 0x2000 -end 0x26ff goregular 8 | -------------------------------------------------------------------------------- /rcp/cpu/cache_stub.s: -------------------------------------------------------------------------------- 1 | //go:build !mips64 2 | 3 | #include "go_asm.h" 4 | #include "funcdata.h" 5 | #include "textflag.h" 6 | 7 | // func writeback(addr uintptr, length uint) 8 | TEXT ·writeback(SB),NOSPLIT|NOFRAME,$0-16 9 | RET 10 | 11 | 12 | // func invalidate(addr uintptr, length uint) 13 | TEXT ·invalidate(SB),NOSPLIT|NOFRAME,$0-16 14 | RET 15 | -------------------------------------------------------------------------------- /rcp/doc.go: -------------------------------------------------------------------------------- 1 | // The rcp package provides an hardware abstraction layer for the Nintendo 64. 2 | // 3 | // It implements low-level access to the hardware. All hardware capabilities are 4 | // directly exposed and in general unsafe. Use the higher level libraries to 5 | // write applications instead. 6 | package rcp 7 | 8 | // Reality Coprocessor 9 | // https://ultra64.ca/files/documentation/online-manuals/man/pro-man/pro08/index8.1.html 10 | -------------------------------------------------------------------------------- /machine/exception.s: -------------------------------------------------------------------------------- 1 | #include "go_asm.h" 2 | #include "funcdata.h" 3 | #include "textflag.h" 4 | 5 | #include "asm_mips64.h" 6 | 7 | TEXT runtime·unhandledException(SB),NOSPLIT|NOFRAME,$0 8 | SUB $48, R29 9 | MOVV M(C0_CAUSE), R26 10 | MOVV R26, 8(R29) 11 | MOVV M(C0_EPC), R26 12 | MOVV R26, 16(R29) 13 | MOVV M(C0_SR), R26 14 | MOVV R26, 24(R29) 15 | MOVV M(C0_BADVADDR), R26 16 | MOVV R26, 32(R29) 17 | MOVV R31, 40(R29) 18 | 19 | JAL ·exception(SB) 20 | NOOP 21 | JMP -1(PC) 22 | -------------------------------------------------------------------------------- /fonts/gomono12/subfonts.go: -------------------------------------------------------------------------------- 1 | // Go Mono 12 2 | package gomono12 3 | 4 | import ( 5 | "embed" 6 | 7 | "github.com/clktmr/n64/drivers/cartfs" 8 | "github.com/clktmr/n64/fonts" 9 | "github.com/embeddedgo/display/font/subfont" 10 | ) 11 | 12 | const ( 13 | Height = 14 14 | Ascent = 11 15 | ) 16 | 17 | //go:embed *.tex *.pos 18 | var _fontData embed.FS 19 | var fontData = cartfs.Embed(_fontData) 20 | 21 | func NewFace() *fonts.Face { 22 | return &fonts.Face{ 23 | subfont.Face{Height: Height, 24 | Ascent: Ascent, 25 | Loader: fonts.NewLoader(&fontData, Height, Ascent), 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fonts/basicfont12/subfonts.go: -------------------------------------------------------------------------------- 1 | // basicfont 12 2 | package basicfont12 3 | 4 | import ( 5 | "embed" 6 | 7 | "github.com/clktmr/n64/drivers/cartfs" 8 | "github.com/clktmr/n64/fonts" 9 | "github.com/embeddedgo/display/font/subfont" 10 | ) 11 | 12 | const ( 13 | Height = 13 14 | Ascent = 11 15 | ) 16 | 17 | //go:embed *.tex *.pos 18 | var _fontData embed.FS 19 | var fontData = cartfs.Embed(_fontData) 20 | 21 | func NewFace() *fonts.Face { 22 | return &fonts.Face{ 23 | subfont.Face{Height: Height, 24 | Ascent: Ascent, 25 | Loader: fonts.NewLoader(&fontData, Height, Ascent), 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fonts/goregular12/subfonts.go: -------------------------------------------------------------------------------- 1 | // Go Regular 12 2 | package goregular12 3 | 4 | import ( 5 | "embed" 6 | 7 | "github.com/clktmr/n64/drivers/cartfs" 8 | "github.com/clktmr/n64/fonts" 9 | "github.com/embeddedgo/display/font/subfont" 10 | ) 11 | 12 | const ( 13 | Height = 14 14 | Ascent = 11 15 | ) 16 | 17 | //go:embed *.tex *.pos 18 | var _fontData embed.FS 19 | var fontData = cartfs.Embed(_fontData) 20 | 21 | func NewFace() *fonts.Face { 22 | return &fonts.Face{ 23 | subfont.Face{Height: Height, 24 | Ascent: Ascent, 25 | Loader: fonts.NewLoader(&fontData, Height, Ascent), 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /drivers/drivers.go: -------------------------------------------------------------------------------- 1 | // Package drivers and it's subdirectories build upon 2 | // [github.com/clktmr/n64/rcp] to provide common interfaces and higher-level 3 | // features. 4 | package drivers 5 | 6 | import "io" 7 | 8 | // SystemWriter is a function that implements the builtin print(). It can be 9 | // passed to [embedded/rtos.SetSystemWriter]. 10 | type SystemWriter func(int, []byte) int 11 | 12 | // Returns a SystemWriter from an io.Writer. 13 | func NewSystemWriter(w io.Writer) SystemWriter { 14 | // FIXME SystemWriter needs go:nosplit pragma 15 | return func(fd int, p []byte) int { 16 | n, _ := w.Write(p) 17 | return n 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /debug/assert_release.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | // Package debug provides assertions that can be enabled with the debug build 4 | // tag or will otherwise compile to no-ops. 5 | // 6 | // This is not considered idiomatic Go, but might be useful in an embedded 7 | // environment. 8 | package debug 9 | 10 | // Guard more complex assertions (i.e. anything that could panic) with `if 11 | // debug.Enabled{...}`, otherwise they can't be removed in release builds. 12 | const Enabled = false 13 | 14 | // Assert panics if b is false. 15 | func Assert(b bool, message string) {} 16 | 17 | // AssertErrNil panics if err is not nil. 18 | func AssertErrNil(err error) {} 19 | -------------------------------------------------------------------------------- /rcp/fixed/int11_5_fixed.go: -------------------------------------------------------------------------------- 1 | package fixed 2 | 3 | import "fmt" 4 | 5 | func Int11_5U(i int) Int11_5 { return Int11_5(i << 5) } 6 | func Int11_5F(f float32) Int11_5 { return Int11_5(f * (1 << 5)) } 7 | 8 | func (x Int11_5) Floor() int { return int(x >> 5) } 9 | func (x Int11_5) Ceil() int { return int(int32(x) + (1<<5-1)>>5) } 10 | func (x Int11_5) Mul(y Int11_5) Int11_5 { return Int11_5((int32(x) * int32(y)) >> 5) } 11 | func (x Int11_5) Div(y Int11_5) Int11_5 { return Int11_5(int32(x) << 5 / int32(y)) } 12 | 13 | func (x Int11_5) String() string { 14 | const shift, mask = 5, 1<<5 - 1 15 | return fmt.Sprintf("%d:%02d", int32(x>>shift), int32(x&mask)) 16 | } 17 | -------------------------------------------------------------------------------- /rcp/fixed/int6_10_fixed.go: -------------------------------------------------------------------------------- 1 | package fixed 2 | 3 | import "fmt" 4 | 5 | func Int6_10U(i int) Int6_10 { return Int6_10(i << 10) } 6 | func Int6_10F(f float32) Int6_10 { return Int6_10(f * (1 << 10)) } 7 | 8 | func (x Int6_10) Floor() int { return int(x >> 10) } 9 | func (x Int6_10) Ceil() int { return int(int32(x) + (1<<10-1)>>10) } 10 | func (x Int6_10) Mul(y Int6_10) Int6_10 { return Int6_10((int32(x) * int32(y)) >> 10) } 11 | func (x Int6_10) Div(y Int6_10) Int6_10 { return Int6_10(int32(x) << 10 / int32(y)) } 12 | 13 | func (x Int6_10) String() string { 14 | const shift, mask = 10, 1<<10 - 1 15 | return fmt.Sprintf("%d:%04d", int32(x>>shift), int32(x&mask)) 16 | } 17 | -------------------------------------------------------------------------------- /rcp/fixed/uint14_2_fixed.go: -------------------------------------------------------------------------------- 1 | package fixed 2 | 3 | import "fmt" 4 | 5 | func UInt14_2U(i int) UInt14_2 { return UInt14_2(i << 2) } 6 | func UInt14_2F(f float32) UInt14_2 { return UInt14_2(f * (1 << 2)) } 7 | 8 | func (x UInt14_2) Floor() int { return int(x >> 2) } 9 | func (x UInt14_2) Ceil() int { return int(uint32(x) + (1<<2-1)>>2) } 10 | func (x UInt14_2) Mul(y UInt14_2) UInt14_2 { return UInt14_2((uint32(x) * uint32(y)) >> 2) } 11 | func (x UInt14_2) Div(y UInt14_2) UInt14_2 { return UInt14_2(uint32(x) << 2 / uint32(y)) } 12 | 13 | func (x UInt14_2) String() string { 14 | const shift, mask = 2, 1<<2 - 1 15 | return fmt.Sprintf("%d:%01d", uint32(x>>shift), uint32(x&mask)) 16 | } 17 | -------------------------------------------------------------------------------- /drivers/cartfs/fs_other.go: -------------------------------------------------------------------------------- 1 | //go:build !n64 2 | 3 | package cartfs 4 | 5 | import ( 6 | "embed" 7 | "io/fs" 8 | ) 9 | 10 | type baseType = *embed.FS 11 | 12 | func embedfs(f embed.FS) FS { return FS{base: &f} } 13 | 14 | func (f *FS) baseOpen(name string) (fs.File, error) { 15 | if f.base != nil { 16 | f, err := (*f.base).Open(name) 17 | return f, err 18 | } 19 | return f.cartfsOpen(name) 20 | } 21 | 22 | func (f *FS) baseReadFile(name string) ([]byte, error) { 23 | if f.base != nil { 24 | return (*f.base).ReadFile(name) 25 | } 26 | return f.cartfsReadFile(name) 27 | } 28 | 29 | func (f *FS) baseReadDir(name string) ([]fs.DirEntry, error) { 30 | if f.base != nil { 31 | return (*f.base).ReadDir(name) 32 | } 33 | return f.cartfsReadDir(name) 34 | } 35 | -------------------------------------------------------------------------------- /rcp/rsp/rsp.go: -------------------------------------------------------------------------------- 1 | package rsp 2 | 3 | import ( 4 | "embedded/rtos" 5 | 6 | "github.com/clktmr/n64/rcp" 7 | "github.com/clktmr/n64/rcp/rsp/ucode" 8 | ) 9 | 10 | func init() { 11 | regs().status.Store(setHalt | clrSingleStep) 12 | pc().Store(0x1000) 13 | rcp.SetHandler(rcp.IntrRSP, handler) 14 | rcp.EnableInterrupts(rcp.IntrRSP) 15 | } 16 | 17 | var IntBreak rtos.Cond 18 | 19 | //go:nosplit 20 | //go:nowritebarrierrec 21 | func handler() { 22 | regs().status.Store(clrIntr) 23 | IntBreak.Signal() 24 | } 25 | 26 | func Load(ucode *ucode.UCode) { 27 | _, err := IMEM.WriteAt(ucode.Text, 0x0) 28 | if err != nil { 29 | panic(err) 30 | } 31 | _, err = DMEM.WriteAt(ucode.Data, 0x0) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | pc().Store(ucode.Entry) 37 | } 38 | -------------------------------------------------------------------------------- /rcp/cpu/cache_mips64.s: -------------------------------------------------------------------------------- 1 | #include "go_asm.h" 2 | #include "funcdata.h" 3 | #include "textflag.h" 4 | 5 | // func writeback(addr uintptr, length uint) 6 | TEXT ·writeback(SB),NOSPLIT|NOFRAME,$0-16 7 | MOVV addr+0(FP), R4 8 | MOVV length+8(FP), R5 9 | ADDU R5, R4, R8 10 | AND $const_cacheLineMask, R4 11 | 12 | loop: 13 | SUB R4, R8, R9 14 | BLEZ R9, done 15 | BREAK R25, 0(R4) // asm generates cache op 16 | ADDU $const_CacheLineSize, R4 17 | JMP loop 18 | 19 | done: 20 | RET 21 | 22 | 23 | // func invalidate(addr uintptr, length uint) 24 | TEXT ·invalidate(SB),NOSPLIT|NOFRAME,$0-16 25 | MOVV addr+0(FP), R4 26 | MOVV length+8(FP), R5 27 | ADDU R5, R4, R8 28 | AND $const_cacheLineMask, R4 29 | 30 | loop: 31 | SUB R4, R8, R9 32 | BLEZ R9, done 33 | BREAK R17, 0(R4) // asm generates cache op 34 | ADDU $const_CacheLineSize, R4 35 | JMP loop 36 | 37 | done: 38 | RET 39 | -------------------------------------------------------------------------------- /drivers/carts/carts.go: -------------------------------------------------------------------------------- 1 | // Package carts provides probing for various n64 flashcarts. 2 | // 3 | // These are not required to run ROMs on the flashcarts. They provide access to 4 | // additional features of the carts like usb logging. 5 | // 6 | // See the subdirectories for supported flashcarts. 7 | package carts 8 | 9 | import ( 10 | "io" 11 | 12 | "github.com/clktmr/n64/drivers/carts/everdrive64" 13 | "github.com/clktmr/n64/drivers/carts/isviewer" 14 | "github.com/clktmr/n64/drivers/carts/summercart64" 15 | ) 16 | 17 | type Cart interface { 18 | io.Writer 19 | } 20 | 21 | func ProbeAll() (c Cart) { 22 | if sc64 := summercart64.Probe(); sc64 != nil { 23 | c = sc64 24 | } else if ed64 := everdrive64.Probe(); ed64 != nil { 25 | // TODO should return the cart 26 | c = everdrive64.NewUNFLoader(ed64) 27 | } else if isv := isviewer.Probe(); isv != nil { 28 | c = isv 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /rcp/periph/regs.go: -------------------------------------------------------------------------------- 1 | package periph 2 | 3 | import ( 4 | "embedded/mmio" 5 | 6 | "github.com/clktmr/n64/rcp/cpu" 7 | ) 8 | 9 | func regs() *registers { return cpu.MMIO[registers](0x0460_0000) } 10 | 11 | type statusFlags uint32 12 | 13 | // Read access to status register 14 | const ( 15 | dmaBusy statusFlags = 1 << iota 16 | ioBusy 17 | dmaError 18 | dmaFinished 19 | ) 20 | 21 | // Write access to status register 22 | const ( 23 | reset statusFlags = 1 << iota 24 | clearInterrupt 25 | ) 26 | 27 | type registers struct { 28 | dramAddr mmio.R32[cpu.Addr] 29 | cartAddr mmio.R32[cpu.Addr] 30 | readLen mmio.U32 31 | writeLen mmio.U32 32 | status mmio.R32[statusFlags] 33 | 34 | latch1 mmio.U32 35 | pulseWidth1 mmio.U32 36 | pageSize1 mmio.U32 37 | release1 mmio.U32 38 | latch2 mmio.U32 39 | pulseWidth2 mmio.U32 40 | pageSize2 mmio.U32 41 | release2 mmio.U32 42 | } 43 | -------------------------------------------------------------------------------- /fonts/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package fonts_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/clktmr/n64/fonts/gomono12" 7 | n64testing "github.com/clktmr/n64/testing" 8 | ) 9 | 10 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 11 | 12 | const lorem = `Lorem ipsum dolor sit amet, consectetur adipisici elit, sed 13 | eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad 14 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid 15 | ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit 16 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat 17 | cupiditat non proident, sunt in culpa qui officia deserunt mollit anim 18 | id est laborum.` 19 | 20 | func BenchmarkGlyphMap(b *testing.B) { 21 | gomono := gomono12.NewFace() 22 | 23 | i := 0 24 | for b.Loop() { 25 | gomono.GlyphMap(rune(lorem[i%len(lorem)])) 26 | i++ 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rcp/cpu/const.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import "unsafe" 4 | 5 | // The CPU's clock speed 6 | const ClockSpeed = 93.75e6 7 | 8 | // Memory regions in 32bit Kernel mode 9 | const ( 10 | KSEG0 uintptr = 0xffffffff_80000000 // unmapped, cached 11 | KSEG1 uintptr = 0xffffffff_a0000000 // unmapped, uncached 12 | ) 13 | 14 | // Addr represents a physical memory address 15 | type Addr uint32 16 | 17 | // PAddr returns the physical address of a virtual address. 18 | func PAddr(addr uintptr) Addr { 19 | return Addr(addr & ^uintptr(0xe000_0000)) 20 | } 21 | 22 | type Pointer[T any] interface{ *T } 23 | 24 | // PhysicalAddress returns the physical address of a pointer. 25 | func PhysicalAddress[T Pointer[Q], Q any](s T) Addr { 26 | return PAddr(uintptr(unsafe.Pointer(s))) 27 | } 28 | 29 | // Same as [PhysicalAddress] but for slices. 30 | func PhysicalAddressSlice[T any](s []T) Addr { 31 | return PAddr(uintptr(unsafe.Pointer(unsafe.SliceData(s)))) 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/clktmr/n64 2 | 3 | go 1.24.3 4 | 5 | toolchain go1.24.5 6 | 7 | require ( 8 | github.com/aymanbagabas/go-pty v0.2.2 9 | github.com/buildkite/shellwords v1.0.0 10 | github.com/embeddedgo/display v1.1.0 11 | github.com/embeddedgo/fs v0.1.0 12 | github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 13 | github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f 14 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b 15 | golang.org/x/image v0.13.0 16 | golang.org/x/text v0.14.0 17 | rsc.io/rsc v0.0.0-20180427141835-fc6202590229 18 | ) 19 | 20 | require ( 21 | github.com/creack/pty v1.1.24 // indirect 22 | github.com/u-root/u-root v0.11.0 // indirect 23 | golang.org/x/crypto v0.17.0 // indirect 24 | golang.org/x/mod v0.25.0 // indirect 25 | golang.org/x/sync v0.15.0 // indirect 26 | golang.org/x/sys v0.33.0 // indirect 27 | golang.org/x/tools v0.34.0 // indirect 28 | ) 29 | 30 | tool ( 31 | github.com/clktmr/n64/tools/n64go 32 | golang.org/x/exp/cmd/gorelease 33 | ) 34 | -------------------------------------------------------------------------------- /drivers/draw/cpu.go: -------------------------------------------------------------------------------- 1 | package draw 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | 7 | "github.com/clktmr/n64/rcp/texture" 8 | ) 9 | 10 | // SW implements a software-based drawer by forwarding the calls to image.draw. 11 | // 12 | // It ensures to that image.draw uses the optimized path by passing images 13 | // instead of textures. Note that as of now using 32bpp textures has better 14 | // performance, since there is no optimized implementation for 16bpp images. 15 | type SW draw.Op 16 | 17 | const ( 18 | OverSW = SW(draw.Over) 19 | SrcSW = SW(draw.Src) 20 | ) 21 | 22 | func (op SW) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { 23 | op.DrawMask(dst, r, src, sp, nil, image.Point{}) 24 | } 25 | 26 | func (op SW) DrawMask(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point) { 27 | if tex, ok := src.(texture.Texture); ok { 28 | src = tex.Image 29 | } 30 | if tex, ok := mask.(texture.Texture); ok { 31 | mask = tex.Image 32 | } 33 | draw.DrawMask(dst, r, src, sp, mask, mp, draw.Op(op)) 34 | } 35 | -------------------------------------------------------------------------------- /drivers/carts/everdrive64/regs.go: -------------------------------------------------------------------------------- 1 | // Package everdrive64 implements support for EverDrive64. 2 | // 3 | // Tested on EverDrive64 X7, but should also work on X3. 4 | package everdrive64 5 | 6 | import ( 7 | "github.com/clktmr/n64/rcp/cpu" 8 | "github.com/clktmr/n64/rcp/periph" 9 | ) 10 | 11 | type usbMode uint32 12 | 13 | const ( 14 | readNop usbMode = 0xC400 15 | read usbMode = 0xC600 16 | writeNop usbMode = 0xC000 17 | write usbMode = 0xC200 18 | ) 19 | 20 | type usbStatus uint32 21 | 22 | const ( 23 | act usbStatus = 0x0200 24 | rxf usbStatus = 0x0400 25 | txe usbStatus = 0x0800 26 | power usbStatus = 0x1000 27 | busy usbStatus = 0x2000 28 | ) 29 | 30 | type registers struct { 31 | usbCfgR *periph.R32[usbStatus] 32 | usbCfgW *periph.R32[usbMode] 33 | version *periph.U32 34 | sysCfg *periph.U32 35 | key *periph.U32 36 | } 37 | 38 | func regs() *registers { 39 | return ®isters{ 40 | cpu.MMIO[periph.R32[usbStatus]](0x1f80_0004), 41 | cpu.MMIO[periph.R32[usbMode]](0x1f80_0004), 42 | cpu.MMIO[periph.U32](0x1f80_0014), 43 | cpu.MMIO[periph.U32](0x1f80_8000), 44 | cpu.MMIO[periph.U32](0x1f80_8004), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rcp/serial/regs.go: -------------------------------------------------------------------------------- 1 | // Package serial provides running commands on the PIF microchip. 2 | // 3 | // The serial interface provides access to the PIF microchip, which in turn 4 | // handles console startup, reset and most importantly the joyBus. The joyBus is 5 | // connected to the controllers and their accessories, e.g. rumble pak. 6 | // 7 | // The serial interface is very slow. Blocking reads and writes should be 8 | // avoided. 9 | package serial 10 | 11 | import ( 12 | "embedded/mmio" 13 | 14 | "github.com/clktmr/n64/rcp/cpu" 15 | ) 16 | 17 | func regs() *registers { return cpu.MMIO[registers](0x0480_0000) } 18 | 19 | const ( 20 | pifRamAddr cpu.Addr = 0x1fc0_07c0 21 | pifRamSize = 64 22 | ) 23 | 24 | type statusFlags uint32 25 | 26 | const ( 27 | dmaBusy statusFlags = 1 << iota 28 | ioBusy 29 | ) 30 | 31 | type registers struct { 32 | dramAddr mmio.R32[cpu.Addr] 33 | pifReadAddr mmio.R32[cpu.Addr] // Writing triggers the actual joybus exchange 34 | pifWriteAddr4B mmio.R32[cpu.Addr] 35 | _ mmio.U32 36 | pifWriteAddr mmio.R32[cpu.Addr] 37 | pifReadAddr4B mmio.R32[cpu.Addr] 38 | status mmio.R32[statusFlags] 39 | } 40 | -------------------------------------------------------------------------------- /drivers/draw/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package draw_test 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/clktmr/n64/drivers/draw" 9 | "github.com/clktmr/n64/fonts/gomono12" 10 | "github.com/clktmr/n64/rcp/video" 11 | ) 12 | 13 | var lorem = []byte(`Lorem ipsum dolor sit amet, consectetur 14 | adipisici elit, sed eiusmod tempor 15 | incidunt ut labore et dolore magna 16 | aliqua. Ut enim ad minim veniam, quis 17 | nostrud exercitation ullamco laboris 18 | nisi ut aliquid ex ea commodi consequat. 19 | Quis aute iure reprehenderit in 20 | voluptate velit esse cillum dolore eu 21 | fugiat nulla pariatur. Excepteur sint 22 | obcaecat cupiditat non proident, sunt in 23 | culpa qui officia deserunt mollit anim 24 | id est laborum.`) 25 | 26 | var gomono = gomono12.NewFace() 27 | 28 | func init() { 29 | gomono.Glyph('A') 30 | } 31 | 32 | func BenchmarkDrawText(b *testing.B) { 33 | fb := video.Framebuffer() 34 | 35 | white := color.NRGBA{0xff, 0xff, 0xff, 0xff} 36 | black := color.NRGBA{0x0, 0x0, 0x0, 0xff} 37 | 38 | for b.Loop() { 39 | draw.DrawText(fb, fb.Bounds(), gomono, image.Point{0, int(gomono.Ascent)}, white, black, lorem) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tools/pakfs/main.go: -------------------------------------------------------------------------------- 1 | package pakfs 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | const usageString = `Controller Pak File System Utility. 12 | 13 | Usage: 14 | 15 | %s [arguments] 16 | 17 | The commands are: 18 | 19 | mount serve pakfs image via fuse 20 | ` 21 | 22 | var flags = flag.NewFlagSet("pakfs", flag.ExitOnError) 23 | 24 | func usage() { 25 | fmt.Fprintf(flags.Output(), usageString, "pakfs") 26 | flags.PrintDefaults() 27 | } 28 | 29 | var sigintr = make(chan os.Signal, 1) 30 | 31 | func Main(args []string) { 32 | flags.Usage = usage 33 | flags.Parse(args[1:]) 34 | 35 | if flags.NArg() < 1 { 36 | flags.Usage() 37 | os.Exit(1) 38 | } 39 | 40 | signal.Notify(sigintr, os.Interrupt) 41 | 42 | switch flags.Arg(0) { 43 | case "mount": 44 | if flags.NArg() < 3 { 45 | flags.Usage() 46 | os.Exit(1) 47 | } 48 | image := flags.Arg(1) 49 | dir := flags.Arg(2) 50 | err := mount(image, dir) 51 | if err != nil { 52 | log.Fatalln("mount:", err) 53 | } 54 | default: 55 | fmt.Fprintf(flags.Output(), "unknown command: %s\n", flags.Arg(0)) 56 | flags.Usage() 57 | os.Exit(1) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /drivers/rspq/mixer/fixed.go: -------------------------------------------------------------------------------- 1 | package mixer 2 | 3 | import "fmt" 4 | 5 | type uint20_12 uint32 6 | 7 | func uint20_12U(u uint) uint20_12 { return uint20_12(u << 12) } 8 | func uint20_12F(f float32) uint20_12 { return uint20_12(f * (1<<12 - 1)) } 9 | 10 | func (x uint20_12) Floor() uint { return uint(x >> 12) } 11 | func (x uint20_12) Ceil() uint { return uint((x + (1<<12 - 1)) >> 12) } 12 | func (x uint20_12) Mul(y uint20_12) uint20_12 { return uint20_12((uint64(x) * uint64(y)) >> 12) } 13 | func (x uint20_12) Div(y uint20_12) uint20_12 { return uint20_12(uint64(x) << 12 / uint64(y)) } 14 | 15 | func (x uint20_12) String() string { 16 | const shift, mask = 12, 1<<12 - 1 17 | return fmt.Sprintf("%d:%04d", uint32(x>>shift), uint32(x&mask)) 18 | } 19 | 20 | type int1_15 int16 21 | 22 | func int1_15F(f float32) int1_15 { return int1_15(f * (1<<15 - 1)) } 23 | 24 | func (x int1_15) Mul(y int1_15) int1_15 { return int1_15((int32(x) * int32(y)) >> 15) } 25 | func (x int1_15) Div(y int1_15) int1_15 { return int1_15(int32(x) << 15 / int32(y)) } 26 | 27 | func (x int1_15) String() string { 28 | const shift, mask = 15, 1<<15 - 1 29 | return fmt.Sprintf("%d:%05d", int16(x>>shift), int16(x&mask)) 30 | } 31 | -------------------------------------------------------------------------------- /rcp/rdp/rdp_test.go: -------------------------------------------------------------------------------- 1 | package rdp_test 2 | 3 | import ( 4 | _ "embed" 5 | "image" 6 | "image/color" 7 | "testing" 8 | 9 | "github.com/clktmr/n64/rcp/rdp" 10 | "github.com/clktmr/n64/rcp/texture" 11 | n64testing "github.com/clktmr/n64/testing" 12 | ) 13 | 14 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 15 | 16 | func TestFillRect(t *testing.T) { 17 | testcolor := color.NRGBA{R: 0, G: 0x37, B: 0x77, A: 0xff} 18 | img := texture.NewFramebuffer(image.Rect(0, 0, 32, 32)) 19 | 20 | dl := &rdp.RDP 21 | 22 | dl.SetColorImage(img) 23 | 24 | bounds := image.Rectangle{ 25 | image.Point{0, 0}, 26 | image.Point{10, 5}, 27 | } 28 | dl.SetScissor(bounds, rdp.InterlaceNone) 29 | dl.SetFillColor(testcolor) 30 | dl.SetOtherModes( 31 | rdp.ForceBlend|rdp.AtomicPrimitive, 32 | rdp.CycleTypeFill, rdp.RGBDitherNone, rdp.AlphaDitherNone, rdp.ZmodeOpaque, rdp.CvgDestClamp, rdp.BlendMode{}, 33 | ) 34 | 35 | img.Invalidate() 36 | 37 | dl.FillRectangle(bounds) 38 | dl.Flush() 39 | 40 | for x := range bounds.Max.X { 41 | for y := range bounds.Max.Y { 42 | result := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) 43 | if result != testcolor { 44 | t.Errorf("%v at (%d,%d)", result, x, y) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /machine/machine.go: -------------------------------------------------------------------------------- 1 | // Package machine implements some target specific functions of the runtime and 2 | // provides access to the bootloader. 3 | // 4 | // The machine package must be imported by all n64 applications. If unused 5 | // import it for it's side effects: 6 | // 7 | // import _ github.com/clktmr/n64/machine 8 | package machine 9 | 10 | import ( 11 | "unsafe" 12 | 13 | "github.com/clktmr/n64/rcp/cpu" 14 | ) 15 | 16 | type video uint32 17 | 18 | const ( 19 | VideoPAL video = 0 20 | VideoNTSC video = 1 21 | VideoMPAL video = 2 22 | ) 23 | 24 | type reset uint32 25 | 26 | const ( 27 | ResetCold reset = 0 // Power switch 28 | ResetWarm reset = 1 // Reset button 29 | ) 30 | 31 | type pak uint32 32 | 33 | const ( 34 | PakJumper pak = 4 * 1024 * 1024 35 | PakExpansion pak = 8 * 1024 * 1024 36 | ) 37 | 38 | // These variables are set by bootloader (aka IPL3). 39 | var ( 40 | // Reports whether the console booted from a power cycle or reset. 41 | ResetType reset = *(*reset)(unsafe.Pointer(cpu.KSEG1 | 0x8000_030C)) 42 | 43 | // Reports the console's region. 44 | VideoType video = *(*video)(unsafe.Pointer(cpu.KSEG1 | 0x8000_0300)) 45 | 46 | // Reports if an expansion pak is installed. 47 | PakType pak = *(*pak)(unsafe.Pointer(cpu.KSEG1 | 0x8000_0318)) 48 | ) 49 | -------------------------------------------------------------------------------- /drivers/cartfs/cartfs_test.go: -------------------------------------------------------------------------------- 1 | package cartfs_test 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | 7 | "github.com/clktmr/n64/drivers/cartfs" 8 | n64testing "github.com/clktmr/n64/testing" 9 | ) 10 | 11 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 12 | 13 | //go:embed concurrency.txt 14 | var _embed1 embed.FS 15 | 16 | var ( 17 | //go:embed testdata/ken.txt 18 | _embed2 embed.FS 19 | embed1, embed2 cartfs.FS = cartfs.Embed(_embed1), cartfs.Embed(_embed2) 20 | ) 21 | 22 | //go:embed testdata/hello.txt 23 | var _embed3 embed.FS 24 | 25 | var embed3 cartfs.FS = cartfs.Embed(_embed3) 26 | 27 | var _nocomment embed.FS 28 | var nocomment cartfs.FS = cartfs.Embed(_nocomment) 29 | 30 | //go:embed testdata/hello.txt 31 | var _notype embed.FS 32 | var notype = cartfs.Embed(_notype) 33 | 34 | // TestMkrom checks if the different declaration styles for variables are 35 | // correctly parsed by the mkrom tool. 36 | func TestEmbed(t *testing.T) { 37 | testFiles(t, embed1, "concurrency.txt", "Concurrency is not parallelism.\n") 38 | testFiles(t, embed3, "testdata/hello.txt", "hello, world\n") 39 | testFiles(t, embed2, "testdata/ken.txt", "If a program is too slow, it must have a loop.\n") 40 | testFiles(t, notype, "testdata/hello.txt", "hello, world\n") 41 | testDir(t, nocomment, ".") 42 | } 43 | -------------------------------------------------------------------------------- /drivers/rspq/commands.go: -------------------------------------------------------------------------------- 1 | package rspq 2 | 3 | import ( 4 | "github.com/clktmr/n64/debug" 5 | "github.com/clktmr/n64/rcp/cpu" 6 | ) 7 | 8 | type Command byte 9 | 10 | const ( 11 | CmdWaitNewInput Command = 0x00 12 | CmdNoop Command = 0x01 13 | CmdJump Command = 0x02 14 | CmdCall Command = 0x03 15 | CmdRet Command = 0x04 16 | CmdDma Command = 0x05 17 | CmdWriteStatus Command = 0x06 18 | CmdSwapBuffers Command = 0x07 19 | CmdTestWriteStatus Command = 0x08 20 | CmdRdpWaitIdle Command = 0x09 21 | CmdRdpSetBuffer Command = 0x0A 22 | CmdRdpAppendBuffer Command = 0x0B 23 | ) 24 | 25 | func dma(p []byte, dmemAddr cpu.Addr, n uint32, flags uint32) { 26 | debug.Assert(dmemAddr&0x7 == 0 && n&0x7 == 0, "unaligned dma") 27 | Write(CmdDma, uint32(cpu.PhysicalAddressSlice(p)), uint32(dmemAddr), n-1, flags) 28 | } 29 | 30 | const dmaBusyOrFull = 12 31 | 32 | // dmaWrite enqueues a DMA write command (dmem to rdram) 33 | func DMAWrite(p []byte, addr cpu.Addr, n uint32) { 34 | cpu.InvalidateSlice(p) 35 | dma(p, addr, n, 0xffff_8000|dmaBusyOrFull) 36 | } 37 | 38 | // dmaWrite enqueues a DMA read command (rdram to dmem) 39 | func DMARead(p []byte, addr cpu.Addr, n uint32) { 40 | cpu.WritebackSlice(p) 41 | dma(p, addr, n, dmaBusyOrFull) 42 | } 43 | -------------------------------------------------------------------------------- /drivers/rspq/rspq_test.go: -------------------------------------------------------------------------------- 1 | package rspq_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/clktmr/n64/drivers/rspq" 8 | "github.com/clktmr/n64/rcp/cpu" 9 | "github.com/clktmr/n64/rcp/rsp" 10 | n64testing "github.com/clktmr/n64/testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 14 | 15 | func TestWrite(t *testing.T) { 16 | rspq.Reset() 17 | 18 | // Write enough commands for multiple buffer swaps 19 | for i := 0; i < 1000; i++ { 20 | rspq.Write(rspq.CmdNoop) 21 | if rspq.Crashed() { 22 | t.Fatal("rspq crashed") 23 | } 24 | } 25 | } 26 | 27 | func TestCrash(t *testing.T) { 28 | rspq.Reset() 29 | 30 | // Cause rspq assertion fail with invalid command 31 | rspq.Write(rspq.Command(0xde)) 32 | for !rsp.Stopped() { 33 | // wait 34 | } 35 | 36 | if !rspq.Crashed() { 37 | t.Fatal("rspq should have crashed") 38 | } 39 | } 40 | 41 | func TestDMA(t *testing.T) { 42 | got := cpu.MakePaddedSlice[byte](128) 43 | expected := cpu.MakePaddedSlice[byte](128) 44 | rspq.Reset() 45 | 46 | rspq.DMAWrite(got, 256, uint32(len(got))) 47 | for !rsp.Stopped() { 48 | // wait 49 | } 50 | 51 | if rspq.Crashed() { 52 | t.Fatal("rspq crashed") 53 | } 54 | _, err := rsp.DMEM.ReadAt(expected, 256) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | if !bytes.Equal(got, expected) { 59 | t.Fatalf("dma data mismatch\n%q\n%q", got, expected) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /machine/asm_mips64.h: -------------------------------------------------------------------------------- 1 | #define C0_INDEX 0 /* Index of TLB Entry */ 2 | #define C0_ENTRYLO0 2 /* TLB entry's first PFN */ 3 | #define C0_ENTRYLO1 3 /* TLB entry's second PFN */ 4 | #define C0_PAGEMASK 5 /* Size of TLB Entries */ 5 | #define C0_BADVADDR 8 /* Address that occurred an error */ 6 | #define C0_COUNT 9 /* Timer Count Register */ 7 | #define C0_ENTRYHI 10 /* VPN and ASID of two TLB entry */ 8 | #define C0_COMPARE 11 /* Timer Compare Register */ 9 | #define C0_SR 12 /* Status Register */ 10 | #define C0_CAUSE 13 /* last exception description */ 11 | #define C0_EPC 14 /* Exception error address */ 12 | #define C0_PRID 15 /* Processor Revision ID */ 13 | #define C0_CONFIG 16 /* CPU configuration */ 14 | #define C0_WATCHLO 18 /* Watchpoint */ 15 | 16 | #define SR_CU1 0x20000000 /* Mark CP1 as usable */ 17 | #define SR_FR 0x04000000 /* Enable MIPS III FP registers */ 18 | #define SR_BEV 0x00400000 /* Controls location of exception vectors */ 19 | #define SR_PE 0x00100000 /* Mark soft reset (clear parity error) */ 20 | 21 | 22 | // Prepend NOOP to avert CP0 hazards 23 | #define TLBWI NOOP; WORD $0x42000002 24 | 25 | -------------------------------------------------------------------------------- /machine/exception.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | var excNames = [32]string{ 4 | 0: "Interrupt", 5 | 1: "TLB Modification", 6 | 2: "TLB Miss (load)", 7 | 3: "TLB Miss (store)", 8 | 4: "Address Error (load)", 9 | 5: "Address Error (store)", 10 | 6: "Bus Error (instruction)", 11 | 7: "Bus Error (data)", 12 | 8: "Syscall", 13 | 9: "Breakpoint", 14 | 10: "Reserved Instruction", 15 | 11: "Coprocessor Unusable", 16 | 12: "Arithmetic Overflow", 17 | 13: "Trap", 18 | 15: "Floating-Point", 19 | 23: "Watch", 20 | } 21 | 22 | //go:nosplit 23 | func exception(cause, epc, status, badvaddr, ra uint64) { 24 | var buf [16]byte 25 | DefaultWrite(0, []byte("unhandled exception: ")) 26 | DefaultWrite(0, []byte(excNames[cause>>2&31])) 27 | 28 | DefaultWrite(0, []byte("\ncause 0x")) 29 | DefaultWrite(0, itoa(buf[:], cause)) 30 | DefaultWrite(0, []byte("\nepc 0x")) 31 | DefaultWrite(0, itoa(buf[:], epc)) 32 | DefaultWrite(0, []byte("\nstatus 0x")) 33 | DefaultWrite(0, itoa(buf[:], status)) 34 | DefaultWrite(0, []byte("\nbadvaddr 0x")) 35 | DefaultWrite(0, itoa(buf[:], badvaddr)) 36 | DefaultWrite(0, []byte("\nra 0x")) 37 | DefaultWrite(0, itoa(buf[:], ra)) 38 | DefaultWrite(0, []byte("\n")) 39 | } 40 | 41 | //go:nosplit 42 | func itoa(buf []byte, num uint64) []byte { 43 | for i := range 16 { 44 | char := byte(num>>(60-(4*i))) & 0xf 45 | if char > 9 { 46 | char += 'a' - 10 47 | } else { 48 | char += '0' 49 | } 50 | buf[i] = char 51 | } 52 | return buf 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Timur Çelik. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /drivers/carts/summercart64/config.go: -------------------------------------------------------------------------------- 1 | package summercart64 2 | 3 | import "errors" 4 | 5 | type config uint32 6 | 7 | // Errors returned by the cart. 8 | var ( 9 | ErrBadArgument = errors.New("bad argument") 10 | ErrBadAddress = errors.New("bad address") 11 | ErrBadConfigId = errors.New("bad config id") 12 | ErrTimeout = errors.New("timeout") 13 | ErrSdCard = errors.New("sdcard") 14 | ErrUnknownCmd = errors.New("unknown command") 15 | ) 16 | 17 | var errCodes = map[uint32]error{ 18 | 1: ErrBadArgument, 19 | 2: ErrBadAddress, 20 | 3: ErrBadConfigId, 21 | 4: ErrTimeout, 22 | 5: ErrSdCard, 23 | 24 | 0xffffff: ErrUnknownCmd, 25 | } 26 | 27 | // Configuration options for [Cart.SetConfig]. 28 | const ( 29 | CfgBootloaderSwitch = iota 30 | CfgROMWriteEnable 31 | CfgROMShadowEnable 32 | CfgDDMode 33 | CfgISVAddress 34 | CfgBootMode 35 | CfgSaveType 36 | CfgCICSeed 37 | CfgTVType 38 | CfgDDSDEnable 39 | CfgDDDriveType 40 | CfgDDDiskState 41 | CfgButtonState 42 | CfgButtonMode 43 | CfgROMExtendedEnable 44 | ) 45 | 46 | // Config option values for [CfgButtonMode]. 47 | const ( 48 | ButtonModeDisabled uint32 = iota 49 | ButtonModeInterrupt 50 | ButtonModeUSBPacket 51 | ButtonMode64DDDiskChange 52 | ) 53 | 54 | func (v *Cart) SetConfig(option config, value uint32) (old uint32, err error) { 55 | _, old, err = execCommand(cmdConfigSet, uint32(option), value) 56 | return 57 | } 58 | 59 | func (v *Cart) Config(option config) (current uint32, err error) { 60 | _, current, err = execCommand(cmdConfigGet, uint32(option), 0) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /rcp/rsp/ucode/ucode.go: -------------------------------------------------------------------------------- 1 | package ucode 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/clktmr/n64/rcp/cpu" 8 | ) 9 | 10 | type UCode struct { 11 | Name string 12 | 13 | Entry cpu.Addr // initial value of RSP PC register 14 | Text []byte // instructions copied to IMEM 15 | Data []byte // data copied to DMEM 16 | } 17 | 18 | func NewUCode(name string, entry cpu.Addr, text []byte, data []byte) *UCode { 19 | return &UCode{ 20 | Name: name, 21 | Entry: entry, 22 | Text: cpu.CopyPaddedSlice(text), 23 | Data: cpu.CopyPaddedSlice(data), 24 | } 25 | } 26 | 27 | func Load(r io.Reader) (ucode *UCode, err error) { 28 | ucode = &UCode{} 29 | load := func(data any) { 30 | if err != nil { 31 | return 32 | } 33 | err = binary.Read(r, binary.BigEndian, data) 34 | } 35 | var size uint32 36 | load(&size) 37 | name := make([]byte, size) 38 | load(&name) 39 | ucode.Name = string(name) 40 | load(&ucode.Entry) 41 | 42 | load(&size) 43 | ucode.Text = cpu.MakePaddedSlice[byte](int(size)) 44 | load(&ucode.Text) 45 | 46 | load(&size) 47 | ucode.Data = cpu.MakePaddedSlice[byte](int(size)) 48 | load(&ucode.Data) 49 | return 50 | } 51 | 52 | func (ucode *UCode) Store(w io.Writer) (err error) { 53 | store := func(data any) { 54 | if err != nil { 55 | return 56 | } 57 | err = binary.Write(w, binary.BigEndian, data) 58 | } 59 | store(uint32(len(ucode.Name))) 60 | store([]byte(ucode.Name)) 61 | store(ucode.Entry) 62 | store(uint32(len(ucode.Text))) 63 | store(ucode.Text) 64 | store(uint32(len(ucode.Data))) 65 | store(ucode.Data) 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /drivers/cartfs/fs_n64.go: -------------------------------------------------------------------------------- 1 | //go:build n64 2 | 3 | package cartfs 4 | 5 | import ( 6 | "embed" 7 | "errors" 8 | "io/fs" 9 | "math" 10 | 11 | "github.com/clktmr/n64/rcp/cpu" 12 | "github.com/clktmr/n64/rcp/periph" 13 | ) 14 | 15 | // On target n64 base specifies the pi bus address where to find the cartfs to 16 | // read from. If is set to a non-zero value, a pi bus device is initialized on 17 | // first use of the filesystem. 18 | type baseType = cpu.Addr 19 | 20 | func embedfs(_ embed.FS) FS { 21 | // Initialize with non-zero value, just to force the symbol to be placed 22 | // in .data instead of .bss. Actual initialization will be done after 23 | // linking by the mkrom tool. 24 | return FS{base: 0xffff_ffff} 25 | } 26 | 27 | func (f *FS) baseInit() error { 28 | if f.dev != nil || f.base == 0x0 { 29 | return nil 30 | } 31 | if f.base == 0xffff_ffff { 32 | return errors.New("cartfs.Embed not initialized by mkrom") 33 | } 34 | dev := periph.NewDevice(f.base, math.MaxUint32) 35 | fnew, err := Read(dev) 36 | if err != nil { 37 | return err 38 | } 39 | *f = *fnew 40 | return nil 41 | } 42 | 43 | func (f *FS) baseOpen(name string) (fs.File, error) { 44 | if err := f.baseInit(); err != nil { 45 | return nil, err 46 | } 47 | return f.cartfsOpen(name) 48 | } 49 | func (f *FS) baseReadFile(name string) ([]byte, error) { 50 | if err := f.baseInit(); err != nil { 51 | return nil, err 52 | } 53 | return f.cartfsReadFile(name) 54 | } 55 | func (f *FS) baseReadDir(name string) ([]fs.DirEntry, error) { 56 | if err := f.baseInit(); err != nil { 57 | return nil, err 58 | } 59 | return f.cartfsReadDir(name) 60 | } 61 | -------------------------------------------------------------------------------- /rcp/cpu/pinner.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import ( 4 | "runtime" 5 | "slices" 6 | "unsafe" 7 | ) 8 | 9 | // Pinner is a lightweight version of runtime.Pinner. In contrast to 10 | // runtime.Pinner it's not compatible with cgocheck=1. 11 | type Pinner struct { 12 | *pinner 13 | } 14 | 15 | type pinner struct { 16 | // The object is pinned by keeping a reference from heap to it. This 17 | // will also enforce it to escape, since only pointers on the stack can 18 | // point into the stack. This is necessary because in the current 19 | // runtime implementation (go1.24) the stack might be moved. 20 | refs []unsafe.Pointer 21 | } 22 | 23 | type eface struct { 24 | _type, data unsafe.Pointer 25 | } 26 | 27 | func (p *Pinner) Pin(pointer any) { 28 | if p.pinner == nil { 29 | p.pinner = new(pinner) 30 | p.refs = make([]unsafe.Pointer, 0, 8) 31 | runtime.SetFinalizer(p.pinner, func(i *pinner) { 32 | if len(i.refs) != 0 { 33 | panic("cpu.Pinner: memory leak") 34 | } 35 | }) 36 | 37 | } 38 | itf := (*eface)(unsafe.Pointer(&pointer)) 39 | 40 | // TODO debug.Assert(pointer holds pointer type) 41 | 42 | // In contrast to runtime.Pinner, we'll only add the pointer if it's not 43 | // already pinned to keep p.refs small. 44 | if !slices.Contains(p.refs, itf.data) { 45 | p.refs = append(p.refs, itf.data) 46 | } 47 | } 48 | 49 | func (p *Pinner) Unpin() { 50 | // In contrast to runtime.Pinner, we clear p.refs instead of dropping 51 | // the reference to it to avoid an allocation next time 52 | clear(p.refs[:]) 53 | p.refs = p.refs[:0] 54 | } 55 | 56 | func PinSlice[T any](p *Pinner, slice []T) { 57 | p.Pin(unsafe.SliceData(slice)) 58 | } 59 | -------------------------------------------------------------------------------- /rcp/tasker_test.go: -------------------------------------------------------------------------------- 1 | package rcp_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/clktmr/n64/rcp" 9 | "github.com/clktmr/n64/rcp/rdp" 10 | ) 11 | 12 | var f float32 13 | 14 | func fpuClobber() { 15 | rcp.ClearDPIntr() 16 | f += 0.33 17 | } 18 | 19 | func TestFPUPreemption(t *testing.T) { 20 | rdpHandler := rcp.Handler(rcp.IntrRDP) 21 | rcp.SetHandler(rcp.IntrRDP, fpuClobber) 22 | t.Cleanup(func() { 23 | rcp.SetHandler(rcp.IntrRDP, rdpHandler) 24 | }) 25 | 26 | const numGoroutines = 10 27 | results := [numGoroutines]float64{} 28 | wg := sync.WaitGroup{} 29 | 30 | wg.Add(numGoroutines) 31 | for i := range numGoroutines { 32 | i := i 33 | go func(f float64) { 34 | for range 1000000 { 35 | f += 0.125 36 | } 37 | results[i] = f 38 | wg.Done() 39 | }(float64(i)) 40 | } 41 | 42 | // generate some fpu preemptions using hardware interrupts 43 | done := make(chan struct{}) 44 | go func() { 45 | for { 46 | select { 47 | case <-done: 48 | return 49 | default: 50 | rdp.RDP.Push(rdp.SyncFull) 51 | time.Sleep(time.Millisecond) 52 | } 53 | } 54 | }() 55 | 56 | wg.Wait() 57 | done <- struct{}{} 58 | 59 | for i, v := range results { 60 | expected := float64(i) + 125000.0 61 | if v != expected { 62 | t.Errorf("unexpected result: %v != %v", v, expected) 63 | } 64 | } 65 | } 66 | 67 | func BenchmarkSchedule(b *testing.B) { 68 | start := make(chan bool) 69 | stop := make(chan bool) 70 | 71 | go func() { 72 | for <-start { 73 | stop <- true 74 | } 75 | stop <- false 76 | }() 77 | 78 | for i := 0; i < b.N; i++ { 79 | start <- true 80 | <-stop 81 | } 82 | start <- false 83 | <-stop 84 | } 85 | -------------------------------------------------------------------------------- /tools/toolexec/embed.go: -------------------------------------------------------------------------------- 1 | package toolexec 2 | 3 | // This file contains code copied from Go's embed implementation. 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | // Stolen from go/src/cmd/compile/internal/noder/noder.go 14 | func parseGoEmbed(args string) ([]string, error) { 15 | var list []string 16 | for args = strings.TrimSpace(args); args != ""; args = strings.TrimSpace(args) { 17 | var path string 18 | Switch: 19 | switch args[0] { 20 | default: 21 | i := len(args) 22 | for j, c := range args { 23 | if unicode.IsSpace(c) { 24 | i = j 25 | break 26 | } 27 | } 28 | path = args[:i] 29 | args = args[i:] 30 | 31 | case '`': 32 | i := strings.Index(args[1:], "`") 33 | if i < 0 { 34 | return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) 35 | } 36 | path = args[1 : 1+i] 37 | args = args[1+i+1:] 38 | 39 | case '"': 40 | i := 1 41 | for ; i < len(args); i++ { 42 | if args[i] == '\\' { 43 | i++ 44 | continue 45 | } 46 | if args[i] == '"' { 47 | q, err := strconv.Unquote(args[:i+1]) 48 | if err != nil { 49 | return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1]) 50 | } 51 | path = q 52 | args = args[i+1:] 53 | break Switch 54 | } 55 | } 56 | if i >= len(args) { 57 | return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) 58 | } 59 | } 60 | 61 | if args != "" { 62 | r, _ := utf8.DecodeRuneInString(args) 63 | if !unicode.IsSpace(r) { 64 | return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) 65 | } 66 | } 67 | list = append(list, path) 68 | } 69 | return list, nil 70 | } 71 | -------------------------------------------------------------------------------- /rcp/fixed/integer.go: -------------------------------------------------------------------------------- 1 | package fixed 2 | 3 | type Int8 int8 4 | type Point8 = Point[Int8] 5 | type Rectangle8 = Rectangle[Int8] 6 | 7 | func (x Int8) Mul(y Int8) Int8 { return x * y } 8 | func (x Int8) Div(y Int8) Int8 { return x / y } 9 | 10 | type UInt8 uint8 11 | type PointU8 = Point[UInt8] 12 | type RectangleU8 = Rectangle[UInt8] 13 | 14 | func (x UInt8) Mul(y UInt8) UInt8 { return x * y } 15 | func (x UInt8) Div(y UInt8) UInt8 { return x / y } 16 | 17 | type Int16 int16 18 | type Point16 = Point[Int16] 19 | type Rectangle16 = Rectangle[Int16] 20 | 21 | func (x Int16) Mul(y Int16) Int16 { return x * y } 22 | func (x Int16) Div(y Int16) Int16 { return x / y } 23 | 24 | type UInt16 uint16 25 | type PointU16 = Point[UInt16] 26 | type RectangleU16 = Rectangle[UInt16] 27 | 28 | func (x UInt16) Mul(y UInt16) UInt16 { return x * y } 29 | func (x UInt16) Div(y UInt16) UInt16 { return x / y } 30 | 31 | type Int32 int32 32 | type Point32 = Point[Int32] 33 | type Rectangle32 = Rectangle[Int32] 34 | 35 | func (x Int32) Mul(y Int32) Int32 { return x * y } 36 | func (x Int32) Div(y Int32) Int32 { return x / y } 37 | 38 | type UInt32 uint32 39 | type PointU32 = Point[UInt32] 40 | type RectangleU32 = Rectangle[UInt32] 41 | 42 | func (x UInt32) Mul(y UInt32) UInt32 { return x * y } 43 | func (x UInt32) Div(y UInt32) UInt32 { return x / y } 44 | 45 | type Int64 int64 46 | type Point64 = Point[Int64] 47 | type Rectangle64 = Rectangle[Int64] 48 | 49 | func (x Int64) Mul(y Int64) Int64 { return x * y } 50 | func (x Int64) Div(y Int64) Int64 { return x / y } 51 | 52 | type UInt64 uint64 53 | type PointU64 = Point[UInt64] 54 | type RectangleU64 = Rectangle[UInt64] 55 | 56 | func (x UInt64) Mul(y UInt64) UInt64 { return x * y } 57 | func (x UInt64) Div(y UInt64) UInt64 { return x / y } 58 | -------------------------------------------------------------------------------- /drivers/carts/isviewer/isviewer.go: -------------------------------------------------------------------------------- 1 | // Package isviewer provides a logging via an ISViewer devices. 2 | // 3 | // ISViewer was a development cartridge with logging capabilities. While the 4 | // original hardware is not available anymore, it can be emulated by ares 5 | // emulator as well as SummerCart64. To enable ISViewer emulation on 6 | // SummerCart64 use the sc64deployer utility: 7 | // 8 | // sc64deployer debug --isv 0x03FF0000 9 | package isviewer 10 | 11 | import ( 12 | "github.com/clktmr/n64/rcp/cpu" 13 | "github.com/clktmr/n64/rcp/periph" 14 | ) 15 | 16 | func regs() *registers { return cpu.MMIO[registers](0x13ff_0000) } 17 | 18 | const token = 0x49533634 19 | const bufferSize = 0x1400_0000 - 0x13ff_0020 20 | 21 | type registers struct { 22 | token periph.U32 23 | readPtr periph.U32 24 | _ [3]periph.U32 25 | writePtr periph.U32 26 | _ [2]periph.U32 27 | buf [bufferSize / 4]periph.U32 28 | } 29 | 30 | var piBuf = periph.NewDevice(cpu.PhysicalAddressSlice(regs().buf[:]), bufferSize) 31 | 32 | type Cart struct{} 33 | 34 | // Probe reports the ISV 35 | func Probe() *Cart { 36 | regs().token.Store(0xbeefcafe) 37 | if regs().token.Load() == 0xbeefcafe { 38 | regs().readPtr.Store(0) 39 | regs().writePtr.Store(0) 40 | return &Cart{} 41 | } 42 | return nil 43 | } 44 | 45 | func (v *Cart) Write(p []byte) (n int, err error) { 46 | for len(p) > 0 { 47 | var nn int 48 | nn, err = piBuf.WriteAt(p[:min(len(p), piBuf.Size())], 0) 49 | if err != nil { 50 | return 51 | } 52 | p = p[nn:] 53 | 54 | regs().readPtr.Store(0) 55 | regs().writePtr.Store(uint32(nn)) 56 | regs().token.Store(token) 57 | 58 | for regs().readPtr.Load() != regs().writePtr.Load() { 59 | // wait 60 | } 61 | 62 | regs().token.Store(0x0) 63 | n += nn 64 | } 65 | 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /drivers/controller/poll.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/clktmr/n64/debug" 5 | "github.com/clktmr/n64/rcp/serial" 6 | "github.com/clktmr/n64/rcp/serial/joybus" 7 | ) 8 | 9 | var ( 10 | cmdAllStates *serial.CommandBlock 11 | cmdAllStatesPorts [4]joybus.ControllerStateCommand 12 | 13 | cmdAllInfo *serial.CommandBlock 14 | cmdAllInfoPorts [4]joybus.InfoCommand 15 | ) 16 | 17 | func init() { 18 | var err error 19 | 20 | cmdAllStates = serial.NewCommandBlock(serial.CmdConfigureJoybus) 21 | for i := range cmdAllStatesPorts { 22 | cmdAllStatesPorts[i], err = joybus.NewControllerStateCommand(cmdAllStates) 23 | debug.AssertErrNil(err) 24 | } 25 | err = joybus.ControlByte(cmdAllStates, joybus.CtrlAbort) 26 | debug.AssertErrNil(err) 27 | 28 | cmdAllInfo = serial.NewCommandBlock(serial.CmdConfigureJoybus) 29 | for i := range cmdAllInfoPorts { 30 | cmdAllInfoPorts[i], err = joybus.NewInfoCommand(cmdAllInfo) 31 | debug.AssertErrNil(err) 32 | } 33 | err = joybus.ControlByte(cmdAllInfo, joybus.CtrlAbort) 34 | debug.AssertErrNil(err) 35 | } 36 | 37 | // Updates the state of all four controllers and stores them in states. Blocks 38 | // until all states were received. 39 | func Poll(states *[4]Controller) { 40 | // poll info 41 | for _, cmd := range cmdAllInfoPorts { 42 | cmd.Reset() 43 | } 44 | serial.Run(cmdAllInfo) 45 | 46 | // poll states 47 | for _, cmd := range cmdAllStatesPorts { 48 | cmd.Reset() 49 | } 50 | 51 | serial.Run(cmdAllStates) 52 | 53 | p := states 54 | for i := range p { 55 | var err error 56 | 57 | p[i].Port.number = uint8(i + 1) 58 | p[i].Port.last = p[i].Port.current 59 | dev, flags, err := cmdAllInfoPorts[i].Info() 60 | p[i].Port.current.device = dev 61 | p[i].Port.current.flags = flags 62 | p[i].Port.err = err 63 | 64 | p[i].last = p[i].current 65 | cur := &p[i].current 66 | cur.down, cur.xAxis, cur.yAxis, p[i].err = cmdAllStatesPorts[i].State() 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /drivers/controller/pakfs/dir.go: -------------------------------------------------------------------------------- 1 | package pakfs 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | "time" 7 | ) 8 | 9 | // pakfs doesn't support subdirectories, only the root dir exists. 10 | type rootDir struct { 11 | fs *FS 12 | entries []fs.DirEntry 13 | } 14 | 15 | // fs.File implementation 16 | func (d *rootDir) Stat() (fs.FileInfo, error) { return d, nil } 17 | func (d *rootDir) Read(p []byte) (int, error) { return 0, fs.ErrInvalid } 18 | func (d *rootDir) Close() error { return nil } 19 | 20 | func (d *rootDir) ReadDir(n int) (root []fs.DirEntry, err error) { 21 | if d.entries == nil { 22 | d.entries = d.fs.ReadDirRoot() 23 | } 24 | 25 | cut := len(d.entries) 26 | if n > 0 { 27 | if n >= cut { 28 | err = io.EOF 29 | n = cut 30 | } 31 | cut = n 32 | } 33 | root = d.entries[:cut] 34 | d.entries = d.entries[cut:] 35 | 36 | return 37 | } 38 | 39 | // fs.FileInfo implementation 40 | func (d *rootDir) Name() string { return "." } 41 | func (d *rootDir) Size() int64 { return 0 } 42 | func (d *rootDir) Mode() fs.FileMode { return fs.ModeDir | 0777 } 43 | func (d *rootDir) ModTime() time.Time { return time.Time{} } 44 | func (d *rootDir) IsDir() bool { return true } 45 | func (d *rootDir) Sys() any { return nil } 46 | 47 | // Holds only the filename and tries to open it on Info(). This resembles the 48 | // behavoiur of the os package, at least on linux ext4. fs.FileInfoToDirEntry 49 | // shouldn't be used create dir entries on writable filesystems, because Name() 50 | // will fail if the underlying file is (re)moved. 51 | type dirEntry struct { 52 | fs *FS 53 | name string 54 | } 55 | 56 | func (p *dirEntry) Name() string { return p.name } 57 | func (p *dirEntry) IsDir() bool { return p.Type().IsDir() } 58 | func (p *dirEntry) Type() fs.FileMode { return 0 } 59 | 60 | func (p *dirEntry) Info() (fs.FileInfo, error) { 61 | f, err := p.fs.Open(p.name) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return f.Stat() 66 | } 67 | -------------------------------------------------------------------------------- /rcp/mmio_test.go: -------------------------------------------------------------------------------- 1 | package rcp_test 2 | 3 | import ( 4 | "bytes" 5 | "embedded/mmio" 6 | "testing" 7 | 8 | "github.com/clktmr/n64/rcp" 9 | "github.com/clktmr/n64/rcp/cpu" 10 | n64testing "github.com/clktmr/n64/testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 14 | 15 | func TestReadWriteIO(t *testing.T) { 16 | testdata := []byte("Hello everybody, I'm Bonzo!") 17 | initBytes := cpu.MakePaddedSliceAligned[byte](64, 4) 18 | for i := range initBytes { 19 | initBytes[i] = byte(i+0x30) % 64 20 | } 21 | 22 | for busAlign := 0; busAlign < 7; busAlign += 1 { 23 | for sliceAlign := 0; sliceAlign < 3; sliceAlign += 1 { 24 | for sliceLen := 0; sliceLen < len(testdata); sliceLen += 1 { 25 | txbuf := cpu.MakePaddedSliceAligned[byte](64, 4) 26 | rxbuf := cpu.MakePaddedSliceAligned[byte](64, 4) 27 | 28 | rcp.WriteIO[*mmio.U32](0x0400_0000, initBytes) 29 | 30 | tx := txbuf[sliceAlign : sliceAlign+sliceLen] 31 | copy(tx, testdata) 32 | rcp.WriteIO[*mmio.U32](0x0400_0000+cpu.Addr(busAlign), tx) 33 | 34 | rx := rxbuf[sliceAlign : sliceAlign+sliceLen] 35 | rcp.ReadIO[*mmio.U32](0x0400_0000+cpu.Addr(busAlign), rx) 36 | 37 | if !bytes.Equal(tx, rx) { 38 | t.Logf("tx %q", string(tx)) 39 | t.Logf("rx %q", string(rx)) 40 | t.Error("mismatch at ", busAlign, sliceAlign, sliceLen) 41 | } 42 | 43 | rcp.ReadIO[*mmio.U32](0x0400_0000, rxbuf) 44 | start := busAlign 45 | if !bytes.Equal(rxbuf[:start], initBytes[:start]) { 46 | t.Logf("got %q", string(rxbuf[:start])) 47 | t.Logf("expected %q", string(initBytes[:start])) 48 | t.Error("modified preceding data", busAlign, sliceAlign, sliceLen) 49 | } 50 | end := busAlign + sliceLen 51 | if !bytes.Equal(rxbuf[end:], initBytes[end:]) { 52 | t.Logf("got %q", string(rxbuf[end:])) 53 | t.Logf("expected %q", string(initBytes[end:])) 54 | t.Error("modified succeeding data", busAlign, sliceAlign, sliceLen) 55 | } 56 | if t.Failed() { 57 | t.Fatal() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /drivers/cartfs/testdata/ascii.txt: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn 2 | !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno 3 | "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop 4 | #$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopq 5 | $%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr 6 | %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrs 7 | &'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrst 8 | '()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstu 9 | ()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuv 10 | )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvw 11 | *+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwx 12 | +,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxy 13 | ,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz 14 | -./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{ 15 | ./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{| 16 | /0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} 17 | 0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} 18 | 123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} ! 19 | 23456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !" 20 | 3456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"# 21 | 456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"#$ 22 | 56789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"#$% 23 | 6789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"#$%& 24 | 789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"#$%&' 25 | 89:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} !"#$%&'( 26 | -------------------------------------------------------------------------------- /rcp/rdp/combiner.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | const ( 4 | CombineCombined CombineSource = iota 5 | CombineTex0 6 | CombineTex1 7 | CombinePrimitive 8 | CombineShade 9 | CombineEnvironment 10 | 11 | CombineAColorOne CombineSource = 6 12 | CombineAColorNoise CombineSource = 7 13 | CombineAColorZero CombineSource = 8 14 | 15 | CombineAAlphaOne CombineSource = 6 16 | CombineAAlphaZero CombineSource = 7 17 | 18 | CombineBColorCenter CombineSource = 6 19 | CombineBColorK4 CombineSource = 7 20 | CombineBColorZero CombineSource = 8 21 | 22 | CombineBAlphaOne CombineSource = 6 23 | CombineBAlphaZero CombineSource = 7 24 | 25 | CombineCColorCenter CombineSource = 6 26 | CombineCColorCombinedAlpha CombineSource = 7 27 | CombineCColorTex0Alpha CombineSource = 8 28 | CombineCColorTex1Alpha CombineSource = 9 29 | CombineCColorPrimitiveAlpha CombineSource = 10 30 | CombineCColorShadeAlpha CombineSource = 11 31 | CombineCColorEnvironmentAlpha CombineSource = 12 32 | CombineCColorLODFraction CombineSource = 13 33 | CombineCColorPrimitiveLODFraction CombineSource = 14 34 | CombineCColorK5 CombineSource = 15 35 | CombineCColorZero CombineSource = 16 36 | 37 | CombineCAlphaPrimitiveLODFraction CombineSource = 6 38 | CombineCAlphaZero CombineSource = 7 39 | 40 | CombineDColorOne CombineSource = 6 41 | CombineDColorZero CombineSource = 7 42 | 43 | CombineDAlphaOne CombineSource = 6 44 | CombineDAlphaZero CombineSource = 7 45 | ) 46 | 47 | // The ColorCombiner computes it's output with the equation `(A-B)*C + D`, where 48 | // the inputs A, B, C and D can be choosen from the predefined CombineSource 49 | // values. Color and alpha are calculated separately. 50 | // If CycleTypeTwo is active two passes can be defined, where the second pass 51 | // can use the first pass output as it's input. 52 | type CombineMode struct{ One, Two CombinePass } 53 | type CombinePass struct{ RGB, Alpha CombineParams } 54 | type CombineParams struct{ A, B, C, D CombineSource } 55 | type CombineSource uint64 56 | -------------------------------------------------------------------------------- /tools/ucode/main.go: -------------------------------------------------------------------------------- 1 | package ucode 2 | 3 | import ( 4 | "debug/elf" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "slices" 11 | "strings" 12 | 13 | "github.com/clktmr/n64/rcp/cpu" 14 | "github.com/clktmr/n64/rcp/rsp/ucode" 15 | ) 16 | 17 | const usageString = `RSP microcode converter. 18 | 19 | Usage: %s [flags] 20 | 21 | ` 22 | 23 | var ( 24 | flags = flag.NewFlagSet("ucode", flag.ExitOnError) 25 | 26 | infile string 27 | ) 28 | 29 | func usage() { 30 | fmt.Fprintf(flags.Output(), usageString, "ucode") 31 | flags.PrintDefaults() 32 | } 33 | 34 | func Main(args []string) { 35 | flags.Usage = usage 36 | flags.Parse(args[1:]) 37 | 38 | if flags.NArg() == 1 { 39 | infile = flags.Arg(0) 40 | } else { 41 | log.Println("too many arguments") 42 | flags.Usage() 43 | os.Exit(1) 44 | } 45 | 46 | outfile, _ := strings.CutSuffix(infile, ".elf") 47 | outfile += ".ucode" 48 | 49 | elffile, err := elf.Open(infile) 50 | if err != nil { 51 | log.Fatalln(err) 52 | } 53 | defer elffile.Close() 54 | 55 | var ucode = &ucode.UCode{ 56 | Name: filepath.Base(infile), 57 | Entry: cpu.Addr(elffile.Entry), 58 | Text: sectionData(elffile, ".text"), 59 | Data: sectionData(elffile, ".data"), 60 | } 61 | 62 | w, err := os.Create(outfile) 63 | if err != nil { 64 | log.Fatalln(err) 65 | } 66 | defer w.Close() 67 | 68 | err = ucode.Store(w) 69 | if err != nil { 70 | log.Fatalln(err) 71 | } 72 | } 73 | 74 | func sectionData(elffile *elf.File, section string) []byte { 75 | s := elffile.Section(section) 76 | if s == nil { 77 | log.Fatalln("missing section:", section) 78 | } 79 | data, err := s.Data() 80 | if err != nil { 81 | log.Fatalln("reading section:", err) 82 | } 83 | return data 84 | } 85 | 86 | func symbolValue(elffile *elf.File, name string) uint64 { 87 | syms, err := elffile.Symbols() 88 | if err != nil { 89 | log.Fatalln("read symbols:", err) 90 | } 91 | idx := slices.IndexFunc(syms, func(sym elf.Symbol) bool { 92 | return sym.Name == name 93 | }) 94 | if idx == -1 { 95 | log.Fatalln("read symbol:", err) 96 | } 97 | return syms[idx].Value 98 | } 99 | -------------------------------------------------------------------------------- /rcp/interrupt.go: -------------------------------------------------------------------------------- 1 | package rcp 2 | 3 | import ( 4 | "embedded/rtos" 5 | 6 | _ "unsafe" // for linkname 7 | ) 8 | 9 | const ( 10 | IrqRcp rtos.IRQ = 3 // RCP forwards an interrupt by another peripheral 11 | IrqCart rtos.IRQ = 4 // Interrupt caused by a peripheral on the cartridge 12 | IrqPrenmi rtos.IRQ = 5 // User has pushed reset button on console 13 | IrqRdbRead rtos.IRQ = 6 // Devboard has read the value in the RDB port 14 | IrqRdbWrite rtos.IRQ = 7 // Devboard has written a value in the RDB port 15 | ) 16 | 17 | var handlers = [6]func(){} 18 | 19 | func init() { 20 | DisableInterrupts(^interruptFlag(0)) 21 | IrqRcp.Enable(rtos.IntPrioLow, 0) 22 | IrqPrenmi.Enable(rtos.IntPrioLow, 0) 23 | } 24 | 25 | //go:linkname rcpHandler IRQ3_Handler 26 | //go:interrupthandler 27 | func rcpHandler() { 28 | pending := regs().interrupt.Load() 29 | mask := regs().mask.Load() 30 | irq := 0 31 | for flag := interruptFlag(1); flag != IntrLast; flag = flag << 1 { 32 | if flag&pending != 0 && flag&mask != 0 { 33 | handler := handlers[irq] 34 | if handler == nil { 35 | panic("unhandled interrupt") 36 | } 37 | handler() 38 | } 39 | irq += 1 40 | } 41 | } 42 | 43 | func SetHandler(int interruptFlag, handler func()) { 44 | en, prio, _ := IrqRcp.Status(0) 45 | IrqRcp.Disable(0) 46 | 47 | irq := 0 48 | for flag := interruptFlag(1); flag != IntrLast; flag = flag << 1 { 49 | if flag&int != 0 { 50 | handlers[irq] = handler 51 | break 52 | } 53 | irq += 1 54 | } 55 | 56 | if en { 57 | IrqRcp.Enable(prio, 0) 58 | } 59 | } 60 | 61 | func Handler(int interruptFlag) func() { 62 | irq := 0 63 | for flag := interruptFlag(1); flag != IntrLast; flag = flag << 1 { 64 | if flag&int != 0 { 65 | return handlers[irq] 66 | } 67 | irq += 1 68 | } 69 | return nil 70 | } 71 | 72 | // Reset signals that the console's reset button was pressed. The hardware 73 | // reboots with the button's release, but not before 500ms have passed. 74 | var Reset rtos.Cond 75 | 76 | //go:linkname prenmiHandler IRQ5_Handler 77 | //go:interrupthandler 78 | func prenmiHandler() { 79 | IrqPrenmi.Disable(0) 80 | Reset.Signal() 81 | } 82 | -------------------------------------------------------------------------------- /testing/testmain.go: -------------------------------------------------------------------------------- 1 | // Package testing provides utilities for writing n64 specific tests. 2 | package testing 3 | 4 | import ( 5 | "embedded/rtos" 6 | "fmt" 7 | "image" 8 | "io" 9 | "os" 10 | "syscall" 11 | "testing" 12 | 13 | "github.com/clktmr/n64/drivers/carts" 14 | "github.com/clktmr/n64/drivers/carts/isviewer" 15 | "github.com/clktmr/n64/drivers/console" 16 | "github.com/clktmr/n64/drivers/controller" 17 | _ "github.com/clktmr/n64/machine" 18 | "github.com/clktmr/n64/rcp/serial/joybus" 19 | "github.com/clktmr/n64/rcp/texture" 20 | "github.com/clktmr/n64/rcp/video" 21 | 22 | "github.com/embeddedgo/fs/termfs" 23 | ) 24 | 25 | // TestMain should be used as TestMain for n64 specific tests. 26 | func TestMain(m *testing.M) { 27 | var err error 28 | var cart carts.Cart 29 | 30 | // Redirect stdout and stderr either to cart's logger 31 | if cart = carts.ProbeAll(); cart == nil { 32 | panic("no logging peripheral found") 33 | } 34 | 35 | guiconsole := console.NewConsole() 36 | 37 | fs := termfs.NewLight("termfs", nil, io.MultiWriter(cart, guiconsole)) 38 | rtos.Mount(fs, "/dev/console") 39 | os.Stdout, err = os.OpenFile("/dev/console", syscall.O_WRONLY, 0) 40 | if err != nil { 41 | panic(err) 42 | } 43 | os.Stderr = os.Stdout 44 | 45 | // The default syswriter is a failsafe ISViewer implementation, which 46 | // will print panics. 47 | if isviewer.Probe() == nil { 48 | fmt.Print("\nWARN: no isviewer found, print() and panic() won't printed\n\n") 49 | } 50 | 51 | // TODO find a way to pass these from the 'go test' command 52 | os.Args = append(os.Args, "-test.v") 53 | os.Args = append(os.Args, "-test.bench=.") 54 | os.Args = append(os.Args, "-test.benchmem") 55 | 56 | print("Hold START to enable interactive test.. ") 57 | inputs := [4]controller.Controller{} 58 | controller.Poll(&inputs) 59 | if inputs[0].Down()&joybus.ButtonStart == 0 { 60 | os.Args = append(os.Args, "-test.short") 61 | println("skipping") 62 | } else { 63 | println("ok") 64 | } 65 | 66 | video.Setup(false) 67 | res := video.NativeResolution() 68 | res.X /= 2 69 | fb := texture.NewFramebuffer(image.Rectangle{Max: res}) 70 | video.SetFramebuffer(fb) 71 | 72 | os.Exit(m.Run()) 73 | } 74 | -------------------------------------------------------------------------------- /fonts/face.go: -------------------------------------------------------------------------------- 1 | // Package fonts implements loading of fonts optimized for Nintendo 64. 2 | // 3 | // These fonts can be generated by [github.com/clktmr/n64/tools/mkfont]. 4 | package fonts 5 | 6 | import ( 7 | "image" 8 | 9 | "github.com/clktmr/n64/rcp/fixed" 10 | "github.com/embeddedgo/display/font/subfont" 11 | ) 12 | 13 | // GlyphMap returns an image containing all glyphs of a [Subfont] and a rect 14 | // describing subimage that represents the glyph. All images returned by 15 | // GlyphMap are guaranteed to have the same format/type. 16 | // This interface is an optimization for the [subfont.Data] interface. Subfonts 17 | // can implement this for optimization, to avoid frequent changes in the RDP's 18 | // texture image. 19 | type GlyphMap interface { 20 | GlyphMap(r rune) (img image.Image, rect image.Rectangle, origin image.Point, advance int) 21 | } 22 | 23 | type Glyph struct { 24 | Origin fixed.PointU8 25 | Rect fixed.RectangleU8 26 | Advance fixed.UInt8 27 | } 28 | 29 | // Face is a [subfont.Face] which implements the [GlyphMap] optimization. 30 | type Face struct { 31 | subfont.Face 32 | } 33 | 34 | // GlyphMap returns the image containing a glyph map with the specified rune. 35 | // The subimage defined by rect contains the requested glyph, with it's origin 36 | // and vertical advance in pixels. 37 | // 38 | //go:nosplit 39 | func (f *Face) GlyphMap(r rune) (img image.Image, rect image.Rectangle, origin image.Point, advance int) { 40 | sf := getSubfont(f, r) 41 | if sf == nil { 42 | // try to use rune(0) to render unsupported codepoints 43 | r = 0 44 | sf = getSubfont(f, r) 45 | if sf == nil { 46 | return 47 | } 48 | } 49 | if sfd, ok := sf.Data.(*SubfontData); ok { 50 | return sfd.GlyphMap(int(r - sf.First)) 51 | } 52 | img, origin, advance = sf.Data.Glyph(int(r - sf.First)) 53 | rect = img.Bounds() 54 | return 55 | } 56 | 57 | //go:nosplit 58 | func getSubfont(f *Face, r rune) (sf *subfont.Subfont) { 59 | // TODO: binary search 60 | for _, sf = range f.Subfonts { 61 | if sf != nil && sf.First <= r && r <= sf.Last { 62 | return sf 63 | } 64 | } 65 | if f.Loader == nil { 66 | return nil 67 | } 68 | sf, f.Subfonts = f.Loader.Load(r, f.Subfonts) 69 | return sf 70 | } 71 | -------------------------------------------------------------------------------- /drivers/display/display.go: -------------------------------------------------------------------------------- 1 | // Package display provides video output by managing framebuffers and video DAC 2 | // behind the scenes. 3 | package display 4 | 5 | import ( 6 | "image" 7 | "time" 8 | 9 | "github.com/clktmr/n64/rcp" 10 | "github.com/clktmr/n64/rcp/rdp" 11 | "github.com/clktmr/n64/rcp/texture" 12 | "github.com/clktmr/n64/rcp/video" 13 | ) 14 | 15 | // Display implements a vsynced, double buffered framebuffer. 16 | type Display struct { 17 | read, write *texture.Texture 18 | start time.Time 19 | 20 | rendertime, frametime time.Duration 21 | cmd, pipe, tmem uint32 22 | } 23 | 24 | func NewDisplay(resolution image.Point, bpp video.ColorDepth) *Display { 25 | fb := &Display{} 26 | 27 | bounds := image.Rectangle{Max: resolution} 28 | if bpp == video.BPP16 { 29 | fb.read = texture.NewRGBA16(bounds) 30 | fb.write = texture.NewRGBA16(bounds) 31 | } else if bpp == video.BPP32 { 32 | fb.read = texture.NewRGBA32(bounds) 33 | fb.write = texture.NewRGBA32(bounds) 34 | } 35 | 36 | video.SetFramebuffer(fb.read) 37 | 38 | fb.start = time.Now() 39 | 40 | return fb 41 | } 42 | 43 | // Swap returns the next framebuffer for rendering. The framebuffer returned by 44 | // the last call becomes invalid. Blocks until a framebuffer is available for 45 | // rendering. 46 | func (p *Display) Swap() *texture.Texture { 47 | p.rendertime = time.Since(p.start) 48 | p.cmd, p.pipe, p.tmem = rdp.Busy() 49 | 50 | p.read, p.write = p.write, p.read 51 | video.SetFramebuffer(p.read) 52 | 53 | if video.VSync { 54 | video.VBlank.Wait(0) 55 | if !video.VBlank.Wait(1 * time.Second) { 56 | panic("vblank timeout") 57 | } 58 | } 59 | 60 | p.frametime = time.Since(p.start) 61 | p.start = time.Now() 62 | 63 | return p.write 64 | } 65 | 66 | func (p *Display) FPS() float32 { 67 | return 1e9 / float32(p.frametime) 68 | } 69 | 70 | func (p *Display) Duration() time.Duration { 71 | return p.rendertime 72 | } 73 | 74 | func (p *Display) Utilization() (cmd, pipe, tmem time.Duration) { 75 | cmd = time.Duration(float32(p.cmd) * (1e9 / rcp.ClockSpeed)) 76 | pipe = time.Duration(float32(p.pipe) * (1e9 / rcp.ClockSpeed)) 77 | tmem = time.Duration(float32(p.tmem) * (1e9 / rcp.ClockSpeed)) 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /drivers/console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | 8 | "github.com/clktmr/n64/drivers/controller" 9 | "github.com/clktmr/n64/drivers/draw" 10 | "github.com/clktmr/n64/fonts/basicfont12" 11 | "github.com/clktmr/n64/rcp/rdp" 12 | "github.com/clktmr/n64/rcp/serial/joybus" 13 | "github.com/clktmr/n64/rcp/video" 14 | ) 15 | 16 | type Console struct { 17 | buf bytes.Buffer 18 | scroll image.Point 19 | } 20 | 21 | var font = basicfont12.NewFace() 22 | 23 | func NewConsole() *Console { return &Console{} } 24 | 25 | func (v *Console) Write(p []byte) (n int, err error) { 26 | n, err = v.buf.Write(p) 27 | v.Draw() 28 | rdp.RDP.Flush() 29 | return 30 | } 31 | 32 | func (v *Console) Update(input controller.Controller) { 33 | pressed := input.Pressed() 34 | switch { 35 | case pressed&joybus.ButtonCUp != 0: 36 | v.scroll.Y += 1 37 | case pressed&joybus.ButtonCDown != 0: 38 | v.scroll.Y -= 1 39 | case pressed&joybus.ButtonCLeft != 0: 40 | v.scroll.X = min(0, v.scroll.X+int(font.Advance(0))) 41 | case pressed&joybus.ButtonCRight != 0: 42 | v.scroll.X -= int(font.Advance(0)) 43 | } 44 | } 45 | 46 | // FIXME sync via mutex with Write? 47 | func (v *Console) Draw() { 48 | fb := video.Framebuffer() 49 | if fb == nil { 50 | return 51 | } 52 | bounds := fb.Bounds().Inset(20) 53 | 54 | height := 0 55 | b := v.buf.Bytes() 56 | bb := b 57 | lines := b[:0] 58 | maxLines := bounds.Dy() / int(font.Height) 59 | lineCnt := 0 60 | skipped := 0 61 | for height < bounds.Dy() { 62 | lineCnt++ 63 | 64 | idx := bytes.LastIndexByte(bb, '\n') 65 | if idx == -1 { 66 | lines = b 67 | break 68 | } 69 | bb, lines = b[:idx], b[idx:] 70 | 71 | if skipped < v.scroll.Y { 72 | skipped++ 73 | } else { 74 | height += int(font.Height) 75 | } 76 | } 77 | if len(lines) > 0 && lines[0] == '\n' { 78 | lines = lines[1:] 79 | } 80 | 81 | v.scroll.Y = min(max(0, skipped), lineCnt-maxLines) 82 | 83 | bounds.Min.X += v.scroll.X 84 | pt := bounds.Min.Add(image.Pt(0, int(font.Ascent))) 85 | draw.Src.Draw(fb, fb.Bounds(), &image.Uniform{color.NRGBA{B: 0xff, A: 0xff}}, image.Point{}) 86 | draw.DrawText(fb, bounds, font, pt, image.White, nil, lines) 87 | } 88 | -------------------------------------------------------------------------------- /rcp/video/interrupt.go: -------------------------------------------------------------------------------- 1 | package video 2 | 3 | import ( 4 | "embedded/rtos" 5 | "image" 6 | 7 | "github.com/clktmr/n64/rcp" 8 | "github.com/clktmr/n64/rcp/cpu" 9 | "github.com/clktmr/n64/rcp/texture" 10 | ) 11 | 12 | // VBlank can be used to wait until the next vertical blank. 13 | var VBlank rtos.Cond 14 | 15 | // Consumed by interrupt handler 16 | var ( 17 | framebuffer rcp.IntrInput[*texture.Texture] 18 | scale rcp.IntrInput[image.Rectangle] 19 | ) 20 | 21 | func init() { 22 | rcp.SetHandler(rcp.IntrVideo, handler) 23 | rcp.EnableInterrupts(rcp.IntrVideo) 24 | } 25 | 26 | // The handler is guaranteed to never be called with a nil framebuffer. 27 | // 28 | //go:nosplit 29 | //go:nowritebarrierrec 30 | func handler() { 31 | regs().vCurrent.Store(0) // clears interrupt 32 | 33 | fb, _ := framebuffer.Get() 34 | if fb == nil { // only needed for Ares 35 | return 36 | } 37 | 38 | // update scale if it was changed 39 | if r, updated := scale.Get(); updated { 40 | fbSize := fb.Bounds().Size() 41 | videoSize := r.Size() 42 | regs().hVideo.Store(uint32(r.Min.X<<16 | r.Max.X)) 43 | regs().vVideo.Store(uint32(r.Min.Y<<16 | r.Max.Y)) 44 | regs().xScale.Store(uint32((fbSize.X<<10 + videoSize.X>>1) / (videoSize.X))) 45 | regs().yScale.Store(uint32((fbSize.Y<<10 + videoSize.Y>>2) / (videoSize.Y >> 1))) 46 | } 47 | 48 | updateFramebuffer(fb) 49 | 50 | VBlank.Signal() 51 | } 52 | 53 | // Updates the framebuffer based on currently configured framebuffer and field. 54 | // 55 | //go:nosplit 56 | func updateFramebuffer(fb *texture.Texture) { 57 | addr := fb.Addr() 58 | if regs().control.Load()&uint32(controlSerrate) != 0 { 59 | // Shift the framebuffer vertically based on current field. 60 | yScale := regs().yScale.Load() 61 | if regs().vCurrent.Load()&1 == 0 { // odd field 62 | yOffset := int(0xffff&yScale) >> 1 63 | // Move framebuffer address by a whole line if offset is 64 | // more than a pixel. 65 | for yOffset >= 1024 { 66 | yOffset -= 1024 67 | addr += cpu.Addr(fb.Format().Bytes(fb.Stride())) 68 | } 69 | yScale = (uint32(yOffset)<<16 | 0xffff®s().yScale.Load()) 70 | } else { // even field 71 | yScale = (0xffff & regs().yScale.Load()) 72 | } 73 | regs().yScale.Store(yScale) 74 | } 75 | regs().origin.Store(addr) 76 | } 77 | -------------------------------------------------------------------------------- /tools/n64go/main.go: -------------------------------------------------------------------------------- 1 | // n64go bundles all commands into a single executable. Run available commands 2 | // with: 3 | // 4 | // n64go [arguments] 5 | // 6 | // The commands are: 7 | // 8 | // - [github.com/clktmr/n64/tools/rom] convert and execute elf to n64 ROMs 9 | // - [github.com/clktmr/n64/tools/texture] generate textures to be used on the n64 10 | // - [github.com/clktmr/n64/tools/font] generate fonts to be used on the n64 11 | // - [github.com/clktmr/n64/tools/pakfs] modify and inspect pakfs images 12 | // - [github.com/clktmr/n64/tools/ucode] dump rsp microcode elf to binary 13 | // - [github.com/clktmr/n64/tools/toolexec] used as 'go build -toolexec' parameter 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "log" 20 | "os" 21 | 22 | "github.com/clktmr/n64/tools/font" 23 | "github.com/clktmr/n64/tools/pakfs" 24 | "github.com/clktmr/n64/tools/rom" 25 | "github.com/clktmr/n64/tools/texture" 26 | "github.com/clktmr/n64/tools/toolexec" 27 | "github.com/clktmr/n64/tools/ucode" 28 | ) 29 | 30 | const usageString = `n64go is a tool for development of Nintendo 64 ROMs. 31 | 32 | Usage: 33 | 34 | %s [arguments] 35 | 36 | The commands are: 37 | 38 | rom convert and execute elf to n64 ROMs 39 | texture convert images to n64 textures 40 | font generate fonts to be used on the n64 41 | pakfs modify and inspect pakfs images 42 | ucode dump rsp microcode elf to binary 43 | toolexec used as 'go build -toolexec' parameter 44 | ` 45 | 46 | func usage() { 47 | fmt.Fprintf(flag.CommandLine.Output(), usageString, os.Args[0]) 48 | flag.PrintDefaults() 49 | } 50 | 51 | func main() { 52 | log.Default().SetFlags(0) 53 | flag.Usage = usage 54 | flag.Parse() 55 | 56 | if flag.NArg() < 1 { 57 | flag.Usage() 58 | os.Exit(1) 59 | } 60 | 61 | switch flag.Arg(0) { 62 | case "rom": 63 | rom.Main(flag.Args()) 64 | case "pakfs": 65 | pakfs.Main(flag.Args()) 66 | case "toolexec": 67 | toolexec.Main(flag.Args()) 68 | case "font": 69 | font.Main(flag.Args()) 70 | case "texture": 71 | texture.Main(flag.Args()) 72 | case "ucode": 73 | ucode.Main(flag.Args()) 74 | default: 75 | fmt.Fprintf(flag.CommandLine.Output(), "unknown command: %s\n", flag.Arg(0)) 76 | flag.Usage() 77 | os.Exit(1) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rcp/rsp/dma.go: -------------------------------------------------------------------------------- 1 | package rsp 2 | 3 | import ( 4 | "embedded/mmio" 5 | "errors" 6 | "io" 7 | "runtime" 8 | "sync" 9 | 10 | "github.com/clktmr/n64/debug" 11 | "github.com/clktmr/n64/rcp" 12 | "github.com/clktmr/n64/rcp/cpu" 13 | ) 14 | 15 | type Memory cpu.Addr 16 | 17 | var dmaMtx sync.Mutex 18 | 19 | // ReatAt loads bytes from RSP IMEM/DMEM into RDRAM via DMA 20 | func (m Memory) ReadAt(p []byte, off int64) (n int, err error) { 21 | return m.dma(p, off, true) 22 | } 23 | 24 | // WriteAt stores bytes from RDRAM to RSP IMEM/DMEM via DMA 25 | func (m Memory) WriteAt(p []byte, off int64) (n int, err error) { 26 | return m.dma(p, off, false) 27 | } 28 | 29 | func (m Memory) dma(p []byte, off int64, read bool) (n int, err error) { 30 | if off < 0 || off > 0x1000 { 31 | return 0, errors.New("offset out of bounds") 32 | } 33 | 34 | if len(p) == 0 { 35 | return 36 | } 37 | 38 | addr := cpu.Addr(m) + cpu.Addr(off) 39 | end := cpu.Addr(m) + 0x1000 40 | n = len(p) 41 | if n > int(end-addr) { 42 | n = int(end - addr) 43 | p = p[:n] 44 | err = io.EOF 45 | } 46 | 47 | head, tail := cpu.Pads(p) 48 | if (tail-head)&0x7 != 0 { 49 | tail &^= 0x7 // make sure length is 8 byte multiple 50 | } 51 | if (addr+cpu.Addr(head))%8 != 0 { 52 | // pp and addr have different alignment, fallback to mmio 53 | head = 0 54 | tail = 0 55 | } 56 | pp := p[head:tail] 57 | addr += cpu.Addr(head) 58 | 59 | debug.Assert(regs().status.LoadBits(halted|dmaBusy) != 0, "rsp: dma busy") 60 | 61 | dmaMtx.Lock() 62 | defer dmaMtx.Unlock() 63 | 64 | regs().rdramAddr.Store(cpu.PhysicalAddressSlice(pp)) 65 | regs().rspAddr.Store(addr) 66 | 67 | if read { 68 | if head != tail { 69 | cpu.InvalidateSlice(pp) 70 | regs().writeLen.Store(uint32(tail - head - 1)) 71 | waitDMA() 72 | } 73 | rcp.ReadIO[*mmio.U32](addr, p[:head]) 74 | rcp.ReadIO[*mmio.U32](addr+cpu.Addr(tail), p[tail:]) 75 | } else { 76 | rcp.WriteIO[*mmio.U32](addr, p[:head]) 77 | rcp.WriteIO[*mmio.U32](addr+cpu.Addr(tail), p[tail:]) 78 | if head != tail { 79 | cpu.WritebackSlice(pp) 80 | regs().readLen.Store(uint32(tail - head - 1)) 81 | waitDMA() 82 | } 83 | } 84 | 85 | return 86 | } 87 | 88 | // Blocks until DMA has finished. 89 | func waitDMA() { 90 | for regs().status.Load()&(dmaBusy|ioBusy) != 0 { 91 | runtime.Gosched() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /drivers/controller/joybus_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "io/fs" 5 | "testing" 6 | "time" 7 | 8 | "github.com/clktmr/n64/drivers/controller" 9 | "github.com/clktmr/n64/drivers/controller/pakfs" 10 | "github.com/clktmr/n64/rcp/serial/joybus" 11 | n64testing "github.com/clktmr/n64/testing" 12 | ) 13 | 14 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 15 | 16 | func TestControllerState(t *testing.T) { 17 | if testing.Short() { 18 | t.Skip("skipping in short mode") 19 | } 20 | 21 | t.Log("Press L+R+Start to end the test.") 22 | 23 | controllers := [4]controller.Controller{} 24 | for { 25 | controller.Poll(&controllers) 26 | for i, gamepad := range controllers { 27 | if gamepad.Plugged() { 28 | t.Log(i, "plugged") 29 | } 30 | if gamepad.Unplugged() { 31 | t.Log(i, "unplugged") 32 | } 33 | if gamepad.PakInserted() { 34 | go func() { 35 | t.Log(i, "pak inserted") 36 | pak, err := controller.ProbePak(byte(i)) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | switch pak := pak.(type) { 41 | case *controller.MemPak: 42 | t.Log(i, "controller pak detected") 43 | pfs, err := pakfs.Read(pak) 44 | if err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | for _, v := range pfs.ReadDirRoot() { 49 | info, err := v.Info() 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | t.Log(fs.FormatFileInfo(info)) 54 | } 55 | case *controller.RumblePak: 56 | t.Log(i, "rumble pak detected") 57 | for range 6 { 58 | err = pak.Toggle() 59 | if err != nil { 60 | t.Error(err) 61 | return 62 | } 63 | time.Sleep(500 * time.Millisecond) 64 | } 65 | case *controller.TransferPak: 66 | t.Log(i, "transfer pak detected") 67 | default: 68 | t.Log(i, "no pak type detected") 69 | } 70 | }() 71 | } 72 | if gamepad.PakRemoved() { 73 | t.Log(i, "pak removed") 74 | } 75 | if gamepad.Pressed() != 0 { 76 | t.Log(i, "pressed:", gamepad.Pressed()) 77 | if gamepad.Pressed()&joybus.ButtonReset != 0 { 78 | return 79 | } 80 | } 81 | if gamepad.Released() != 0 { 82 | t.Log(i, "released:", gamepad.Released()) 83 | } 84 | if gamepad.DX() != 0 || gamepad.DY() != 0 { 85 | t.Log(i, "X: ", gamepad.X(), "Y:", gamepad.Y()) 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /drivers/cartfs/file.go: -------------------------------------------------------------------------------- 1 | package cartfs 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/fs" 7 | "time" 8 | ) 9 | 10 | // dotFile is a file for the root directory, 11 | // which is omitted from the files list in a FS. 12 | var dotFile = &file{name: "./"} 13 | 14 | type file struct { 15 | name string 16 | size int64 17 | offset int64 18 | } 19 | 20 | var ( 21 | _ fs.FileInfo = (*file)(nil) 22 | _ fs.DirEntry = (*file)(nil) 23 | ) 24 | 25 | func (f *file) Name() string { _, elem, _ := split(f.name); return elem } 26 | func (f *file) Size() int64 { return f.size } 27 | func (f *file) ModTime() time.Time { return time.Time{} } 28 | func (f *file) IsDir() bool { _, _, isDir := split(f.name); return isDir } 29 | func (f *file) Sys() any { return nil } 30 | func (f *file) Type() fs.FileMode { return f.Mode().Type() } 31 | func (f *file) Info() (fs.FileInfo, error) { return f, nil } 32 | 33 | func (f *file) Mode() fs.FileMode { 34 | if f.IsDir() { 35 | return fs.ModeDir | 0555 36 | } 37 | return 0444 38 | } 39 | 40 | func (f *file) String() string { 41 | return fs.FormatFileInfo(f) 42 | } 43 | 44 | // An openFile is a regular file open for reading. 45 | type openFile struct { 46 | *io.SectionReader 47 | f *file // the file itself 48 | } 49 | 50 | func (f *openFile) Close() error { return nil } 51 | func (f *openFile) Stat() (fs.FileInfo, error) { return f.f, nil } 52 | 53 | // An openDir is a directory open for reading. 54 | type openDir struct { 55 | f *file // the directory file itself 56 | files []file // the directory contents 57 | offset int // the read offset, an index into the files slice 58 | } 59 | 60 | func (d *openDir) Close() error { return nil } 61 | func (d *openDir) Stat() (fs.FileInfo, error) { return d.f, nil } 62 | 63 | func (d *openDir) Read([]byte) (int, error) { 64 | return 0, &fs.PathError{Op: "read", Path: d.f.name, Err: errors.New("is a directory")} 65 | } 66 | 67 | func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) { 68 | n := len(d.files) - d.offset 69 | if n == 0 { 70 | if count <= 0 { 71 | return nil, nil 72 | } 73 | return nil, io.EOF 74 | } 75 | if count > 0 && n > count { 76 | n = count 77 | } 78 | list := make([]fs.DirEntry, n) 79 | for i := range list { 80 | list[i] = &d.files[d.offset+i] 81 | } 82 | d.offset += n 83 | return list, nil 84 | } 85 | -------------------------------------------------------------------------------- /drivers/carts/summercart64/usb_test.go: -------------------------------------------------------------------------------- 1 | package summercart64_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/clktmr/n64/drivers/carts/summercart64" 10 | "github.com/clktmr/n64/rcp/cpu" 11 | n64testing "github.com/clktmr/n64/testing" 12 | ) 13 | 14 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 15 | 16 | func mustSC64(t *testing.T) (sc64 *summercart64.Cart) { 17 | sc64 = summercart64.Probe() 18 | if sc64 == nil { 19 | t.Skip("needs SummerCart64") 20 | } 21 | return 22 | } 23 | 24 | func TestUSBRead(t *testing.T) { 25 | if testing.Short() { 26 | t.Skip("skipping in short mode") 27 | } 28 | sc64 := mustSC64(t) 29 | buf := cpu.MakePaddedSlice[byte](7) 30 | 31 | tests := map[string]struct { 32 | testdata []byte 33 | }{ 34 | "short": {[]byte("foo\n")}, 35 | "fit": {[]byte("barbaz\n")}, 36 | "long": {[]byte("summercart64\n")}, 37 | } 38 | 39 | for name, tc := range tests { 40 | t.Run(name, func(t *testing.T) { 41 | var err error 42 | var n int 43 | for n = -1; n != 0; n, err = sc64.Read(buf) { 44 | // discard, make we start with empty buffer 45 | } 46 | 47 | t.Logf("Please type \"%v\"", string(tc.testdata[:len(tc.testdata)-1])) 48 | 49 | for len(tc.testdata) > 0 { 50 | n, err = sc64.Read(buf) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | if n == 0 { 55 | continue 56 | } 57 | if n != min(len(tc.testdata), len(buf)) { 58 | t.Fatalf("length: %v", n) 59 | } 60 | if !bytes.Equal(buf[:n], tc.testdata[:n]) { 61 | t.Fatalf("data: exptected %v, got %v", tc.testdata[:n], buf[:n]) 62 | } 63 | tc.testdata = tc.testdata[n:] 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestSaveStorage(t *testing.T) { 70 | if testing.Short() { 71 | t.Skip("skipping in short mode") 72 | } 73 | 74 | sc64 := mustSC64(t) 75 | testBytes := []byte("hello savegame!") 76 | if sc64.SaveStorage().Size() == 0 { 77 | t.Skip("no savetype configured, use 'sc64deployer upload --save-type'") 78 | } 79 | 80 | buf := cpu.MakePaddedSlice[byte](len(testBytes)) 81 | 82 | _, err := sc64.SaveStorage().ReadAt(buf, 0) 83 | if err != nil && err != io.EOF { 84 | t.Fatal(err) 85 | } 86 | t.Log(strconv.Quote(string(buf))) 87 | 88 | _, err = sc64.SaveStorage().WriteAt([]byte("hello savegame!"), 0) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | t.Log("Save must have been written back") 94 | } 95 | -------------------------------------------------------------------------------- /machine/syswriter.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | _ "unsafe" // for linkname 5 | 6 | "github.com/clktmr/n64/rcp/cpu" 7 | "github.com/clktmr/n64/rcp/periph" 8 | ) 9 | 10 | func regs() *registers { return cpu.MMIO[registers](0x13ff_0000) } 11 | 12 | const token = 0x49533634 13 | const bufferSize = 512 // actually 64*1024 - 0x20, but ISViewer.buf will allocate this 14 | 15 | type registers struct { 16 | token periph.U32 17 | readPtr periph.U32 18 | _ [3]periph.U32 19 | writePtr periph.U32 20 | _ [2]periph.U32 21 | buf [bufferSize / 4]periph.U32 22 | } 23 | 24 | // DefaultWrite implements the targets print() function. It can be changed by 25 | // [embedded/rtos.SetSystemWriter]. This implementation writes to ISViewer 26 | // registers, regardless if a ISViewer is present or not. It's rather slow, 27 | // because it avoids using DMA. Only intended as a fail safe logger for early 28 | // boot and unrecovered panics. 29 | // 30 | //go:nowritebarrierrec 31 | //go:nosplit 32 | //go:linkname DefaultWrite runtime.defaultWrite 33 | func DefaultWrite(fd int, p []byte) int { 34 | written := len(p) 35 | for len(p) > 0 { 36 | n := len(p) 37 | if n > bufferSize { 38 | n = bufferSize 39 | } 40 | 41 | for i := 0; i < n/4; i++ { 42 | pi := 4 * i 43 | regs().buf[i].StoreSafe(0 | 44 | uint32(p[pi])<<24 | 45 | uint32(p[pi+1])<<16 | 46 | uint32(p[pi+2])<<8 | 47 | uint32(p[pi+3])) 48 | } 49 | 50 | if n%4 != 0 { 51 | var tail uint32 52 | for i := 0; i < n%4; i++ { 53 | base := len(p) - n%4 54 | tail |= uint32(p[base+i]) << ((3 - i) * 8) 55 | } 56 | regs().buf[n/4].StoreSafe(tail) 57 | } 58 | 59 | regs().readPtr.StoreSafe(0) 60 | regs().writePtr.StoreSafe(uint32(n)) 61 | regs().token.StoreSafe(token) 62 | 63 | for regs().readPtr.LoadSafe() != regs().writePtr.LoadSafe() { 64 | if regs().writePtr.LoadSafe() != uint32(n) { 65 | // Abort wait loop and discard bytes if the 66 | // write ptr wasn't set. There is probably no 67 | // isviewer available. 68 | break 69 | } 70 | // wait 71 | } 72 | 73 | regs().token.StoreSafe(0x0) 74 | p = p[n:] 75 | } 76 | 77 | return written 78 | } 79 | 80 | type defaultWriter int 81 | 82 | // DefaultWriter implements [io.Writer] using the [DefaultWrite] function. 83 | const DefaultWriter defaultWriter = 0 84 | 85 | func (v defaultWriter) Write(p []byte) (int, error) { 86 | return DefaultWrite(int(v), p), nil 87 | } 88 | -------------------------------------------------------------------------------- /rcp/regs.go: -------------------------------------------------------------------------------- 1 | package rcp 2 | 3 | import ( 4 | "embedded/mmio" 5 | 6 | "github.com/clktmr/n64/rcp/cpu" 7 | ) 8 | 9 | // The RCP's clock speed 10 | const ClockSpeed = 62.5e6 11 | 12 | func regs() *registers { return cpu.MMIO[registers](0x0430_0000) } 13 | 14 | // The RCP has multiple interrupts, which are all routed to the same external 15 | // interrupt line on the CPU. So all of these must be handled in the 16 | // IRQ3_Handler. 17 | type interruptFlag uint32 18 | 19 | const ( 20 | IntrRSP interruptFlag = 1 << iota // RSP breakpoint or software interrupt 21 | IntrSerial // SI DMA to/from PIF RAM finished 22 | IntrAudio // playback of audio buffer started 23 | IntrVideo // VBlank, line configurable with video.regs.vInt 24 | IntrPeriph // PI bus DMA tranfer finished 25 | IntrRDP // RDP full sync (see FULL_SYNC command) 26 | 27 | IntrLast 28 | ) 29 | 30 | type modeFlag uint32 31 | 32 | const RepeatCountMask modeFlag = 0x7f 33 | 34 | // mode read access 35 | const ( 36 | Repeat modeFlag = 1 << (iota + 7) 37 | EBus 38 | Upper 39 | ) 40 | 41 | // mode write access 42 | const ( 43 | ClearRepeat modeFlag = 1 << (iota + 7) 44 | SetRepeat 45 | ClearEBus 46 | SetEBus 47 | ClearDP 48 | ClearUpper 49 | SetUpper 50 | ) 51 | 52 | type registers struct { 53 | mode mmio.R32[modeFlag] 54 | 55 | rspVersion mmio.U8 56 | rdpVersion mmio.U8 57 | racVersion mmio.U8 58 | ioVersion mmio.U8 59 | 60 | // Read-only register with pending interrupts 61 | interrupt mmio.R32[interruptFlag] 62 | 63 | // When writing to this register, the bits have another meaning: Each 64 | // interrupt has two bits: 65 | // 0 - clear SP 66 | // 1 - set SP 67 | // 2 - clear SI 68 | // 3 - set SI 69 | // ... and so on. 70 | mask mmio.R32[interruptFlag] 71 | } 72 | 73 | func EnableInterrupts(mask interruptFlag) { 74 | mask = convertMask(mask) 75 | mask = mask << 1 76 | regs().mask.Store(mask) 77 | } 78 | 79 | func DisableInterrupts(mask interruptFlag) { 80 | mask = convertMask(mask) 81 | regs().mask.Store(mask) 82 | } 83 | 84 | func Interrupts() { 85 | regs().mask.Load() 86 | } 87 | 88 | func ClearDPIntr() { regs().mode.Store(ClearDP) } 89 | 90 | func convertMask(mask interruptFlag) interruptFlag { 91 | var wmask interruptFlag 92 | for i := IntrRSP; i < IntrLast; i = i << 1 { 93 | if mask&i != 0 { 94 | wmask |= i * i 95 | } 96 | } 97 | return wmask 98 | } 99 | -------------------------------------------------------------------------------- /rcp/mmio.go: -------------------------------------------------------------------------------- 1 | package rcp 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/clktmr/n64/rcp/cpu" 7 | ) 8 | 9 | type Register32[T any] interface { 10 | *T 11 | Load() uint32 12 | Store(uint32) 13 | } 14 | 15 | // WriteIO copies slice p to physical address busAddr using SysAd bus MMIO. Note 16 | // that it needs to read from the SysAd bus if p's start or end aren't 4 byte 17 | // aligned. This might lead to unexpected behaviour of write-only address 18 | // ranges. 19 | // 20 | //go:nosplit 21 | func WriteIO[T Register32[Q], Q any](busAddr cpu.Addr, p []byte) { 22 | end := cpu.KSEG1 | uintptr(busAddr+cpu.Addr(len(p)+3))&^0x3 23 | shift := -(int(busAddr) & 0x3) 24 | endshift := ^(int(busAddr) + len(p) - 1) & 0x3 25 | 26 | busPtr := unsafe.Pointer(cpu.KSEG1 | uintptr(busAddr&^0x3)) 27 | pPtr := unsafe.Pointer(unsafe.SliceData(p)) 28 | pPtr = unsafe.Add(pPtr, shift) 29 | for uintptr(busPtr) < end { 30 | data, mask := uint32(0), uint32(0xffff_ffff) 31 | if shift != 0 { // first dword 32 | mask &= 0xffff_ffff >> ((-shift) << 3) 33 | } 34 | if end-uintptr(busPtr) == 4 && endshift != 0 { // last dword 35 | mask &= 0xffff_ffff << (endshift << 3) 36 | } 37 | if mask != 0xffff_ffff { // read data before writing 38 | data = (T)(busPtr).Load() &^ mask 39 | } 40 | if uintptr(pPtr)&0x3 == 0 { 41 | data |= *(*uint32)(pPtr) & mask 42 | } else { // unaligned access forbidden on mips 43 | p := *(*[4]byte)(pPtr) 44 | data |= (uint32(p[0])<<24 | uint32(p[1])<<16 | uint32(p[2])<<8 | uint32(p[3])) & mask 45 | } 46 | (T)(busPtr).Store(data) 47 | 48 | shift = 0 49 | pPtr = unsafe.Add(pPtr, 4) 50 | busPtr = unsafe.Add(busPtr, 4) 51 | } 52 | } 53 | 54 | // ReadIO copies from physical address busAddr to slice p using SysAd bud MMIO. 55 | // 56 | //go:nosplit 57 | func ReadIO[T Register32[Q], Q any](busAddr cpu.Addr, p []byte) { 58 | end := cpu.KSEG1 | uintptr(busAddr+cpu.Addr(len(p)+3))&^0x3 59 | shift := -(int(busAddr) & 0x3) 60 | 61 | busPtr := unsafe.Pointer(cpu.KSEG1 | uintptr(busAddr&^0x3)) 62 | pPtr := unsafe.Pointer(unsafe.SliceData(p)) 63 | pPtr = unsafe.Add(pPtr, shift) 64 | for uintptr(busPtr) < end { 65 | data := (T)(busPtr).Load() 66 | if uintptr(pPtr)&0x3 == 0 { 67 | *(*uint32)(pPtr) = data 68 | } else { // unaligned access forbidden on mips 69 | i, s := 0, (3+shift)<<3 70 | for i < min(len(p), shift+4) { 71 | p[i] = byte(data >> s) 72 | i, s = i+1, s-8 73 | } 74 | p = p[i:] 75 | shift = 0 76 | } 77 | 78 | pPtr = unsafe.Add(pPtr, 4) 79 | busPtr = unsafe.Add(busPtr, 4) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rcp/texture/fileformat.go: -------------------------------------------------------------------------------- 1 | package texture 2 | 3 | import ( 4 | "compress/zlib" 5 | "encoding/binary" 6 | "errors" 7 | "image" 8 | "io" 9 | ) 10 | 11 | type header struct { 12 | Format Format 13 | HasAlpha bool 14 | Width, Height uint16 15 | PaletteSize uint16 16 | } 17 | 18 | func Load(r io.Reader) (tex *Texture, err error) { 19 | zr, err := zlib.NewReader(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer zr.Close() 24 | 25 | var hdr header 26 | err = binary.Read(zr, binary.BigEndian, &hdr) 27 | if err != nil { 28 | return nil, err 29 | } 30 | rect := image.Rect(0, 0, int(hdr.Width), int(hdr.Height)) 31 | switch hdr.Format { 32 | case RGBA32: 33 | tex = NewRGBA32(rect) 34 | case RGBA16: 35 | tex = NewRGBA16(rect) 36 | // case fmtYUV16: 37 | // case fmtIA16: 38 | // case fmtIA8: 39 | // case fmtIA4: 40 | case I8: 41 | if hdr.HasAlpha { 42 | tex = NewAlpha(rect) 43 | } else { 44 | tex = NewI8(rect) 45 | } 46 | case I4: 47 | tex = NewI4(rect) 48 | case CI8: 49 | palette, err := NewColorPalette(int(hdr.PaletteSize)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | tex = NewCI8(rect, palette) 54 | // case fmtCI4: 55 | default: 56 | return nil, errors.New("unsupported format") 57 | } 58 | 59 | _, err = io.ReadFull(zr, tex.pix) 60 | if err != nil && err != io.EOF { 61 | return nil, err 62 | } 63 | tex.Writeback() 64 | 65 | if hdr.PaletteSize > 0 { 66 | _, err = io.ReadFull(zr, tex.palette.pix) 67 | if err != nil && err != io.EOF { 68 | return nil, err 69 | } 70 | tex.palette.Writeback() 71 | } 72 | 73 | return tex, nil 74 | } 75 | 76 | func (p *Texture) Store(w io.Writer) error { 77 | if p.stride != p.Bounds().Dx() { 78 | return errors.New("is subimage") 79 | } 80 | 81 | var hdr = header{ 82 | Format: p.Format(), 83 | HasAlpha: p.HasAlpha(), 84 | Width: uint16(p.Bounds().Dx()), 85 | Height: uint16(p.Bounds().Dy()), 86 | } 87 | 88 | if p.palette != nil { 89 | r := p.palette.Bounds() 90 | hdr.PaletteSize = uint16(r.Dx() * r.Dy()) 91 | } 92 | 93 | zw := zlib.NewWriter(w) 94 | defer zw.Close() 95 | err := binary.Write(zw, binary.BigEndian, hdr) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | _, err = zw.Write(p.pix) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if p.palette != nil { 106 | _, err = zw.Write(p.palette.pix) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /rcp/periph/device.go: -------------------------------------------------------------------------------- 1 | // Package periph provides IO and DMA on the PI bus. 2 | package periph 3 | 4 | import ( 5 | "embedded/rtos" 6 | "errors" 7 | "io" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | "unsafe" 12 | 13 | "github.com/clktmr/n64/debug" 14 | "github.com/clktmr/n64/rcp/cpu" 15 | ) 16 | 17 | const ( 18 | piBus0Start = 0x0500_0000 19 | piBus0End = 0x1fbf_ffff 20 | piBus1Start = 0x1fd0_0000 21 | piBus1End = 0x7fff_ffff 22 | ) 23 | 24 | var ErrEndOfDevice = errors.New("end of device") 25 | 26 | // Device implememts io.ReaderAt and io.WriterAt for accessing devices on the PI 27 | // bus. It will automatically choose DMA transfers where alignment and cacheline 28 | // padding allow it, otherwise fall back to copying via mmio. 29 | type Device struct { 30 | addr cpu.Addr 31 | size uint32 32 | 33 | done *rtos.Cond 34 | mtx sync.Mutex 35 | } 36 | 37 | func NewDevice(piAddr cpu.Addr, size uint32) *Device { 38 | addr := uint32(piAddr) 39 | debug.Assert((addr >= piBus0Start && addr+size <= piBus0End) || 40 | (addr >= piBus1Start && addr+size <= piBus1End), 41 | "invalid pi bus address") 42 | return &Device{addr: piAddr, size: size, done: allocCond()} 43 | } 44 | 45 | func (v *Device) Addr() cpu.Addr { 46 | return v.addr 47 | } 48 | 49 | func (v *Device) Size() int { 50 | return int(v.size) 51 | } 52 | 53 | func (v *Device) ReadAt(p []byte, off int64) (n int, err error) { 54 | v.mtx.Lock() 55 | defer v.mtx.Unlock() 56 | 57 | left := int(v.size) - int(off) 58 | if len(p) >= left { 59 | p = p[:left] 60 | err = io.EOF 61 | } 62 | 63 | addr := uintptr(unsafe.Pointer(unsafe.SliceData(p))) 64 | dmaSync(addr, dmaJob{v.addr + cpu.Addr(off), p, dmaLoad, v.done}) 65 | n = len(p) 66 | 67 | return 68 | } 69 | 70 | func (v *Device) WriteAt(p []byte, off int64) (n int, err error) { 71 | v.mtx.Lock() 72 | defer v.mtx.Unlock() 73 | 74 | left := int(v.size) - int(off) 75 | if len(p) > left { 76 | p = p[:left] 77 | err = ErrEndOfDevice 78 | } 79 | 80 | addr := uintptr(unsafe.Pointer(unsafe.SliceData(p))) 81 | dmaSync(addr, dmaJob{v.addr + cpu.Addr(off), p, dmaStore, v.done}) 82 | n = len(p) 83 | 84 | return 85 | } 86 | 87 | //go:uintptrescapes 88 | func dmaSync(_ uintptr, job dmaJob) { 89 | dma(job) 90 | if !job.done.Wait(1 * time.Second) { 91 | panic("dma timeout") 92 | } 93 | } 94 | 95 | var ( 96 | condPool [64]rtos.Cond 97 | condPoolIdx atomic.Int32 98 | ) 99 | 100 | func allocCond() *rtos.Cond { 101 | return &condPool[condPoolIdx.Add(1)-1] 102 | } 103 | -------------------------------------------------------------------------------- /rcp/syscall_test.go: -------------------------------------------------------------------------------- 1 | package rcp_test 2 | 3 | import ( 4 | "embedded/rtos" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | _ "unsafe" // for linkname 10 | 11 | "github.com/clktmr/n64/drivers/carts/summercart64" 12 | "github.com/clktmr/n64/rcp" 13 | "github.com/clktmr/n64/rcp/rdp" 14 | ) 15 | 16 | var blocker atomic.Bool 17 | var sc64 *summercart64.Cart 18 | var note rtos.Cond 19 | 20 | //go:linkname cartHandler IRQ4_Handler 21 | //go:interrupthandler 22 | func cartHandler() { 23 | if sc64 == nil { 24 | panic("sc64 not initialized") 25 | } 26 | blocker.Store(false) 27 | sc64.ClearInterrupt() 28 | } 29 | 30 | //go:nosplit 31 | //go:nowritebarrierrec 32 | func blockingHandler() { 33 | rcp.ClearDPIntr() 34 | start := time.Now() 35 | for time.Since(start) < 5*time.Second && blocker.Load() == true { 36 | // block 37 | } 38 | note.Signal() 39 | } 40 | 41 | func TestInterruptPrio(t *testing.T) { 42 | sc64 = summercart64.Probe() 43 | if testing.Short() { 44 | t.Skip("skipping in short mode") 45 | } 46 | if sc64 == nil { 47 | t.Skip("requires SummerCart64") 48 | } 49 | 50 | tests := map[string]struct { 51 | prio int 52 | preempt bool 53 | }{ 54 | "high": {rtos.IntPrioHighest, true}, 55 | "normal": {rtos.IntPrioMid, false}, 56 | } 57 | 58 | for name, tc := range tests { 59 | t.Run(name, func(t *testing.T) { 60 | rcp.IrqCart.Enable(tc.prio, 0) 61 | 62 | _, prio, err := rcp.IrqCart.Status(0) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | if prio != tc.prio { 67 | t.Fatal("prio not set") 68 | } 69 | 70 | _, err = sc64.SetConfig(summercart64.CfgButtonMode, summercart64.ButtonModeInterrupt) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | blocker.Store(true) 75 | 76 | // generate single 5 second blocking low prio interrupt 77 | t.Log("Press SummerCart64 button in the next 5 seconds") 78 | 79 | rdpHandler := rcp.Handler(rcp.IntrRDP) 80 | rcp.SetHandler(rcp.IntrRDP, blockingHandler) 81 | 82 | start := time.Now() 83 | rdp.RDP.Push(rdp.SyncFull) 84 | note.Wait(5 * time.Second) 85 | 86 | _, err = sc64.SetConfig(summercart64.CfgButtonMode, summercart64.ButtonModeDisabled) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | rcp.SetHandler(rcp.IntrRDP, rdpHandler) 91 | 92 | if blocker.Load() == true { 93 | t.Fatal("no button press detected") 94 | } 95 | if time.Since(start) > 5*time.Second == tc.preempt { 96 | t.Fatal("priorities not applied") 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tools/texture/main.go: -------------------------------------------------------------------------------- 1 | package texture 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "image/color" 13 | "image/draw" 14 | _ "image/gif" 15 | _ "image/jpeg" 16 | _ "image/png" 17 | 18 | "github.com/clktmr/n64/rcp/texture" 19 | "github.com/ericpauley/go-quantize/quantize" 20 | ) 21 | 22 | var ( 23 | flags = flag.NewFlagSet("texture", flag.ExitOnError) 24 | 25 | format = flags.String("format", "RGBA32", "image format and bit depth") 26 | dither = flags.Bool("dither", false, "enable Floyd-Steinberg error diffusion") 27 | palette = flags.Int("palette", 256, "number of colors in CI4 and CI8 format") 28 | 29 | imagefile string 30 | ) 31 | 32 | const usageString = `Image to n64 texture converter. 33 | 34 | Usage: %s [flags] 35 | 36 | ` 37 | 38 | func usage() { 39 | fmt.Fprintf(flags.Output(), usageString, "texture") 40 | flags.PrintDefaults() 41 | } 42 | 43 | func Main(args []string) { 44 | flags.Usage = usage 45 | flags.Parse(args[1:]) 46 | 47 | if flags.NArg() == 1 { 48 | imagefile = flags.Arg(0) 49 | } else { 50 | flags.Usage() 51 | os.Exit(1) 52 | } 53 | 54 | // Read the font data. 55 | r, err := os.Open(imagefile) 56 | if err != nil { 57 | log.Fatalln(err) 58 | } 59 | 60 | src, _, err := image.Decode(r) 61 | if err != nil { 62 | log.Fatalln(err) 63 | } 64 | 65 | var dst *texture.Texture 66 | 67 | switch *format { 68 | case "RGBA32": 69 | dst = texture.NewRGBA32(src.Bounds()) 70 | case "RGBA16": 71 | dst = texture.NewRGBA16(src.Bounds()) 72 | // case "YUV16": 73 | // case "IA16": 74 | // case "IA8": 75 | // case "IA4": 76 | case "I8": 77 | dst = texture.NewI8(src.Bounds()) 78 | case "I4": 79 | dst = texture.NewI4(src.Bounds()) 80 | case "CI8": 81 | q := quantize.MedianCutQuantizer{} 82 | p := q.Quantize(make([]color.Color, 0, *palette), src) 83 | cp, err := texture.CopyColorPalette(p) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | dst = texture.NewCI8(src.Bounds(), cp) 88 | // case "CI4": 89 | default: 90 | log.Fatal("unsupported format:", *format) 91 | } 92 | 93 | var d draw.Drawer = draw.Src 94 | if *dither { 95 | d = draw.FloydSteinberg 96 | } 97 | 98 | d.Draw(dst, dst.Bounds(), src, image.Point{}) 99 | 100 | outfile := strings.TrimSuffix(imagefile, filepath.Ext(imagefile)) 101 | outfile += "." + *format 102 | w, err := os.Create(outfile) 103 | if err != nil { 104 | log.Fatalln(err) 105 | } 106 | defer w.Close() 107 | 108 | err = dst.Store(w) 109 | if err != nil { 110 | log.Fatalln(err) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /drivers/carts/summercart64/usb.go: -------------------------------------------------------------------------------- 1 | package summercart64 2 | 3 | import ( 4 | "embedded/rtos" 5 | "errors" 6 | "runtime" 7 | "time" 8 | ) 9 | 10 | // Write writes data from p to the USB port. 11 | func (v *Cart) Write(p []byte) (n int, err error) { 12 | for len(p) > 0 { 13 | err = waitUSB(cmdUSBWriteStatus) 14 | if err != nil { 15 | return 16 | } 17 | 18 | var nn int 19 | nn, err = usbBuf.WriteAt(p[:min(len(p), usbBuf.Size())], 0) 20 | if err != nil { 21 | return 22 | } 23 | p = p[nn:] 24 | 25 | datatype := 1 26 | header := uint32(((datatype) << 24) | ((nn) & 0x00FFFFFF)) 27 | _, _, err = execCommand(cmdUSBWrite, uint32(usbBuf.Addr()), header) 28 | if err != nil { 29 | return 30 | } 31 | 32 | n += nn 33 | } 34 | 35 | return 36 | } 37 | 38 | // Read reads pending data from the USB port into p. 39 | func (v *Cart) Read(p []byte) (n int, err error) { 40 | msgtype, length, err := execCommand(cmdUSBReadStatus, 0, 0) 41 | if msgtype == 0 || err != nil { 42 | return 0, err 43 | } 44 | 45 | writeEnable, err := v.SetConfig(CfgROMWriteEnable, 1) 46 | if err != nil { 47 | return 0, err 48 | } 49 | 50 | pending := min(len(p), int(length), bufferSize) 51 | _, _, err = execCommand(cmdUSBRead, uint32(usbBuf.Addr()), uint32(pending)) 52 | if err != nil { 53 | return 0, err 54 | } 55 | 56 | err = waitUSB(cmdUSBReadStatus) 57 | if err != nil { 58 | return 0, err 59 | } 60 | 61 | n, err1 := usbBuf.ReadAt(p[:pending], 0) 62 | 63 | _, err = v.SetConfig(CfgROMWriteEnable, writeEnable) 64 | if err != nil { 65 | return 0, err 66 | } 67 | 68 | // sc64 adds null terminator as EOL, replace with newline 69 | if p[n-1] == 0 { 70 | p[n-1] = '\n' 71 | } 72 | 73 | return n, err1 74 | } 75 | 76 | func waitUSB(cmd command) error { 77 | start := rtos.Nanotime() 78 | for { 79 | status, _, err := execCommand(cmd, 0, 0) 80 | if err != nil { 81 | return err 82 | } 83 | if status&uint32(statusBusy) == 0 { 84 | break 85 | } 86 | if rtos.Nanotime()-start > time.Second { 87 | return errors.New("usb timeout") 88 | } 89 | runtime.Gosched() 90 | } 91 | return nil 92 | } 93 | 94 | func execCommand(cmdId command, data0 uint32, data1 uint32) (result0 uint32, result1 uint32, err error) { 95 | regs().data0.Store(data0) 96 | regs().data1.Store(data1) 97 | regs().status.Store(status(cmdId)) 98 | 99 | status := statusBusy 100 | for status&statusBusy != 0 { 101 | status = regs().status.Load() 102 | } 103 | 104 | result0 = regs().data0.Load() 105 | result1 = regs().data1.Load() 106 | 107 | if status&statusError != 0 { 108 | err = errCodes[result0] 109 | } 110 | 111 | return 112 | } 113 | -------------------------------------------------------------------------------- /rcp/rdp/regs.go: -------------------------------------------------------------------------------- 1 | // Package rdp provides writing commands to the display processor. 2 | // 3 | // The diplay processor is a hardware rasterizer. It controls the texture cache 4 | // and draws primitives directly into a framebuffer in RDRAM. It's usually not 5 | // used directly but through the RSP instead. 6 | // 7 | // This package gives direct access to some of the low-level RDP commands, which 8 | // can be used for simple 2D graphics. For 3D graphics the RSP with a suitable 9 | // microcode will be necessary. 10 | package rdp 11 | 12 | import ( 13 | "embedded/mmio" 14 | "embedded/rtos" 15 | 16 | "github.com/clktmr/n64/rcp" 17 | "github.com/clktmr/n64/rcp/cpu" 18 | ) 19 | 20 | func regs() *registers { return cpu.MMIO[registers](0x0410_0000) } 21 | 22 | type statusFlags uint32 23 | 24 | // Read access to status register 25 | const ( 26 | xbus statusFlags = 1 << iota // Unset to use XBUS as source for DMA transfers instead of DMEM 27 | freeze // Set to stop processing primitives 28 | flush // Set to abort all current RDP transfers immediately 29 | startGclk 30 | tmemBusy 31 | pipeBusy 32 | busy // Set from DMA transfer start until SYNC_FULL 33 | ready 34 | dmaBusy 35 | endPending // Set when end register was written and transfer hasn't started yet 36 | startPending // Set when start register was written and transfer hasn't started yet 37 | ) 38 | 39 | // Write access to status register 40 | const ( 41 | clrXbus statusFlags = 1 << iota 42 | setXbus 43 | clrFreeze 44 | setFreeze 45 | clrFlush 46 | setFlush 47 | clrTMEMBusy 48 | clrPipeBusy 49 | clrBufferBusy 50 | clrClock // Reset the clock register to zero 51 | ) 52 | 53 | type registers struct { 54 | start mmio.R32[cpu.Addr] // Physical start address of DMA transfer 55 | end mmio.R32[cpu.Addr] // Physical end address of DMA transfer 56 | current mmio.R32[cpu.Addr] // DMA transfer progress. Address between start and end. Read-only. 57 | 58 | status mmio.R32[statusFlags] 59 | clock mmio.U32 // 24-bit counter running at RCP frequency 60 | 61 | cmdBusy mmio.U32 62 | pipeBusy mmio.U32 63 | tmemBusy mmio.U32 64 | 65 | // Note: There are more undocumented registers (DPS_*) 66 | } 67 | 68 | var fullSync rtos.Cond 69 | 70 | func init() { 71 | rcp.SetHandler(rcp.IntrRDP, handler) 72 | rcp.EnableInterrupts(rcp.IntrRDP) 73 | } 74 | 75 | //go:nosplit 76 | //go:nowritebarrierrec 77 | func handler() { 78 | rcp.ClearDPIntr() 79 | fullSync.Signal() 80 | } 81 | 82 | // Busy returns the number of GCLK cycles in which the specified component was 83 | // busy since the last call to Busy. 84 | func Busy() (cmd, pipe, tmem uint32) { 85 | cmd = regs().cmdBusy.Load() 86 | pipe = regs().pipeBusy.Load() 87 | tmem = regs().tmemBusy.Load() 88 | regs().status.Store(clrBufferBusy | clrPipeBusy | clrTMEMBusy) 89 | return 90 | } 91 | -------------------------------------------------------------------------------- /drivers/carts/everdrive64/usb.go: -------------------------------------------------------------------------------- 1 | package everdrive64 2 | 3 | import ( 4 | "github.com/clktmr/n64/rcp/periph" 5 | ) 6 | 7 | var usbBuf = periph.NewDevice(0x1f80_0400, 512) 8 | 9 | type Cart struct{} 10 | 11 | // Probe returns the [Cart] if an EverDrive64 was detected. 12 | func Probe() *Cart { 13 | regs().key.Store(0xaa55) // magic key to unlock registers 14 | switch regs().version.Load() { 15 | case 0xed64_0008: // EverDrive64 X3 16 | fallthrough 17 | case 0x0000_0001: // EverDrive64 X7 without sdcard inserted 18 | fallthrough 19 | case 0xed64_0013: // EverDrive64 X7 20 | cart := &Cart{} 21 | return cart 22 | } 23 | return nil 24 | } 25 | 26 | // Write writes data from p to the USB port as raw bytes. 27 | func (v *Cart) Write(p []byte) (n int, err error) { 28 | for len(p) > 0 { 29 | regs().usbCfgW.Store(writeNop) 30 | 31 | offset := usbBuf.Size() - min(len(p), usbBuf.Size()) 32 | 33 | var nn int 34 | nn, err = usbBuf.WriteAt(p, int64(offset)) 35 | if err != nil { 36 | return 37 | } 38 | p = p[nn:] 39 | 40 | regs().usbCfgW.Store(write | usbMode(offset)) 41 | 42 | for regs().usbCfgR.Load()&act != 0 { 43 | // wait 44 | } 45 | 46 | n += nn 47 | } 48 | 49 | return 50 | } 51 | 52 | // Wraps an io.Writer to provide a new io.Writer, which encapsulates each write 53 | // in an UNFLoader packet. 54 | type UNFLoader struct { 55 | // Can't use an interface here because presumably it causes "malloc 56 | // during signal" if called via SystemWriter in a syscall. 57 | w *Cart 58 | } 59 | 60 | // Returns a new [UNFLoader]. Use this if you intend to use the USB port for 61 | // logging. 62 | func NewUNFLoader(w *Cart) *UNFLoader { 63 | // send a single heartbeat to let UNFLoader know which protocol version 64 | // we are speaking. 65 | w.Write([]byte{'D', 'M', 'A', '@', 5, 0, 0, 4, 0, 2, 0, 1, 'C', 'M', 'P', 'H'}) 66 | return &UNFLoader{w: w} 67 | } 68 | 69 | // Write writes data from p to the underlying writer in UNFLoader packets. 70 | func (v *UNFLoader) Write(p []byte) (n int, err error) { 71 | for len(p) > 0 { 72 | nn := min(len(p), (1<<24)-1) 73 | _, err = v.w.Write([]byte{'D', 'M', 'A', '@', 1, byte(nn >> 16), byte(nn >> 8), byte(nn)}) 74 | if err != nil { 75 | return 76 | } 77 | 78 | // Align pi addr to 2 byte to ensure use of DMA. This might 79 | // cause the last byte to be discarded. If that's the case, we 80 | // prepend it to the footer. 81 | _, err = v.w.Write(p[:nn&^1]) 82 | if err != nil { 83 | return 84 | } 85 | 86 | footer := []byte{p[nn-1], 'C', 'M', 'P', 'H', '0'} 87 | if nn&1 == 0 { 88 | footer = footer[1 : len(footer)-1] 89 | } 90 | _, err = v.w.Write(footer) 91 | if err != nil { 92 | return 93 | } 94 | 95 | p = p[nn:] 96 | n += nn 97 | } 98 | 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go on Nintendo 64 2 | 3 | Develop applications for the Nintendo 64 in pure Go. Builds upon embeddedgo, 4 | which adds a minimal rtos to the runtime via GOOS=noos. 5 | 6 | ## Prerequisites 7 | 8 | - Go 9 | - Git 10 | - ares emulator (optional) 11 | 12 | ## Getting Started 13 | 14 | 1. Install the embeddedgo toolchain: 15 | 16 | ```sh 17 | go install github.com/embeddedgo/dl/go1.24.4-embedded@latest 18 | go1.24.4-embedded download 19 | ``` 20 | 21 | 2. Install n64go: 22 | 23 | ```sh 24 | go install github.com/clktmr/n64/tools/n64go@latest 25 | ``` 26 | 27 | This tool helps managing n64 file formats. It's also hooked into the go 28 | command via the -toolexec flag to provide generation of z64 and uf2 ROM 29 | files. 30 | 31 | 3. Setup your build environment. Copy `go.env` from this repository to your 32 | desired location and make use of it: 33 | 34 | ```sh 35 | export GOENV="path/to/go.env" 36 | ``` 37 | 38 | Alternatively you can of course use your preferred way of managing 39 | environment variables. 40 | 41 | You can now use `go build` and `go run` as usual! Try it with the minimal hello 42 | world example: 43 | 44 | ```go 45 | package main 46 | 47 | import _ "github.com/clktmr/n64/machine" 48 | 49 | func main() { 50 | println("hello world!") 51 | } 52 | ``` 53 | 54 | ## Differences from mainline Go 55 | 56 | ### machine 57 | 58 | Your application needs to import `github.com/clktmr/n64/machine` at some point, 59 | which provides basic system setup. Otherwise your build will fail with a linker 60 | error. 61 | 62 | ### fmt and log 63 | 64 | Per default `fmt.Print()` and `log.Print()` write to `os.Stdout`, which isn't 65 | set after boot. Use `embedded/rtos.Mount()` and 66 | `github.com/embeddedgo/fs/termfs` to place an `io.Writer` at that location. 67 | 68 | ### os and net 69 | 70 | Having no operating system has obvious consequences for the os package. There 71 | are neither processes nor any network stack in the kernel. While `os/exec` is 72 | not supported, networking applications can run if an implementation of the Conn 73 | or Listener interface is passed to them. 74 | 75 | ### embed 76 | 77 | While embed can be used, it will load all embedded files into RAM at boot. As an 78 | alternative `github.com/clktmr/n64/drivers/cartfs` provides a fs.FS 79 | implementation to read embedded files from the cartridge via DMA instead. 80 | 81 | ### testing 82 | 83 | The `go test` command does currently not work reliably for several reasons: 84 | 85 | - The build might fail because of missing machine import 86 | - The tests might fail if they try to access testdata directory 87 | 88 | This will probably be solved in the future. In the meantime fall back to 89 | providing a TestMain for each package that should run tests on the Nintendo 64. 90 | Package `github.com/clktmr/n64/testing` provides a reusable TestMain 91 | implementation for that purpose. 92 | 93 | ### cgo 94 | 95 | cgo is not supported! 96 | -------------------------------------------------------------------------------- /rcp/serial/command.go: -------------------------------------------------------------------------------- 1 | package serial 2 | 3 | import ( 4 | "embedded/rtos" 5 | "errors" 6 | "sync" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/clktmr/n64/rcp" 11 | "github.com/clktmr/n64/rcp/cpu" 12 | ) 13 | 14 | type pifCommand byte 15 | 16 | // Commands known by PIF microchip. 17 | const ( 18 | CmdConfigureJoybus pifCommand = 0x1 << iota 19 | CmdCICChallenge 20 | _ 21 | CmdTerminateBoot 22 | CmdLockROM 23 | CmdAcquireChecksum 24 | CmdRunChecksum 25 | ) 26 | 27 | var mtx sync.Mutex 28 | 29 | // state shared with interrupt handler 30 | var ( 31 | cmdFinished rtos.Cond 32 | cmdBuffer rcp.IntrInput[[]byte] 33 | ) 34 | 35 | func init() { 36 | rcp.SetHandler(rcp.IntrSerial, handler) 37 | rcp.EnableInterrupts(rcp.IntrSerial) 38 | } 39 | 40 | //go:nosplit 41 | //go:nowritebarrierrec 42 | func handler() { 43 | regs().status.Store(0) // clears interrupt 44 | 45 | buf, _ := cmdBuffer.Get() 46 | if buf == nil { 47 | return 48 | } 49 | 50 | if buf[pifRamSize-1] == 0x00 { 51 | // DMA read finished 52 | cmdFinished.Signal() 53 | } else { 54 | // DMA write finished, trigger read back 55 | cpu.InvalidateSlice(buf) 56 | regs().dramAddr.Store(cpu.PhysicalAddressSlice(buf)) 57 | regs().pifReadAddr.Store(pifRamAddr) 58 | } 59 | } 60 | 61 | // CommandBlock holds the buffer that is used to write the command and read the 62 | // response. 63 | type CommandBlock struct { 64 | cmd pifCommand 65 | buf []byte 66 | } 67 | 68 | func NewCommandBlock(cmd pifCommand) *CommandBlock { 69 | buf := cpu.MakePaddedSlice[byte](pifRamSize)[:0] 70 | return &CommandBlock{cmd, buf} 71 | } 72 | 73 | // Alloc returns a slice with the next n bytes. It returns [io.EOF] if there 74 | // aren't enough free bytes. 75 | func (c *CommandBlock) Alloc(n int) ([]byte, error) { 76 | if n > c.Free() { 77 | return nil, errors.New("command block full") 78 | } 79 | l := len(c.buf) 80 | c.buf = c.buf[:l+n] 81 | return c.buf[l:], nil 82 | } 83 | 84 | // Free returns the number of free bytes available in the CommandBlock for 85 | // additional commands. 86 | func (c *CommandBlock) Free() int { 87 | return cap(c.buf) - len(c.buf) - 1 // save one byte for PIF command 88 | } 89 | 90 | // Run executes the given CommandBlock on the PIF and blocks until the response 91 | // was written back. 92 | func Run(block *CommandBlock) { 93 | mtx.Lock() 94 | defer mtx.Unlock() 95 | 96 | buf := block.buf[:pifRamSize] 97 | buf[len(buf)-1] = byte(block.cmd) 98 | 99 | cmdBuffer.Put(buf) 100 | cpu.WritebackSlice(buf) 101 | ramAddr := uintptr(unsafe.Pointer(unsafe.SliceData(buf))) 102 | run(ramAddr, pifRamAddr) 103 | 104 | cmdBuffer.Put(nil) 105 | } 106 | 107 | //go:uintptrescapes 108 | func run(ramAddr uintptr, pifRamAddr cpu.Addr) { 109 | regs().dramAddr.Store(cpu.PAddr(ramAddr)) 110 | regs().pifWriteAddr.Store(pifRamAddr) 111 | 112 | // Wait until message was received 113 | if !cmdFinished.Wait(1 * time.Second) { 114 | panic("pif timeout") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /rcp/rsp/rsp_test.go: -------------------------------------------------------------------------------- 1 | package rsp_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "testing" 8 | "time" 9 | 10 | "github.com/clktmr/n64/rcp/cpu" 11 | "github.com/clktmr/n64/rcp/rsp" 12 | "github.com/clktmr/n64/rcp/rsp/ucode" 13 | n64testing "github.com/clktmr/n64/testing" 14 | ) 15 | 16 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 17 | 18 | func TestDMA(t *testing.T) { 19 | testdata := cpu.MakePaddedSlice[byte](80) 20 | for i := range len(testdata) { 21 | testdata[i] = byte(i) 22 | } 23 | _, err := rsp.DMEM.WriteAt(testdata, 0x100) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | result := cpu.MakePaddedSlice[byte](len(testdata)) 29 | _, err = rsp.DMEM.ReadAt(result, 0x100) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | if !bytes.Equal(testdata, result) { 34 | t.Error("exptected to read same data back that was written") 35 | } 36 | 37 | shift := int64(0x20) 38 | _, err = rsp.DMEM.ReadAt(result, 0x100+shift) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if !bytes.Equal(testdata[shift:], result[:len(result)-int(shift)]) { 43 | t.Error("exptected to read part of same data back that was written") 44 | } 45 | } 46 | 47 | func TestRun(t *testing.T) { 48 | // Simple program that will swap the first two dwords in DMEM 49 | code := []byte{ 50 | 0x3c, 0x09, 0xa4, 0x00, //lui t1,0xa400 51 | 0x8d, 0x29, 0x00, 0x00, //lw t1,0(t1) 52 | 0x3c, 0x0a, 0xa4, 0x00, //lui t2,0xa400 53 | 0x8d, 0x4a, 0x00, 0x04, //lw t2,4(t2) 54 | 0x3c, 0x01, 0xa4, 0x00, //lui at,0xa400 55 | 0xac, 0x2a, 0x00, 0x00, //sw t2,0(at) 56 | 0x3c, 0x01, 0xa4, 0x00, //lui at,0xa400 57 | 0xac, 0x29, 0x00, 0x04, //sw t1,4(at) 58 | 0x00, 0x00, 0x00, 0x0d, //break 59 | } 60 | data := []byte{ 61 | 0xde, 0xad, 0xbe, 0xef, 62 | 0xbe, 0xef, 0xf0, 0x0d, 63 | } 64 | uc := ucode.NewUCode("testcode", cpu.Addr(rsp.IMEM&0xffffffff), code, data) 65 | rsp.Load(uc) 66 | 67 | var results = cpu.MakePaddedSlice[uint32](2) 68 | sr := io.NewSectionReader(rsp.DMEM, 0, 8) 69 | err := binary.Read(sr, binary.BigEndian, &results) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | if results[0] != 0xdeadbeef || results[1] != 0xbeeff00d { 74 | t.Fatal("failed to load ucode data") 75 | } 76 | 77 | rsp.Resume() 78 | 79 | sr.Seek(0, io.SeekStart) 80 | err = binary.Read(sr, binary.BigEndian, &results) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | if results[0] != 0xbeeff00d || results[1] != 0xdeadbeef { 85 | t.Fatalf("unexpected result after ucode execution: %x", results) 86 | } 87 | } 88 | 89 | func TestInterrupt(t *testing.T) { 90 | t.Cleanup(func() { 91 | rsp.SetInterrupt(false) 92 | }) 93 | 94 | rsp.SetInterrupt(true) 95 | 96 | code := []byte{ 97 | 0x00, 0x00, 0x00, 0x0d, //break 98 | } 99 | data := []byte{} 100 | uc := ucode.NewUCode("testcode", cpu.Addr(rsp.IMEM&0xffffffff), code, data) 101 | rsp.Load(uc) 102 | 103 | rsp.Resume() 104 | 105 | if triggered := rsp.IntBreak.Wait(10 * time.Millisecond); !triggered { 106 | t.Fatal("timeout") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rcp/sync.go: -------------------------------------------------------------------------------- 1 | package rcp 2 | 3 | import ( 4 | "embedded/rtos" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | const flagUpdated = 1 << 31 11 | 12 | // IntrInput passes any value safely into an interrupt context using a double 13 | // buffer. Only a single writer and a single reader are allowed. 14 | type IntrInput[T any] struct { 15 | bufs [2]T 16 | seq atomic.Uint32 // bit 0: read index, bit 31: update flag 17 | } 18 | 19 | // Read can be used by the writer goroutine to read back the currently stored 20 | // value, i.e. the argument of the last call to [Put]. 21 | func (p *IntrInput[T]) Read() (v T, consumed bool) { 22 | seq := p.seq.Load() 23 | return p.bufs[seq&0x1], seq&flagUpdated == 0 24 | } 25 | 26 | // Put updates the stored value atomically. 27 | func (p *IntrInput[T]) Put(v T) { 28 | new := (p.seq.Load() + 1) | flagUpdated 29 | p.bufs[new&0x1] = v 30 | p.seq.Store(new) 31 | } 32 | 33 | // Get returns the currently stored value and if it was updated by [Put] since 34 | // the last call to Get. 35 | // 36 | //go:nosplit 37 | func (p *IntrInput[T]) Get() (v T, updated bool) { 38 | for { 39 | old := p.seq.Load() 40 | v = p.bufs[old&0x1] 41 | updated = old&flagUpdated != 0 42 | 43 | new := old &^ flagUpdated 44 | if p.seq.CompareAndSwap(old, new) { 45 | return 46 | } 47 | } 48 | } 49 | 50 | const qsize = 32 51 | 52 | // IntrQueue queues any value safely into an interrupt context. Multiple writer 53 | // goroutines and a single reader are allowed. The reader must not be 54 | // preemptible by the writers, i.e. an interrupt. 55 | type IntrQueue[T any] struct { 56 | ring [qsize]T 57 | start, end atomic.Int32 58 | mtx sync.Mutex 59 | pop rtos.Cond 60 | } 61 | 62 | func (p *IntrQueue[T]) Push(v T) { 63 | p.mtx.Lock() 64 | defer p.mtx.Unlock() 65 | 66 | retry: 67 | p.pop.Wait(0) 68 | 69 | start := p.start.Load() 70 | end := p.end.Load() 71 | next := (end + 1) % int32(len(p.ring)) 72 | 73 | if next == start { 74 | if !p.pop.Wait(1 * time.Second) { 75 | panic("dma queue timeout") 76 | } 77 | goto retry 78 | } 79 | 80 | p.ring[end] = v 81 | 82 | if !p.end.CompareAndSwap(end, next) { 83 | panic("intr queue corrupted") 84 | } 85 | } 86 | 87 | //go:nosplit 88 | func (p *IntrQueue[T]) Peek() (v *T, ok bool) { 89 | start := p.start.Load() 90 | end := p.end.Load() 91 | if end == start { 92 | return v, false 93 | } 94 | 95 | return &p.ring[start], true 96 | } 97 | 98 | //go:nosplit 99 | func (p *IntrQueue[T]) Pop() (v *T, ok bool) { 100 | start := p.start.Load() 101 | end := p.end.Load() 102 | if end == start { 103 | return v, false 104 | } 105 | 106 | v = &p.ring[start] 107 | ok = true 108 | 109 | // Write zero value in the unused buffer to avoid holding hidden 110 | // references that might prevent freeing memory. 111 | // TODO not possible due to go:nowritebarrierrec 112 | // var zero T 113 | // p.ring[start] = zero 114 | 115 | if !p.start.CompareAndSwap(start, (start+1)%int32(len(p.ring))) { 116 | panic("multiple readers") 117 | } 118 | 119 | p.pop.Signal() 120 | 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /machine/rt0.s: -------------------------------------------------------------------------------- 1 | #include "go_asm.h" 2 | #include "funcdata.h" 3 | #include "textflag.h" 4 | 5 | #include "asm_mips64.h" 6 | 7 | // _rt0_mips64_noos is the entry point called by IPL3. 8 | TEXT _rt0_mips64_noos(SB),NOSPLIT|NOFRAME,$0 9 | // start at a known state 10 | MOVW $(SR_CU1|SR_PE|SR_FR), R2 11 | MOVW R2, M(C0_SR) 12 | MOVW R0, M(C0_CAUSE) 13 | MOVW R0, M(C0_WATCHLO) 14 | 15 | // The n64 actually needs to be compiled for GOARCH=mips64p32 which isn't 16 | // supported by gc. Instead we use mips64, but to do so we must ensure at 17 | // runtime that pointers are always 32-bit and correctly sign-extended to 64-bit 18 | // pointers. Sign-extending means, setting all bits of the upper DWORD to the 19 | // same value as bit 31. 20 | // In 32-bit kernel mode the VR4300 has all of it's physical memory mapped to 21 | // KSEG0=0x80000000 and again at KSEG1=0xa0000000 for uncached access. Running 22 | // code there generally works, but we get in trouble as soon es we read pointers 23 | // from external sources, e.g. when doing symbol lookup. These addresses won't 24 | // get sign-extended correctly, but always padded with zeroes instead. 25 | // To solve this we map the RDRAM to the beginning of the virtual 26 | // address space and continue execution there. This saves us from 27 | // sign-extending pointers correctly, as we avoid pointers with bit 31 28 | // set, leaving us effectively with an 31-bit wide address space. 29 | // 30 | // Possibly another way of solving this would be running the n64 in actual 31 | // 64-bit mode, but I'm not sure what other problems might occur when accessing 32 | // the 32-bit wide system bus. 33 | MOVV $0, R8 34 | MOVV R8, M(C0_INDEX) 35 | MOVV $0x3ff << 13, R8 // pagesize = 4 MB 36 | MOVV R8, M(C0_PAGEMASK) 37 | MOVV $(0x00000000 >> 6) | 0x7, R8 // first page 38 | MOVV R8, M(C0_ENTRYLO0) 39 | MOVV $(0x00400000 >> 6) | 0x7, R8 // second page 40 | MOVV R8, M(C0_ENTRYLO1) 41 | MOVV $0x00000000, R8 // vaddr = 0x0 42 | MOVV R8, M(C0_ENTRYHI) 43 | TLBWI 44 | 45 | MOVW (0x80000318), R16 // memory size 46 | MOVV $0x10, R9 47 | SUBV R9, R16, R29 // init stack pointer 48 | MOVV $0, RSB // init data pointer 49 | MOVW $8, R2 50 | MOVW R2, (0xbfc007fc) // trigger PIF command 'terminate boot process' 51 | 52 | // Check if PI DMA transfer is required, knowing that IPL3 loads 1 MiB 53 | // of ROM to RAM. 54 | // TODO Use libdragons ipl3 to do load the rom 55 | MOVW $_rt0_mips64_noos(SB), R4 56 | MOVW $runtime·edata(SB), R5 57 | MOVW $0x100000, R8 // stock IPL3 load size (1 MiB) 58 | SUBU R4, R5, R6 // calculate data size 59 | SUB R8, R6, R6 // calculate remaining data size 60 | BLEZ R6, wait_dma_end // skip PI DMA if data is already loaded 61 | 62 | // Copy code and data via DMA 63 | MOVW $0x10001000, R5 // address in rom 64 | ADDU R8, R4, R4 // skip over loaded data 65 | ADDU R8, R5, R5 66 | 67 | // Start PI DMA transfer 68 | MOVW $0xA4600000, R8 69 | MOVW R4, 0x00(R8) // PI_DRAM_ADDR 70 | MOVW R5, 0x04(R8) // PI_CART_ADDR 71 | ADD $-1, R6 72 | MOVW R6, 0x0C(R8) // PI_WR_LEN 73 | 74 | wait_dma_end: 75 | MOVW $0xA4600000, R8 76 | MOVW 0x10(R8), R9 // PI_STATUS 77 | AND $3, R9 // PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY 78 | BGTZ R9, wait_dma_end 79 | 80 | MOVV R16, R4 81 | JMP runtime·_rt0_mips64_noos1(SB) 82 | -------------------------------------------------------------------------------- /rcp/rsp/regs.go: -------------------------------------------------------------------------------- 1 | // Package rsp provides loading and running microcode on the signal processor. 2 | // 3 | // The signal processor provides fast vector instructions. It's usually used for 4 | // vertex transformations and audio mixing. It can directly control the RDP via 5 | // XBUS or shared memory in RDRAM. There are several precompiled microcodes 6 | // which can be loaded to provide different functionalities. 7 | package rsp 8 | 9 | import ( 10 | "embedded/mmio" 11 | 12 | "github.com/clktmr/n64/rcp/cpu" 13 | ) 14 | 15 | // RSP program counter. Access only allowed when RSP is halted. 16 | func pc() *mmio.R32[cpu.Addr] { return cpu.MMIO[mmio.R32[cpu.Addr]](0x0408_0000) } 17 | func regs() *registers { return cpu.MMIO[registers](0x0404_0000) } 18 | 19 | type statusFlags uint32 20 | 21 | // Read access to status register 22 | const ( 23 | halted statusFlags = 1 << iota 24 | broke 25 | dmaBusy 26 | dmaFull 27 | ioBusy 28 | singleStep 29 | intrOnBreak 30 | sig0 31 | sig1 32 | sig2 33 | sig3 34 | sig4 35 | sig5 36 | sig6 37 | sig7 38 | ) 39 | 40 | // Write access to status register 41 | const ( 42 | clrHalt statusFlags = 1 << iota 43 | setHalt 44 | clrBroke 45 | clrIntr 46 | setIntr 47 | clrSingleStep 48 | setSingleStep 49 | clrIntbreak 50 | setIntbreak 51 | clrSig0 52 | setSig0 53 | clrSig1 54 | setSig1 55 | clrSig2 56 | setSig2 57 | clrSig3 58 | setSig3 59 | clrSig4 60 | setSig4 61 | clrSig5 62 | setSig5 63 | clrSig6 64 | setSig6 65 | clrSig7 66 | setSig7 67 | ) 68 | 69 | type registers struct { 70 | rspAddr mmio.R32[cpu.Addr] 71 | rdramAddr mmio.R32[cpu.Addr] 72 | readLen mmio.U32 73 | writeLen mmio.U32 74 | status mmio.R32[statusFlags] 75 | dmaFull mmio.U32 76 | dmaBusy mmio.U32 77 | semaphore mmio.U32 78 | } 79 | 80 | const ( 81 | DMEM = Memory(0x0400_0000) 82 | IMEM = Memory(0x0400_1000) 83 | ) 84 | 85 | func SetInterrupt(en bool) { 86 | if en { 87 | regs().status.Store(setIntbreak) 88 | } else { 89 | regs().status.Store(clrIntbreak) 90 | } 91 | } 92 | 93 | func Stopped() bool { return regs().status.LoadBits(halted|dmaBusy) == halted } 94 | func Broke() bool { return regs().status.LoadBits(broke) != 0 } 95 | func Resume() { regs().status.Store(clrBroke | clrHalt) } 96 | func Step() { 97 | regs().status.Store(setSingleStep) 98 | Resume() 99 | for !Stopped() { 100 | // wait 101 | } 102 | } 103 | 104 | type Signal uint8 105 | 106 | func Signals() Signal { return Signal(regs().status.Load() >> 7) } 107 | func SetSignals(s Signal) { regs().status.Store(s.SetMask()) } 108 | func ClearSignals(s Signal) { regs().status.Store(s.ClearMask()) } 109 | 110 | func (s Signal) SetMask() statusFlags { return statusFlags(interleave(uint8(s))) << 10 } 111 | func (s Signal) ClearMask() statusFlags { return statusFlags(interleave(uint8(s))) << 9 } 112 | 113 | // interleave puts a zero bit before every bit in mask. 114 | func interleave(mask uint8) (r uint16) { 115 | r = uint16(mask) 116 | r = (r ^ (r << 4)) & 0x0f0f 117 | r = (r ^ (r << 2)) & 0x3333 118 | r = (r ^ (r << 1)) & 0x5555 119 | return 120 | } 121 | 122 | // PC returns the RSP's current program counter value. Can only be read while 123 | // halted, otherwise returns 0. 124 | func PC() cpu.Addr { 125 | if Stopped() { 126 | return pc().Load() 127 | } 128 | return 0xffff_ffff 129 | } 130 | -------------------------------------------------------------------------------- /rcp/periph/mmio.go: -------------------------------------------------------------------------------- 1 | package periph 2 | 3 | import ( 4 | "embedded/mmio" 5 | "embedded/rtos" 6 | "sync/atomic" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/clktmr/n64/rcp/cpu" 11 | ) 12 | 13 | // R32 represents an register on the PI external bus. 14 | // - 0x0500_0000 to 0x1fbf_ffff 15 | // - 0x1fd0_0000 to 0x7fff_ffff 16 | // 17 | // MMIO on the PI external bus has additional sync and aligment requirements. 18 | // Further reading: https://n64brew.dev/wiki/Memory_map#Physical_Memory_Map_accesses 19 | type R32[T mmio.T32] struct{ r uint32 } 20 | type U32 struct{ R32[uint32] } 21 | 22 | // Store writes the value to the register. If the PI bus is currently busy via 23 | // MMIO or DMA the goroutine is parked until the value was written. 24 | func (r *R32[T]) Store(val T) { 25 | bufid, p, done := getBuf() 26 | _ = p[3] 27 | p[0] = byte(val >> 24) 28 | p[1] = byte(val >> 16) 29 | p[2] = byte(val >> 8) 30 | p[3] = byte(val) 31 | dma(dmaJob{cpu.PhysicalAddress(r), p[:], dmaStore, done}) 32 | if !done.Wait(1 * time.Second) { 33 | panic("dma timeout") 34 | } 35 | putBuf(bufid) 36 | } 37 | 38 | // Load reads the value from the register. If the PI bus is currently busy via 39 | // MMIO or DMA the goroutine is parked until the value was read. 40 | func (r *R32[T]) Load() (v T) { 41 | bufid, p, done := getBuf() 42 | dma(dmaJob{cpu.PhysicalAddress(r), p[:], dmaLoad, done}) 43 | if !done.Wait(1 * time.Second) { 44 | panic("dma timeout") 45 | } 46 | v = T(p[0])<<24 | T(p[1])<<16 | T(p[2])<<8 | T(p[3]) 47 | putBuf(bufid) 48 | return 49 | } 50 | 51 | // StoreSafe is the same as [R32.Store] but instead of parking the goroutine it 52 | // will busywait until done, which makes it safe to use from interrupt. 53 | // 54 | //go:nosplit 55 | func (r *R32[T]) StoreSafe(v T) { 56 | for !dmaActive.CompareAndSwap(false, true) { 57 | // wait 58 | } 59 | (*r32[T])(unsafe.Pointer(r)).Store(v) 60 | dmaActive.Store(false) 61 | } 62 | 63 | // LoadSafe is the same as [R32.Load] but instead of parking the goroutine it 64 | // will busywait until done, which makes it safe to use from interrupt. 65 | // 66 | //go:nosplit 67 | func (r *R32[T]) LoadSafe() (v T) { 68 | for !dmaActive.CompareAndSwap(false, true) { 69 | // wait 70 | } 71 | v = (*r32[T])(unsafe.Pointer(r)).Load() 72 | dmaActive.Store(false) 73 | return 74 | } 75 | 76 | // Addr returns the virtual address which is used to access the register. 77 | func (r *R32[_]) Addr() uintptr { 78 | return uintptr(unsafe.Pointer(r)) 79 | } 80 | 81 | var dmaBufPool [32]struct { 82 | buf [4]byte 83 | done rtos.Cond 84 | used atomic.Bool 85 | } 86 | 87 | func getBuf() (int, []byte, *rtos.Cond) { 88 | for i := range dmaBufPool { 89 | b := &dmaBufPool[i] 90 | if b.used.CompareAndSwap(false, true) { 91 | return i, b.buf[:], &b.done 92 | } 93 | } 94 | 95 | var buf [4]byte 96 | return -1, buf[:], &rtos.Cond{} 97 | } 98 | 99 | func putBuf(i int) { 100 | if i < 0 { 101 | return 102 | } 103 | dmaBufPool[i].used.Store(false) 104 | } 105 | 106 | type u32 struct{ r32[uint32] } 107 | type r32[T mmio.T32] struct{ r mmio.R32[T] } 108 | 109 | //go:nosplit 110 | func (r *r32[T]) Store(v T) { 111 | r.r.Store(v) 112 | for regs().status.Load()&(ioBusy) != 0 { 113 | // wait 114 | } 115 | } 116 | 117 | //go:nosplit 118 | func (r *r32[T]) Load() T { return r.r.Load() } 119 | 120 | //go:nosplit 121 | func (r *r32[_]) Addr() uintptr { return r.r.Addr() } 122 | -------------------------------------------------------------------------------- /drivers/rspq/state.go: -------------------------------------------------------------------------------- 1 | package rspq 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/clktmr/n64/rcp/cpu" 8 | "github.com/clktmr/n64/rcp/rsp" 9 | ) 10 | 11 | const ( 12 | MaxCommandSize = 62 13 | MaxShortCommandSize = 16 14 | ) 15 | 16 | var ( 17 | rspqData rspQueue 18 | 19 | ctx = lowpri 20 | lowpri = newContext(0x200, sigBufdoneLow) 21 | highpri = newContext(0x80, sigBufdoneHigh) 22 | 23 | dummyOverlayState = cpu.MakePaddedSlice[uint64](2) 24 | ) 25 | 26 | func newContext(bufsize int, signal rsp.Signal) *context { 27 | ctx := &context{bufdoneSig: signal} 28 | for i := range ctx.buffers { 29 | ctx.buffers[i] = cpu.MakePaddedSlice[uint32](bufsize) 30 | } 31 | return ctx 32 | } 33 | 34 | // This struct isn't known by the rsp_queue microcode. 35 | // See rspq_ctx_t in libdragons's rspq.c 36 | type context struct { 37 | buffers [2][]uint32 38 | bufIdx int 39 | bufdoneSig rsp.Signal 40 | cur int 41 | } 42 | 43 | func (p *context) ClearBuffer(idx int) { 44 | buffer := cpu.UncachedSlice(p.buffers[idx]) 45 | clear(buffer) 46 | } 47 | 48 | type overlayDescriptor struct { 49 | Code, Data, State cpu.Addr 50 | CodeSize, DataSize uint16 51 | } 52 | 53 | // Struct layout is known by rsp_queue microcode and copied to DMEM. See 54 | // rsp_queue_s in libdragons's rspq_internal.h 55 | type rspQueue struct { 56 | Tables struct { 57 | OverlayTable [0x10]uint8 58 | OverlayDescriptor [8]overlayDescriptor 59 | } 60 | RSPQPointerStack [8]uint32 61 | RSPQDramLowpriAddr cpu.Addr 62 | RSPQDramHighpriAddr cpu.Addr 63 | RSPQDramAddr cpu.Addr 64 | RSPQRdpSentinel uint32 65 | RSPQRdpMode struct { 66 | Combiner uint64 67 | CombinerMipMapMask uint64 68 | BlendStep0, BlendStep1 uint32 69 | OtherModes uint64 70 | } 71 | RDPScissorRect uint64 72 | RSPQRdpBuffers [2]cpu.Addr 73 | RSPQRdpCurrent uint32 74 | RDPFillColor uint32 75 | RDPTargetBitdepth uint8 76 | RDPSyncfullOngoing uint8 77 | RDPQDebug uint8 78 | _ uint8 79 | CurrentOvl int16 80 | } 81 | 82 | // Struct layout is known by rsp_queue microcode and copied to DMEM. 83 | type rspqOverlayHeader struct { 84 | Fields struct { 85 | StateStart uint16 // Start of the portion of DMEM used as "state" 86 | StateSize uint16 // Size of the portion of DMEM used as "state" 87 | CommandBase uint16 // Primary overlay ID used for this overlay 88 | _ uint16 89 | } 90 | Commands []uint16 91 | } 92 | 93 | func loadOverlayHeader(r io.Reader) (*rspqOverlayHeader, error) { 94 | p := &rspqOverlayHeader{} 95 | err := binary.Read(r, binary.BigEndian, &p.Fields) 96 | if err != nil { 97 | return nil, err 98 | } 99 | p.Commands = cpu.MakePaddedSlice[uint16](maxOverlayCommandCount) 100 | for i := range p.Commands { 101 | err = binary.Read(r, binary.BigEndian, &p.Commands[i]) 102 | if err != nil { 103 | return nil, err 104 | } 105 | if p.Commands[i] == 0 { 106 | p.Commands = p.Commands[:i] 107 | break 108 | } 109 | } 110 | return p, nil 111 | } 112 | 113 | func (p *rspqOverlayHeader) Store(w io.Writer) error { 114 | err := binary.Write(w, binary.BigEndian, &p.Fields) 115 | if err != nil { 116 | return err 117 | } 118 | err = binary.Write(w, binary.BigEndian, append(p.Commands, 0)) 119 | if err != nil { 120 | return err 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /rcp/fixed/mkfixed.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "go/format" 9 | "io" 10 | "log" 11 | "os" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | var fixedTemplate = ` 17 | func {{ .Name }}U(i int) {{ .Name }} { return {{ .Name }}(i<<{{ .Frac }}) } 18 | func {{ .Name }}F(f float32) {{ .Name }} { return {{ .Name }}(f*(1<<{{ .Frac }})) } 19 | 20 | func (x {{ .Name }}) Floor() int { return int(x >> {{ .Frac }}) } 21 | func (x {{ .Name }}) Ceil() int { return int({{ .MulType }}(x) + (1<<{{ .Frac }} - 1) >> {{ .Frac }}) } 22 | func (x {{ .Name }}) Mul(y {{ .Name }}) {{ .Name }} { return {{ .Name }}(({{ .MulType }}(x)*{{ .MulType }}(y))>>{{ .Frac }}) } 23 | func (x {{ .Name }}) Div(y {{ .Name }}) {{ .Name }} { return {{ .Name }}({{ .MulType }}(x)<<{{ .Frac }}/{{ .MulType }}(y)) } 24 | 25 | func (x {{ .Name }}) String() string { 26 | const shift, mask = {{ .Frac }}, 1<<{{ .Frac }} - 1 27 | return fmt.Sprintf("%d:%0{{ .Digits }}d", {{ .MulType }}(x>>shift), {{ .MulType }}(x&mask)) 28 | } 29 | ` 30 | 31 | type fixedType struct { 32 | Name, BaseType, MulType string 33 | Frac, Digits uint 34 | } 35 | 36 | func fromDecl(name, basetype string) (f fixedType) { 37 | f.Name = name 38 | f.BaseType = basetype 39 | switch basetype { 40 | case "int32": 41 | f.MulType = "int64" 42 | case "uint32": 43 | f.MulType = "uint64" 44 | case "int16": 45 | f.MulType = "int32" 46 | case "uint16": 47 | f.MulType = "uint32" 48 | case "int8": 49 | f.MulType = "int16" 50 | case "uint8": 51 | f.MulType = "uint16" 52 | default: 53 | log.Fatalln("unsupported basetype:", basetype) 54 | } 55 | 56 | var signed, found bool 57 | if name, found = strings.CutPrefix(name, "Int"); found { 58 | signed = true 59 | } else if name, found = strings.CutPrefix(name, "UInt"); found { 60 | signed = false 61 | } else { 62 | log.Fatalln("invalid name:", f.Name) 63 | } 64 | 65 | var intbits, width uint 66 | _, err := fmt.Sscanf(name, "%d_%d", &intbits, &f.Frac) 67 | if err != nil && err != io.EOF { 68 | log.Fatalln(err) 69 | } 70 | if signed { 71 | _, err = fmt.Sscanf(basetype, "int%d", &width) 72 | } else { 73 | _, err = fmt.Sscanf(basetype, "uint%d", &width) 74 | } 75 | if err != nil && err != io.EOF { 76 | log.Fatalln(err) 77 | } 78 | if f.Frac+intbits != width { 79 | log.Fatalln("must use all bits") 80 | } 81 | f.Digits = digits(f.Frac) 82 | return 83 | } 84 | 85 | func digits(bits uint) uint { 86 | return uint(len(fmt.Sprint((1 << bits) - 1))) 87 | } 88 | 89 | func usage() { 90 | fmt.Printf("Usage: %v \n", os.Args[0]) 91 | } 92 | 93 | func main() { 94 | log.Default().SetFlags(log.Lshortfile) 95 | if len(os.Args) != 3 { 96 | usage() 97 | os.Exit(1) 98 | } 99 | 100 | source := bytes.NewBuffer(nil) 101 | tmpl, err := template.New("fixedTemplate").Parse(fixedTemplate) 102 | if err != nil { 103 | log.Fatalln(err) 104 | } 105 | 106 | fmt.Fprintln(source, "package fixed") 107 | fmt.Fprintln(source, "import \"fmt\"") 108 | 109 | err = tmpl.Execute(source, fromDecl(os.Args[1], os.Args[2])) 110 | if err != nil { 111 | log.Fatalln(err) 112 | } 113 | 114 | formattedSource, err := format.Source(source.Bytes()) 115 | if err != nil { 116 | log.Fatalln(err) 117 | } 118 | err = os.WriteFile(strings.ToLower(os.Args[1])+"_fixed.go", formattedSource, 0644) 119 | if err != nil { 120 | log.Fatalln(err) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /fonts/subfont.go: -------------------------------------------------------------------------------- 1 | package fonts 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "image" 7 | "path" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/clktmr/n64/drivers/cartfs" 12 | "github.com/clktmr/n64/rcp/texture" 13 | "github.com/embeddedgo/display/font/subfont" 14 | ) 15 | 16 | // SubfontData implements [subfont.Data]. 17 | type SubfontData struct { 18 | fontMap *texture.Texture 19 | glyphs []Glyph 20 | } 21 | 22 | func (p *SubfontData) Advance(i int) int { 23 | return int(p.glyphs[i].Advance) 24 | } 25 | 26 | func (p *SubfontData) Glyph(i int) (img image.Image, origin image.Point, advance int) { 27 | g := &p.glyphs[i] 28 | r := image.Rect(int(g.Rect.Min.X), int(g.Rect.Min.Y), int(g.Rect.Max.X), int(g.Rect.Max.Y)) 29 | img = p.fontMap.SubImage(r) 30 | origin = image.Pt(int(g.Origin.X), int(g.Origin.Y)) 31 | advance = int(g.Advance) 32 | return 33 | } 34 | 35 | //go:nosplit 36 | func (p *SubfontData) GlyphMap(i int) (img image.Image, r image.Rectangle, origin image.Point, advance int) { 37 | g := &p.glyphs[i] 38 | img = p.fontMap 39 | r = image.Rect(int(g.Rect.Min.X), int(g.Rect.Min.Y), int(g.Rect.Max.X), int(g.Rect.Max.Y)) 40 | origin = image.Pt(int(g.Origin.X), int(g.Origin.Y)) 41 | advance = int(g.Advance) 42 | return 43 | } 44 | 45 | // Returns data for a subfont from an image. 46 | func NewSubfontData(pos, tex []byte) *SubfontData { 47 | f := &SubfontData{} 48 | 49 | fontMap, err := texture.Load(bytes.NewReader(tex)) 50 | if err != nil { 51 | panic(err) 52 | } 53 | f.fontMap = fontMap 54 | 55 | // TODO perf: use unsafe to cast pos from []byte to []Glyph 56 | f.glyphs = make([]Glyph, len(pos)/7) 57 | err = binary.Read(bytes.NewReader(pos), binary.BigEndian, &f.glyphs) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | return f 63 | } 64 | 65 | type Loader struct { 66 | FS *cartfs.FS 67 | Height, Ascent int 68 | files []subfontPath 69 | } 70 | 71 | type subfontPath struct { 72 | first, last rune 73 | path string 74 | } 75 | 76 | func NewLoader(fs *cartfs.FS, height, ascent int) (l *Loader) { 77 | l = &Loader{fs, height, ascent, nil} 78 | 79 | entries, err := l.FS.ReadDir(".") 80 | if err != nil { 81 | panic(err) 82 | } 83 | for _, entry := range entries { 84 | if ext := path.Ext(entry.Name()); ext == ".pos" { 85 | name := strings.TrimSuffix(entry.Name(), ext) 86 | start, err := strconv.ParseUint(name[:4], 16, 0) 87 | if err != nil { 88 | panic(err) 89 | } 90 | end, err := strconv.ParseUint(name[5:9], 16, 0) 91 | if err != nil { 92 | panic(err) 93 | } 94 | l.files = append(l.files, subfontPath{rune(start), rune(end), name}) 95 | } 96 | } 97 | return 98 | } 99 | 100 | func (l *Loader) Load(r rune, current []*subfont.Subfont) (containing *subfont.Subfont, updated []*subfont.Subfont) { 101 | for _, f := range l.files { 102 | if r >= f.first && r <= f.last { 103 | containing = l.loadSubfont(f.path, f.first, f.last) 104 | updated = append(current, containing) 105 | return 106 | } 107 | } 108 | updated = current 109 | return 110 | } 111 | 112 | func (l *Loader) loadSubfont(name string, first, last rune) *subfont.Subfont { 113 | sfPos, err := l.FS.ReadFile(name + ".pos") 114 | if err != nil { 115 | panic(err) 116 | } 117 | sfTex, err := l.FS.ReadFile(name + ".tex") 118 | if err != nil { 119 | panic(err) 120 | } 121 | return &subfont.Subfont{ 122 | First: first, 123 | Last: last, 124 | Offset: 0, 125 | Data: NewSubfontData(sfPos, sfTex), 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-pty v0.2.2 h1:YZREB4eSj+1xdbbItIokX0ekjjeifgJOA+ZvxU4/WM8= 2 | github.com/aymanbagabas/go-pty v0.2.2/go.mod h1:gfvlwH+0U66BCwxJREjJaAOEs9H1OFf3YFjI9WSiZ04= 3 | github.com/buildkite/shellwords v1.0.0 h1:NqZ4Ynp0dar6ACdP5X2RwI8BnNSvuKFf+2StuJl8tjM= 4 | github.com/buildkite/shellwords v1.0.0/go.mod h1:h/h4NjidF4MJARI+cfAmA/GFChSdroL1GOJXD68s6qU= 5 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 6 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 7 | github.com/embeddedgo/display v1.1.0 h1:jXlY6/FeaUV89hX+ANGpTKogZWjhgno7vrCh86WA8aY= 8 | github.com/embeddedgo/display v1.1.0/go.mod h1:0sxxBoklqMbA1uBqEAg8Plntmwvt7cNsgxaf7UaS4yw= 9 | github.com/embeddedgo/fs v0.1.0 h1:M4bOltC28+cWixII514NKExkrRzBnjWLaRbTvZ87kIE= 10 | github.com/embeddedgo/fs v0.1.0/go.mod h1:0PeMg4i1WKpXdRqXEktx9o0Wd9EIzwWD4KTBbvAvZ3Q= 11 | github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU= 12 | github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= 16 | github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= 17 | github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0= 18 | github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc= 19 | github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= 20 | github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= 21 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 22 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 23 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 24 | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 25 | golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= 26 | golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= 27 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= 28 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 29 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 30 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 31 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 32 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 33 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 34 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 35 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 36 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 37 | golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= 38 | golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 39 | rsc.io/rsc v0.0.0-20180427141835-fc6202590229 h1:6s5zUknxnRp4D3GlNb7uDzlcfFVq9G2ficO+k4Bcb6w= 40 | rsc.io/rsc v0.0.0-20180427141835-fc6202590229/go.mod h1:nHU4RAWoD9u1Hr+vTW0mktVbANmwCPkTwT2xNpVs/70= 41 | -------------------------------------------------------------------------------- /rcp/fixed/fixed.go: -------------------------------------------------------------------------------- 1 | // Package fixed provides fixed-point arithmetic types used by the RCP. 2 | package fixed 3 | 4 | import "golang.org/x/exp/constraints" 5 | 6 | //go:generate go run mkfixed.go UInt14_2 uint16 7 | type UInt14_2 uint16 8 | 9 | //go:generate go run mkfixed.go Int11_5 int16 10 | type Int11_5 int16 11 | 12 | //go:generate go run mkfixed.go Int6_10 int16 13 | type Int6_10 int16 14 | 15 | type Fixed[T any] interface { 16 | constraints.Integer 17 | 18 | Mul(T) T 19 | Div(T) T 20 | } 21 | 22 | type Point[T Fixed[T]] struct { 23 | X, Y T 24 | } 25 | 26 | // Add returns the vector p+q. 27 | func (p Point[T]) Add(q Point[T]) Point[T] { 28 | return Point[T]{p.X + q.X, p.Y + q.Y} 29 | } 30 | 31 | // Sub returns the vector p-q. 32 | func (p Point[T]) Sub(q Point[T]) Point[T] { 33 | return Point[T]{p.X - q.X, p.Y - q.Y} 34 | } 35 | 36 | // Mul returns the vector p*k. 37 | func (p Point[T]) Mul(k T) Point[T] { 38 | return Point[T]{p.X.Mul(k), p.Y.Mul(k)} 39 | } 40 | 41 | // Div returns the vector p/k. 42 | func (p Point[T]) Div(k T) Point[T] { 43 | return Point[T]{p.X.Div(k), p.Y.Div(k)} 44 | } 45 | 46 | // Rectangle is a fixed-point coordinate rectangle. The Min bound is inclusive 47 | // and the Max bound is exclusive. It is well-formed if Min.X <= Max.X and 48 | // likewise for Y. 49 | // 50 | // It is analogous to the image.Rectangle type in the standard library. 51 | type Rectangle[T Fixed[T]] struct { 52 | Min, Max Point[T] 53 | } 54 | 55 | // Add returns the rectangle r translated by p. 56 | func (r Rectangle[T]) Add(p Point[T]) Rectangle[T] { 57 | return Rectangle[T]{ 58 | Point[T]{r.Min.X + p.X, r.Min.Y + p.Y}, 59 | Point[T]{r.Max.X + p.X, r.Max.Y + p.Y}, 60 | } 61 | } 62 | 63 | // Sub returns the rectangle r translated by -p. 64 | func (r Rectangle[T]) Sub(p Point[T]) Rectangle[T] { 65 | return Rectangle[T]{ 66 | Point[T]{r.Min.X - p.X, r.Min.Y - p.Y}, 67 | Point[T]{r.Max.X - p.X, r.Max.Y - p.Y}, 68 | } 69 | } 70 | 71 | // Intersect returns the largest rectangle contained by both r and s. If the two 72 | // rectangles do not overlap then the zero rectangle will be returned. 73 | func (r Rectangle[T]) Intersect(s Rectangle[T]) Rectangle[T] { 74 | if r.Min.X < s.Min.X { 75 | r.Min.X = s.Min.X 76 | } 77 | if r.Min.Y < s.Min.Y { 78 | r.Min.Y = s.Min.Y 79 | } 80 | if r.Max.X > s.Max.X { 81 | r.Max.X = s.Max.X 82 | } 83 | if r.Max.Y > s.Max.Y { 84 | r.Max.Y = s.Max.Y 85 | } 86 | // Letting r0 and s0 be the values of r and s at the time that the method 87 | // is called, this next line is equivalent to: 88 | // 89 | // if max(r0.Min.X, s0.Min.X) >= min(r0.Max.X, s0.Max.X) || likewiseForY { etc } 90 | if r.Empty() { 91 | return Rectangle[T]{} 92 | } 93 | return r 94 | } 95 | 96 | // Union returns the smallest rectangle that contains both r and s. 97 | func (r Rectangle[T]) Union(s Rectangle[T]) Rectangle[T] { 98 | if r.Empty() { 99 | return s 100 | } 101 | if s.Empty() { 102 | return r 103 | } 104 | if r.Min.X > s.Min.X { 105 | r.Min.X = s.Min.X 106 | } 107 | if r.Min.Y > s.Min.Y { 108 | r.Min.Y = s.Min.Y 109 | } 110 | if r.Max.X < s.Max.X { 111 | r.Max.X = s.Max.X 112 | } 113 | if r.Max.Y < s.Max.Y { 114 | r.Max.Y = s.Max.Y 115 | } 116 | return r 117 | } 118 | 119 | // Empty returns whether the rectangle contains no points. 120 | func (r Rectangle[T]) Empty() bool { 121 | return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y 122 | } 123 | 124 | // In returns whether every point in r is in s. 125 | func (r Rectangle[T]) In(s Rectangle[T]) bool { 126 | if r.Empty() { 127 | return true 128 | } 129 | // Note that r.Max is an exclusive bound for r, so that r.In(s) 130 | // does not require that r.Max.In(s). 131 | return s.Min.X <= r.Min.X && r.Max.X <= s.Max.X && 132 | s.Min.Y <= r.Min.Y && r.Max.Y <= s.Max.Y 133 | } 134 | -------------------------------------------------------------------------------- /drivers/controller/controller.go: -------------------------------------------------------------------------------- 1 | // Package controller implements helpers for reading the states of the gamepads 2 | // and their pak accessories. 3 | package controller 4 | 5 | import ( 6 | "github.com/clktmr/n64/rcp/serial/joybus" 7 | ) 8 | 9 | // Port represents the state of a joybus port as returned by 10 | // [joybus.InfoCommand]. 11 | type Port struct { 12 | current, last struct { 13 | device joybus.Device 14 | flags byte 15 | } 16 | 17 | number uint8 18 | err error 19 | } 20 | 21 | // Nr returns the joybus port's number from 0 to 3. 22 | func (p *Port) Nr() uint8 { 23 | return p.number 24 | } 25 | 26 | const ( 27 | pakInserted = 0x01 28 | pakNotInserted = 0x02 29 | ) 30 | 31 | // Controller represents the state of a connected controller port as returned by 32 | // [joybus.ControllerStateCommand]. 33 | type Controller struct { 34 | // The joybus port the controller is connected to. 35 | Port 36 | 37 | current, last struct { 38 | down joybus.ButtonMask 39 | xAxis int8 40 | yAxis int8 41 | } 42 | 43 | err error 44 | } 45 | 46 | // Down reports which buttons were pressed during the last call to [Poll]. 47 | func (c *Controller) Down() joybus.ButtonMask { 48 | return c.current.down 49 | } 50 | 51 | // Changed reports which buttons changed state between the last two calls to 52 | // [Poll]. 53 | func (c *Controller) Changed() joybus.ButtonMask { 54 | return c.current.down ^ c.last.down 55 | } 56 | 57 | // Pressed reports which buttons were pressed between the last two calls to 58 | // [Poll]. 59 | func (c *Controller) Pressed() joybus.ButtonMask { 60 | return c.Changed() & c.current.down 61 | } 62 | 63 | // Released reports which buttons were released between the last two calls to 64 | // [Poll]. 65 | func (c *Controller) Released() joybus.ButtonMask { 66 | return c.Changed() & c.last.down 67 | } 68 | 69 | // X returns the raw horizontal position of the analog stick. It typically 70 | // ranges from -85 to 85. 71 | func (c *Controller) X() int8 { 72 | return c.current.xAxis 73 | } 74 | 75 | // Y returns the raw vertical position of the analog stick. It typically ranges 76 | // from -85 to 85. 77 | func (c *Controller) Y() int8 { 78 | return c.current.yAxis 79 | } 80 | 81 | // DX returns the change of the analog stick's horizontal position between the 82 | // last two calls to [Poll]. 83 | func (c *Controller) DX() int8 { 84 | return c.current.xAxis - c.last.xAxis 85 | } 86 | 87 | // DX returns the change of the analog stick's vertical position between the 88 | // last two calls to [Poll]. 89 | func (c *Controller) DY() int8 { 90 | return c.current.yAxis - c.last.yAxis 91 | } 92 | 93 | // Present reports whether a controller is connected to the port. It will return 94 | // false if no device is connected or 95 | func (c *Controller) Present() bool { 96 | return c.Port.current.device == joybus.Controller 97 | } 98 | 99 | // Plugged reports if a controller was plugged into the port between the last 100 | // two calls to [Poll]. 101 | func (c *Controller) Plugged() bool { 102 | return c.Port.current.device == joybus.Controller && 103 | c.Port.last.device != joybus.Controller 104 | } 105 | 106 | // Unplugged reports if a controller was unplugged from the port between the 107 | // last two calls to [Poll]. 108 | func (c *Controller) Unplugged() bool { 109 | return c.Port.current.device != joybus.Controller && 110 | c.Port.last.device == joybus.Controller 111 | } 112 | 113 | // PakInserted reports if a pak accessory was inserted into the controller 114 | // between the last two calls to [Poll]. 115 | func (c *Controller) PakInserted() bool { 116 | return c.Port.current.flags&pakInserted != 0 && 117 | c.Port.last.flags&pakInserted == 0 118 | } 119 | 120 | // PakRemoved reports if a pak accessory was removed from the controller between 121 | // the last two calls to [Poll]. 122 | func (c *Controller) PakRemoved() bool { 123 | return c.Port.current.flags&pakNotInserted != 0 && 124 | c.Port.last.flags&pakInserted != 0 125 | } 126 | -------------------------------------------------------------------------------- /drivers/carts/summercart64/regs.go: -------------------------------------------------------------------------------- 1 | // Package summercart64 implements support for SummerCart64. 2 | // 3 | // See https://summercart64.dev/ 4 | package summercart64 5 | 6 | import ( 7 | "github.com/clktmr/n64/rcp/cpu" 8 | "github.com/clktmr/n64/rcp/periph" 9 | ) 10 | 11 | const bufferSize = 512 12 | 13 | // It's up to us to choose a location in the ROM. This puts it at the end of a 14 | // 64MB cartridge. 15 | var usbBuf = periph.NewDevice(0x1400_0000-bufferSize, bufferSize) 16 | 17 | type registers struct { 18 | status periph.R32[status] 19 | data0 periph.U32 20 | data1 periph.U32 21 | identifier periph.U32 22 | key periph.U32 23 | } 24 | 25 | func regs() *registers { return cpu.MMIO[registers](0x1fff_0000) } 26 | 27 | type status uint32 28 | 29 | const ( 30 | statusBusy status = 1 << 31 31 | statusError status = 1 << 30 32 | statusIrqPending status = 1 << 29 33 | statusCmdIdMask status = 0xff 34 | ) 35 | 36 | type command status 37 | 38 | const ( 39 | cmdIdentifierGet command = 'v' 40 | cmdVersionGet command = 'V' 41 | cmdConfigGet command = 'c' 42 | cmdConfigSet command = 'C' 43 | cmdSettingGet command = 'a' 44 | cmdSettingSet command = 'A' 45 | cmdTimeGet command = 't' 46 | cmdTimeSet command = 'T' 47 | cmdUSBRead command = 'm' 48 | cmdUSBWrite command = 'M' 49 | cmdUSBReadStatus command = 'u' 50 | cmdUSBWriteStatus command = 'U' 51 | cmdSDCardOp command = 'i' 52 | cmdSDSectorSet command = 'I' 53 | cmdSDRead command = 's' 54 | cmdSDWrite command = 'S' 55 | cmdDiskMappingSet command = 'D' 56 | cmdWritebackPending command = 'w' 57 | cmdWritebackSDInfo command = 'W' 58 | cmdFlashProgram command = 'K' 59 | cmdFlashWaitBusy command = 'p' 60 | cmdFlashEraseBlock command = 'P' 61 | cmdDiagnosticGet command = '%' 62 | ) 63 | 64 | const ( 65 | SaveNone = iota 66 | SaveEEPROM4k 67 | SaveEEPROM16k 68 | SaveSRAM 69 | SaveFlashRAM 70 | SaveSRAMBanked 71 | SaveSRAM1m 72 | ) 73 | 74 | var saveStorageParams = [...]struct { 75 | addr cpu.Addr 76 | size uint32 77 | }{ 78 | {0x0800_0000, 0}, 79 | {0x1ffe_2000, 512}, 80 | {0x1ffe_2000, 2048}, 81 | {0x0800_0000, 32 * 1024}, 82 | {0x0800_0000, 128 * 1024}, 83 | {0x0800_0000, 3 * 32 * 1024}, 84 | {0x0800_0000, 128 * 1024}, 85 | } 86 | 87 | // Cart represents a SummerCart64. 88 | type Cart struct { 89 | saveStorage periph.Device 90 | } 91 | 92 | // Probe returns the [Cart] if a SummerCart64 was detected and enables write 93 | // access to the ROM. 94 | func Probe() *Cart { 95 | // sc64 magic unlock sequence 96 | regs().key.Store(0x0) 97 | regs().key.Store(0x5f554e4c) 98 | regs().key.Store(0x4f434b5f) 99 | 100 | if regs().identifier.Load() == 0x53437632 { // SummerCart64 V2 101 | s := &Cart{} 102 | if st, err := s.Config(CfgSaveType); err == nil { 103 | params := saveStorageParams[st] 104 | s.saveStorage = *periph.NewDevice(params.addr, params.size) 105 | } 106 | 107 | _, _ = s.SetConfig(CfgROMWriteEnable, 1) 108 | 109 | return s 110 | } 111 | return nil 112 | } 113 | 114 | // Close disables the Cart by setting the ROM to read-only. 115 | func (v *Cart) Close() (err error) { 116 | _, err = v.SetConfig(CfgROMWriteEnable, 0) 117 | return 118 | } 119 | 120 | // Returns the current storage for save files, configured by savetype. Returns a 121 | // device with Size==0 if no savetype is configured. 122 | func (v *Cart) SaveStorage() *periph.Device { 123 | // FIXME shouldn't be here, instead have a generic Probe function to get 124 | // storage. Otherwise we could get multiple periph.Devices pointing to 125 | // the same address range, messing up the caching. 126 | // FIXME no writeback triggered for EEPROM savetypes 127 | return &v.saveStorage 128 | } 129 | 130 | // ClearInterrupt clears a pending interrupt raised by the cart. Call this from 131 | // the handler if your application implements a custom one for 132 | // [github.com/clktmr/n64/rcp.IrqCart]. 133 | // 134 | //go:nosplit 135 | func (v *Cart) ClearInterrupt() { 136 | regs().identifier.StoreSafe(0) 137 | } 138 | -------------------------------------------------------------------------------- /tools/toolexec/archive.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package toolexec 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "os" 13 | ) 14 | 15 | // archive is an archive generated by go compile -pack, for details see 16 | // go/src/cmd/internal/archive/archive.go 17 | type archive struct { 18 | f *os.File 19 | Entries []entry 20 | } 21 | 22 | type entry struct { 23 | Name string 24 | Mtime int64 25 | Uid int 26 | Gid int 27 | Mode os.FileMode 28 | data 29 | } 30 | 31 | type data struct { 32 | Offset int64 33 | Size int64 34 | } 35 | 36 | const ( 37 | entryHeader = "%-16s%-12d%-6d%-6d%-8o%-10d`\n" 38 | entryHeaderScan = "%16s%12d%6d%6d%8o%10d" 39 | entryLen = 16 + 12 + 6 + 6 + 8 + 10 + 1 + 1 40 | ) 41 | 42 | var ( 43 | archiveHeader = []byte("!\n") 44 | archiveMagic = []byte("`\n") 45 | 46 | errCorruptArchive = errors.New("corrupt archive") 47 | errNotObject = errors.New("unrecognized object file format") 48 | ) 49 | 50 | // parseArchive parses an object file or archive from f. 51 | func parseArchive(f *os.File) (a *archive, err error) { 52 | t := make([]byte, 8) 53 | _, err = io.ReadFull(f, t) 54 | if err != nil { 55 | if err == io.EOF { 56 | err = io.ErrUnexpectedEOF 57 | } 58 | return nil, err 59 | } 60 | 61 | if !bytes.Equal(t, archiveHeader) { 62 | return nil, errNotObject 63 | } 64 | 65 | a = &archive{f: f} 66 | for { 67 | header := make([]byte, 60) 68 | _, err := io.ReadFull(a.f, header) 69 | if err == io.EOF { 70 | break 71 | } else if err != nil { 72 | return nil, err 73 | } 74 | 75 | if !bytes.Equal(header[58:60], archiveMagic) { 76 | return nil, errCorruptArchive 77 | } 78 | 79 | var ( 80 | name string 81 | mtime int64 82 | uid, gid int 83 | mode os.FileMode 84 | size int64 85 | ) 86 | _, err = fmt.Sscanf(string(header[:58]), entryHeaderScan, &name, &mtime, &uid, &gid, &mode, &size) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | fsize := size + size&1 92 | if fsize < 0 || fsize < size { 93 | return nil, errCorruptArchive 94 | } 95 | 96 | offset, _ := a.f.Seek(0, io.SeekCurrent) 97 | _, err = a.f.Seek(size, io.SeekCurrent) 98 | if err != nil { 99 | return nil, err 100 | } 101 | a.Entries = append(a.Entries, entry{ 102 | Name: name, 103 | Mtime: mtime, 104 | Uid: uid, 105 | Gid: gid, 106 | Mode: mode, 107 | data: data{offset, size}, 108 | }) 109 | 110 | if size&1 != 0 { 111 | a.f.Seek(1, io.SeekCurrent) 112 | } 113 | } 114 | 115 | return a, nil 116 | } 117 | 118 | func (a *archive) OpenEntry(name string) *io.SectionReader { 119 | for _, entry := range a.Entries { 120 | if entry.Name == name { 121 | return io.NewSectionReader(a.f, entry.Offset, entry.Size) 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | // AddEntry adds an entry to the end of a, with the content from r. 128 | func (a *archive) AddEntry(name string, r io.ReadSeeker) error { 129 | off, _ := a.f.Seek(0, io.SeekEnd) 130 | size, _ := r.Seek(0, io.SeekEnd) 131 | r.Seek(0, io.SeekStart) 132 | 133 | n, err := fmt.Fprintf(a.f, entryHeader, truncateToRune(name, 16), 0, 0, 0, 0, size) 134 | if err != nil || n != entryLen { 135 | fmt.Errorf("writing entry header: %w", err) 136 | } 137 | n1, _ := io.CopyN(a.f, r, size) 138 | if n1 != size { 139 | return err 140 | } 141 | if (off+size)&1 != 0 { 142 | a.f.Write([]byte{0}) // pad to even byte 143 | } 144 | a.Entries = append(a.Entries, entry{ 145 | Name: name, 146 | data: data{off + entryLen, size}, 147 | }) 148 | 149 | return nil 150 | } 151 | 152 | // truncateToRune returns the longest prefix of s, up to a maximum length of n, 153 | // terminating at the boundary after the last complete UTF-8 encoding that fits. 154 | func truncateToRune(s string, n int) string { 155 | t := "" 156 | for _, r := range s { 157 | if len(t+string(r)) > n { 158 | break 159 | } 160 | t += string(r) 161 | } 162 | return t 163 | } 164 | -------------------------------------------------------------------------------- /rcp/periph/dma.go: -------------------------------------------------------------------------------- 1 | package periph 2 | 3 | import ( 4 | "embedded/rtos" 5 | "sync/atomic" 6 | 7 | "github.com/clktmr/n64/rcp" 8 | "github.com/clktmr/n64/rcp/cpu" 9 | ) 10 | 11 | type dmaDirection bool 12 | 13 | const ( 14 | dmaStore dmaDirection = true // RDRAM -> PI bus 15 | dmaLoad dmaDirection = false // PI bus -> RDRAM 16 | ) 17 | 18 | type dmaJob struct { 19 | cart cpu.Addr 20 | buf []byte 21 | dir dmaDirection 22 | done *rtos.Cond 23 | } 24 | 25 | // initiate returns true if a dma transfer was started. If it returns false, 26 | // that means the whole job did fallback to mmio. 27 | func (job *dmaJob) initiate() bool { 28 | if job.buf == nil { 29 | return false 30 | } 31 | head, tail := job.split(job.buf, job.cart) 32 | dmaBuf, headBuf, tailBuf := job.buf[head:tail], job.buf[:head], job.buf[tail:] 33 | 34 | n := uint32(len(dmaBuf) - 1) 35 | if job.dir == dmaStore { 36 | rcp.WriteIO[*u32](job.cart, headBuf) 37 | rcp.WriteIO[*u32](job.cart+cpu.Addr(tail), tailBuf) 38 | if head == tail { 39 | return false 40 | } 41 | regs().dramAddr.Store(cpu.PhysicalAddressSlice(dmaBuf)) 42 | regs().cartAddr.Store(job.cart + cpu.Addr(head)) 43 | cpu.WritebackSlice(dmaBuf) 44 | regs().readLen.Store(n) 45 | } else { // dmaLoad 46 | if head == tail { 47 | return false 48 | } 49 | regs().dramAddr.Store(cpu.PhysicalAddressSlice(dmaBuf)) 50 | regs().cartAddr.Store(job.cart + cpu.Addr(head)) 51 | cpu.InvalidateSlice(dmaBuf) 52 | regs().writeLen.Store(n) 53 | } 54 | 55 | rcp.EnableInterrupts(rcp.IntrPeriph) 56 | return true 57 | } 58 | 59 | // finish does remaining mmio and wakeups any waiter on the job's note. 60 | func (job *dmaJob) finish() { 61 | rcp.DisableInterrupts(rcp.IntrPeriph) 62 | if job.buf != nil { 63 | head, tail := job.split(job.buf, job.cart) 64 | if job.dir == dmaLoad { 65 | // Do the IO after the DMA because it might invalidate parts of 66 | // head and tail. 67 | rcp.ReadIO[*u32](job.cart, job.buf[:head]) 68 | rcp.ReadIO[*u32](job.cart+cpu.Addr(tail), job.buf[tail:]) 69 | } 70 | } 71 | 72 | if job.done != nil { 73 | job.done.Signal() 74 | } 75 | } 76 | 77 | // split returns two positions which split p in three parts. The slice 78 | // p[head:tail] is safe for DMA, p[:head] and p[tail:] must fallback to mmio. 79 | func (job *dmaJob) split(p []byte, addr cpu.Addr) (head, tail int) { 80 | head, tail = cpu.Pads(p) 81 | 82 | // If DMA length isn't 2 byte aligned, fallback to mmio for last byte. 83 | if (tail-head)&0x1 != 0 { 84 | tail -= 1 85 | } 86 | 87 | // If DMA start address isn't 2 byte aligned there is no way to use DMA 88 | // at all, fallback to mmio for the whole transfer. 89 | if (addr+cpu.Addr(head))&0x1 != 0 { 90 | head = 0 91 | tail = 0 92 | } 93 | 94 | return 95 | } 96 | 97 | var dmaQueue rcp.IntrQueue[dmaJob] 98 | 99 | // If true: No PI interrupts scheduled, dmaQueue can be read. 100 | // If false: A PI interrupt will trigger and read the dmaQueue. 101 | var dmaActive atomic.Bool // TODO rename dmaQueueLock, make spinlock 102 | 103 | func init() { 104 | regs().status.Store(clearInterrupt) 105 | rcp.SetHandler(rcp.IntrPeriph, handler) 106 | } 107 | 108 | //go:nosplit 109 | //go:nowritebarrierrec 110 | func handler() { 111 | regs().status.Store(clearInterrupt) 112 | dmaActive.Store(false) 113 | 114 | job, ok := dmaQueue.Pop() 115 | if !ok { 116 | panic("unexpected dma intr") 117 | } 118 | job.finish() 119 | 120 | next: 121 | if job, ok = dmaQueue.Peek(); !ok { 122 | return 123 | } 124 | 125 | if !job.initiate() { 126 | if job, ok = dmaQueue.Pop(); !ok { 127 | panic("empty dma queue") 128 | } 129 | job.finish() 130 | goto next 131 | } 132 | 133 | dmaActive.Store(true) 134 | } 135 | 136 | // dma enqueues a DMA transfer for async execution by the hardware. 137 | func dma(v dmaJob) { 138 | dmaQueue.Push(v) 139 | // might preempt here, but that's ok 140 | if !dmaActive.Swap(true) { 141 | // initially trigger dma queue 142 | for { 143 | job, ok := dmaQueue.Peek() 144 | if !ok { 145 | dmaActive.Store(false) 146 | return 147 | } 148 | if activated := job.initiate(); activated { 149 | return 150 | } 151 | job.finish() 152 | dmaQueue.Pop() 153 | } 154 | } 155 | 156 | return 157 | } 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | go-version: stable 5 | ares-version: v144 6 | embeddedgo-version: go1.24.4-embedded 7 | 8 | on: 9 | push: 10 | branches: [ "master", "wip" ] 11 | pull_request: 12 | branches: [ "master" ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ env.go-version }} 25 | cache: true 26 | 27 | - name: Get gotip version 28 | if: ${{ env.embeddedgo-version == 'gotip-embedded' }} 29 | id: gotip-version 30 | run: | 31 | sha=$(git ls-remote -q https://github.com/clktmr/go.git/ master-embedded | cut -f1) 32 | echo "SHA=$sha" >> "$GITHUB_OUTPUT" 33 | 34 | - name: Cache embeddedgo build 35 | id: embeddedgo-build 36 | uses: actions/cache@v4 37 | with: 38 | path: ~/sdk 39 | key: ${{ env.embeddedgo-version }}-${{ steps.gotip-version.outputs.SHA }} 40 | 41 | - name: Install Embedded Go 42 | run: | 43 | go install -v github.com/embeddedgo/dl/${{ env.embeddedgo-version }}@latest 44 | ${{ env.embeddedgo-version }} version || ${{ env.embeddedgo-version }} download 45 | 46 | - name: Install n64go tool 47 | run: | 48 | go install ./tools/n64go 49 | 50 | - name: Build testing binaries 51 | run: | 52 | go test -c -tags n64,debug ./rcp/... ./drivers/... 53 | env: 54 | GOENV: go.env 55 | 56 | - name: Upload testing binaries 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: testing-ROM-${{ github.sha }} 60 | path: ./*.elf 61 | 62 | run: 63 | needs: build 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout repository 67 | uses: actions/checkout@v4 68 | 69 | - name: Checkout ares 70 | uses: actions/checkout@v4 71 | with: 72 | repository: ares-emulator/ares 73 | path: ares 74 | ref: ${{ env.ares-version }} 75 | 76 | - name: Cache ares build 77 | id: ares-build 78 | uses: actions/cache@v4 79 | with: 80 | path: ares 81 | key: ares-${{ env.ares-version }} 82 | 83 | - name: Build ares 84 | working-directory: ares 85 | run: | 86 | sudo apt update -y 87 | sudo apt install -y cmake g++ libgtk-3-dev xvfb mesa-vulkan-drivers 88 | cmake -B build \ 89 | -DARES_CORES=n64 \ 90 | -DARES_ENABLE_LIBRASHADER=OFF \ 91 | -DARES_ENABLE_OPENAL=OFF \ 92 | -DARES_ENABLE_SDL=OFF \ 93 | -DARES_ENABLE_OSS=OFF \ 94 | -DARES_ENABLE_ALSA=OFF \ 95 | -DARES_ENABLE_PULSEAUDIO=OFF \ 96 | -DARES_ENABLE_AO=OFF \ 97 | -DARES_ENABLE_UDEV=OFF 98 | cd build 99 | make -j4 100 | sudo make install 101 | 102 | - name: Set up Go 103 | uses: actions/setup-go@v5 104 | with: 105 | go-version: ${{ env.go-version }} 106 | cache: true 107 | 108 | - name: Cache embeddedgo build 109 | id: embeddedgo-build 110 | uses: actions/cache@v4 111 | with: 112 | path: ~/sdk 113 | key: ${{ env.embeddedgo-version }}-${{ steps.gotip-version.outputs.SHA }} 114 | 115 | - name: Install Embedded Go 116 | run: | 117 | go install -v github.com/embeddedgo/dl/${{ env.embeddedgo-version }}@latest 118 | ${{ env.embeddedgo-version }} version || ${{ env.embeddedgo-version }} download 119 | 120 | - name: Install n64go tool 121 | run: go install ./tools/n64go 122 | 123 | - name: Run testing ROM 124 | run: | 125 | export PATH="$GITHUB_WORKSPACE/.github/workflows/:$PATH" 126 | go test -p 1 -tags n64,debug ./drivers/... ./rcp/... 127 | env: 128 | GOENV: go.env 129 | GOFLAGS: > 130 | '-exec=n64go rom -run=ares_headless.sh' '-toolexec=n64go toolexec' '-tags=n64' '-trimpath' 131 | -------------------------------------------------------------------------------- /tools/toolexec/cartfs.go: -------------------------------------------------------------------------------- 1 | package toolexec 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "strings" 9 | ) 10 | 11 | type cartfsEmbed struct { 12 | Name string 13 | Patterns []string 14 | } 15 | 16 | // scanCartfsEmbed searches the package at path for global cartfs.FS variable 17 | // declarations initialized with cartfs.Embed. 18 | func scanCartfsEmbed(files []string, pkgname string) (decls []cartfsEmbed, err error) { 19 | importsCartfs := false 20 | fset := token.NewFileSet() 21 | pkgast := make(map[string]*ast.File) 22 | for _, file := range files { 23 | pkgast[file], err = parser.ParseFile(fset, file, nil, parser.ParseComments) 24 | if err != nil { 25 | return 26 | } 27 | for _, importSpec := range pkgast[file].Imports { 28 | if importSpec.Path.Value == `"github.com/clktmr/n64/drivers/cartfs"` { 29 | importsCartfs = true 30 | } 31 | } 32 | } 33 | 34 | if !importsCartfs { 35 | return 36 | } 37 | 38 | // Inspect all global variable declarations 39 | mappings := make(map[string]cartfsEmbed) 40 | for _, file := range pkgast { 41 | for _, decl := range file.Decls { 42 | if decl, ok := decl.(*ast.GenDecl); ok { 43 | if decl.Tok != token.VAR { 44 | continue 45 | } 46 | 47 | err = inspectVarDecl(decl, mappings) 48 | if err != nil { 49 | return nil, fmt.Errorf("%v: %v", file.Name.Name, err) 50 | } 51 | } 52 | } 53 | } 54 | 55 | decls = make([]cartfsEmbed, 0) 56 | for _, v := range mappings { 57 | decls = append(decls, cartfsEmbed{ 58 | Patterns: v.Patterns, 59 | Name: pkgname + "." + v.Name, 60 | }) 61 | } 62 | return 63 | } 64 | 65 | // inspectVarDecl searches decl for cartfs.FS initializations via cartfs.Embed() 66 | // and for embed.FS initializations via go:embed. The results are stored in 67 | // mapping, using the embed.FS variables name as key. 68 | // 69 | // FIXME package cartfs or embed might be imported under a different name 70 | func inspectVarDecl(decl *ast.GenDecl, mapping map[string]cartfsEmbed) error { 71 | var embedfsSpecs []*ast.ValueSpec 72 | for _, spec := range decl.Specs { 73 | if spec, ok := spec.(*ast.ValueSpec); ok { 74 | // search for initializations by cartfs.Embed() 75 | for i := range spec.Values { 76 | var initcall *ast.CallExpr 77 | var initfun *ast.SelectorExpr 78 | if initcall, ok = spec.Values[i].(*ast.CallExpr); !ok { 79 | continue 80 | } 81 | if initfun, ok = initcall.Fun.(*ast.SelectorExpr); !ok { 82 | continue 83 | } 84 | if initfun.Sel.Name != "Embed" { 85 | continue 86 | } 87 | if pkgident, ok := initfun.X.(*ast.Ident); ok { 88 | if pkgident.String() != "cartfs" { 89 | continue 90 | } 91 | if embedfsRef, ok := initcall.Args[0].(*ast.Ident); ok { 92 | m := mapping[embedfsRef.Name] 93 | if m.Name != "" { 94 | return fmt.Errorf("Multiple cartfs.FS embed the same embed.FS") 95 | } 96 | m.Name = spec.Names[i].Name 97 | mapping[embedfsRef.Name] = m 98 | } 99 | } 100 | } 101 | 102 | // search for embed.FS types 103 | if stype, ok := spec.Type.(*ast.SelectorExpr); ok { 104 | if stype.Sel.String() != "FS" { 105 | continue 106 | } 107 | if ident, ok := stype.X.(*ast.Ident); ok { 108 | if ident.String() != "embed" { 109 | continue 110 | } 111 | if spec.Doc == nil && decl.Lparen == 0 { 112 | spec.Doc = decl.Doc // TODO hackish 113 | } 114 | embedfsSpecs = append(embedfsSpecs, spec) 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Find go:embed patterns 121 | for _, spec := range embedfsSpecs { 122 | if len(spec.Names) != 1 { 123 | return fmt.Errorf("Multiple embed.FS per go:embed directive") 124 | } 125 | if spec.Doc == nil { 126 | continue 127 | } 128 | var patterns []string 129 | for _, doc := range spec.Doc.List { 130 | if args, found := strings.CutPrefix(doc.Text, "//go:embed"); found { 131 | var err error 132 | p, err := parseGoEmbed(args) 133 | if err != nil { 134 | return err 135 | } 136 | patterns = append(patterns, p...) 137 | m := mapping[spec.Names[0].Name] 138 | m.Patterns = patterns 139 | mapping[spec.Names[0].Name] = m 140 | } 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /rcp/cpu/cache_test.go: -------------------------------------------------------------------------------- 1 | package cpu_test 2 | 3 | import ( 4 | "runtime" 5 | "slices" 6 | "testing" 7 | "unsafe" 8 | 9 | "github.com/clktmr/n64/rcp/cpu" 10 | n64testing "github.com/clktmr/n64/testing" 11 | ) 12 | 13 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 14 | 15 | func assertPadded[T cpu.Paddable](t *testing.T, slice []T, length int, align uintptr) { 16 | addr := uintptr(unsafe.Pointer(unsafe.SliceData(slice))) 17 | if len(slice) != length { 18 | t.Fatalf("wrong length: expected %v, got %v", length, len(slice)) 19 | } 20 | if !cpu.IsPadded(slice) { 21 | t.Fatalf("got unpadded slice for len=%v: addr=0x%x, cap=%v", length, addr, cap(slice)) 22 | } 23 | if addr%align != 0 { 24 | t.Fatalf("got unaligned slice for len=%v, %v", length, cap(slice)) 25 | } 26 | } 27 | 28 | func testMakePaddedSlice[T cpu.Paddable](t *testing.T) { 29 | for i := range 64 { 30 | slice := cpu.MakePaddedSlice[T](i) 31 | assertPadded(t, slice, i, 1) 32 | } 33 | } 34 | 35 | func TestMakePaddedSlice(t *testing.T) { 36 | t.Run("byte", testMakePaddedSlice[uint8]) 37 | t.Run("uint16", testMakePaddedSlice[uint16]) 38 | t.Run("uint32", testMakePaddedSlice[uint32]) 39 | t.Run("uint64", testMakePaddedSlice[uint64]) 40 | } 41 | 42 | func testMakePaddedSliceAligned[T cpu.Paddable](t *testing.T) { 43 | for i := range 64 { 44 | for _, align := range []uintptr{2, 4, 8, 16, 32, 64, 128, 256} { 45 | slice := cpu.MakePaddedSliceAligned[T](i, align) 46 | assertPadded(t, slice, i, 1) 47 | } 48 | } 49 | } 50 | 51 | func TestMakePaddedSliceAligned(t *testing.T) { 52 | t.Run("byte", testMakePaddedSliceAligned[uint8]) 53 | t.Run("uint16", testMakePaddedSliceAligned[uint16]) 54 | t.Run("uint32", testMakePaddedSliceAligned[uint32]) 55 | t.Run("uint64", testMakePaddedSliceAligned[uint64]) 56 | } 57 | 58 | func assertPadAdded[T cpu.Paddable](t *testing.T, slice, pslice []T, head, tail int) { 59 | if cpu.IsPadded(pslice) == false { 60 | t.Errorf("got unpadded slice") 61 | } 62 | if len(pslice)+head+tail != len(slice) { 63 | t.Errorf("length don't match") 64 | } 65 | } 66 | 67 | func testPadSlice[T cpu.Paddable](t *testing.T) { 68 | var tt T 69 | cls := cpu.CacheLineSize / int(unsafe.Sizeof(tt)) 70 | for i := range 64 { 71 | slice := cpu.MakePaddedSlice[T](i) 72 | pslice, head, tail := cpu.PadSlice(slice) 73 | if len(slice) != len(pslice) || head != 0 || tail != 0 { 74 | t.Fatalf("%v: unnecessary padding added: before=%v, after=%v, head=%v, tail=%v", 75 | i, len(slice), len(pslice), head, tail) 76 | } 77 | 78 | if i < 2 { 79 | continue 80 | } 81 | 82 | tslice := slice[1:] 83 | pslice, head, tail = cpu.PadSlice(slice[1:]) 84 | assertPadAdded(t, tslice, pslice, head, tail) 85 | if len(pslice) > 0 && (head != cls-1 || tail != 0) { 86 | t.Fatalf("%v: wrong padding: head=%v, tail=%v", i, head, tail) 87 | } 88 | 89 | tslice = slice[:len(slice)-1] 90 | pslice, head, tail = cpu.PadSlice(slice[:len(slice)-1]) 91 | assertPadAdded(t, tslice, pslice, head, tail) 92 | if head != 0 || tail != 0 { 93 | t.Fatalf("wrong padding: head=%v, tail=%v", head, tail) 94 | } 95 | 96 | tslice = slice[:cap(slice)] 97 | pslice, head, tail = cpu.PadSlice(slice[:cap(slice)]) 98 | assertPadAdded(t, tslice, pslice, head, tail) 99 | if head != 0 || tail != cap(slice)%cls { 100 | t.Fatalf("%v: wrong padding: head=%v, tail=%v", i, head, tail) 101 | } 102 | } 103 | } 104 | 105 | func TestPadSlice(t *testing.T) { 106 | t.Run("byte", testPadSlice[uint8]) 107 | t.Run("uint16", testPadSlice[uint16]) 108 | t.Run("uint32", testPadSlice[uint32]) 109 | t.Run("uint64", testPadSlice[uint64]) 110 | } 111 | 112 | func TestUncached(t *testing.T) { 113 | bufCached := cpu.MakePaddedSlice[byte](32) 114 | bufUncached := cpu.UncachedSlice(bufCached) 115 | 116 | // Make sure bufUncached isn't collected 117 | runtime.GC() 118 | 119 | cpu.InvalidateSlice(bufCached) 120 | copy(bufUncached, []byte("uncached access")) 121 | 122 | if !slices.Equal(bufCached, bufUncached) { 123 | t.Fatal() 124 | } 125 | } 126 | 127 | func TestPadded(t *testing.T) { 128 | padded := cpu.NewPadded[int, cpu.Align64]() 129 | value := padded.Value() 130 | if cpu.PhysicalAddress(value)%64 != 0 { 131 | t.Fatalf("alignment: %p", value) 132 | } 133 | 134 | runtime.GC() 135 | runtime.KeepAlive(value) 136 | } 137 | -------------------------------------------------------------------------------- /tools/rom/uf2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Embedded Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rom 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "fmt" 11 | "io" 12 | "log" 13 | "os" 14 | "unsafe" 15 | ) 16 | 17 | const ( 18 | uf2NotMainFlash = 0x00000001 19 | uf2FileContainer = 0x00001000 20 | uf2FamilyIDPresent = 0x00002000 21 | uf2MD5ChecksumPresent = 0x00004000 22 | uf2ExtensionTagsPresent = 0x00008000 23 | ) 24 | 25 | // UF2 families 26 | const ( 27 | uf2_rp2040 = 0xe48bff56 28 | uf2_absolute = 0xe48bff57 29 | uf2_data = 0xe48bff58 30 | uf2_rp2350_arm_s = 0xe48bff59 31 | uf2_rp2350_riscv = 0xe48bff5a 32 | uf2_rp2350_arm_ns = 0xe48bff5b 33 | ) 34 | 35 | type uf2block struct { 36 | Magic0 uint32 37 | Magic1 uint32 38 | Flags uint32 39 | Addr uint32 40 | Len uint32 41 | Seq uint32 42 | Total uint32 43 | Family uint32 44 | Data [256]byte 45 | _ [476 - 256]byte 46 | Magic2 uint32 47 | } 48 | 49 | type uf2Writer struct { 50 | w io.Writer 51 | b uf2block 52 | } 53 | 54 | func newUF2Writer(w io.Writer, addr, flags, family uint32, size int) *uf2Writer { 55 | u := new(uf2Writer) 56 | u.w = w 57 | u.b.Magic0 = 0x0a324655 58 | u.b.Magic1 = 0x9e5d5157 59 | u.b.Flags = flags 60 | u.b.Addr = addr 61 | u.b.Total = uint32((size + len(u.b.Data) - 1) / len(u.b.Data)) 62 | u.b.Family = family 63 | u.b.Magic2 = 0x0ab16f30 64 | return u 65 | } 66 | 67 | func (u *uf2Writer) WriteString(s string) (n int, err error) { 68 | b := &u.b 69 | for len(s) != 0 { 70 | m := copy(b.Data[b.Len:], s) 71 | n += m 72 | s = s[m:] 73 | b.Len += uint32(m) 74 | if int(b.Len) == len(b.Data) { 75 | err = binary.Write(u.w, binary.LittleEndian, b) 76 | if err != nil { 77 | return 78 | } 79 | b.Addr += b.Len 80 | b.Seq++ 81 | b.Len = 0 82 | } 83 | } 84 | return 85 | } 86 | 87 | func (u *uf2Writer) Write(p []byte) (n int, err error) { 88 | return u.WriteString(*(*string)(unsafe.Pointer(&p))) 89 | } 90 | 91 | func (u *uf2Writer) Flush() (err error) { 92 | b := &u.b 93 | if b.Len == 0 { 94 | return 95 | } 96 | clear(b.Data[b.Len:]) 97 | b.Len = uint32(len(b.Data)) 98 | err = binary.Write(u.w, binary.LittleEndian, b) 99 | b.Addr += b.Len 100 | b.Seq++ 101 | b.Len = 0 102 | return 103 | } 104 | 105 | // n64WriteUF2 is a translation to Go of the generateAndSaveUF2 function from 106 | // https://kbeckmann.github.io/PicoCart64/js/PicoCart64.js 107 | // Original author: Konrad Beckmann. 108 | func n64WriteUF2(obj string, rom []byte) error { 109 | const ( 110 | chunkSize = 1024 111 | header = "picocartcompress" 112 | _1M = 1024 * 1024 113 | ) 114 | 115 | // Split ROM into chunks 116 | 117 | var ( 118 | chunkData []byte 119 | chunkMap [(0x8000 - len(header)) / 2]uint16 120 | chunkMapLen int 121 | ) 122 | 123 | for i := 0; i < len(rom); i += chunkSize { 124 | k := min(len(rom), i+chunkSize) 125 | chunk := rom[i:k] 126 | 127 | // Check if chunk is in chunkData 128 | for k = 0; k < len(chunkData); k += chunkSize { 129 | if bytes.HasPrefix(chunkData[k:], chunk) { 130 | break 131 | } 132 | } 133 | if k == len(chunkData) { 134 | // Found a unique chunk 135 | chunkData = append(chunkData, chunk...) 136 | } 137 | if chunkMapLen >= len(chunkMap) { 138 | return fmt.Errorf("n64 uf2: chunk map overflow") 139 | } 140 | k /= chunkSize // chunk number in chunkData 141 | chunkMap[chunkMapLen] = uint16(k) 142 | chunkMapLen++ 143 | } 144 | 145 | newSize := len(header) + len(chunkMap)*2 + len(chunkData) 146 | flashStart := 0x10000000 147 | lastAddr := 0x10030000 + newSize 148 | flashEnd := flashStart + 2*_1M 149 | if lastAddr > flashEnd { 150 | log.Printf( 151 | "n64 uf2: the compressed ROM requires %d MiB of Flash (> 2 MiB)\n", 152 | (lastAddr-flashStart+_1M-1)/_1M, 153 | ) 154 | } 155 | 156 | // Save compressed ROM 157 | f, err := os.Create(obj) 158 | if err != nil { 159 | return err 160 | } 161 | defer f.Close() 162 | 163 | w := newUF2Writer(f, 0x10030000, uf2FamilyIDPresent, uf2_rp2040, newSize) 164 | _, err = w.WriteString(header) 165 | if err != nil { 166 | return err 167 | } 168 | err = binary.Write(w, binary.LittleEndian, chunkMap) 169 | if err != nil { 170 | return err 171 | } 172 | _, err = w.Write(chunkData) 173 | if err != nil { 174 | return err 175 | } 176 | err = w.Flush() 177 | if err != nil { 178 | return err 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /tools/pakfs/fuse.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin 2 | 3 | package pakfs 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "io/fs" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "syscall" 13 | 14 | "github.com/clktmr/n64/drivers/controller/pakfs" 15 | "rsc.io/rsc/fuse" 16 | ) 17 | 18 | func mount(image, dir string) error { 19 | c, err := fuse.Mount(dir) 20 | if err != nil { 21 | return err 22 | } 23 | r, err := os.OpenFile(image, os.O_RDWR, 0) 24 | if err != nil { 25 | return err 26 | } 27 | fs, err := pakfs.Read(r) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | go c.Serve(&fusefs{fs}) 33 | <-sigintr 34 | 35 | cmd := exec.Command("/bin/umount", dir) 36 | _, err = cmd.CombinedOutput() 37 | return err 38 | } 39 | 40 | // fusefs implements the file system and the root dir Node. 41 | type fusefs struct { 42 | pakfs *pakfs.FS 43 | } 44 | 45 | func (p *fusefs) Root() (fuse.Node, fuse.Error) { 46 | return p, nil 47 | } 48 | 49 | func (p *fusefs) Attr() fuse.Attr { 50 | dir := p.pakfs.Root() 51 | stat, err := dir.Stat() 52 | if err != nil { 53 | log.Println("stat:", err) 54 | return fuse.Attr{} 55 | } 56 | return fuse.Attr{ 57 | Mode: stat.Mode(), 58 | Mtime: stat.ModTime(), 59 | } 60 | } 61 | 62 | func (p *fusefs) Lookup(name string, intr fuse.Intr) (fuse.Node, fuse.Error) { 63 | f, err := p.pakfs.Open(name) 64 | if err != nil { 65 | return nil, errno(err) 66 | } 67 | pakfile, ok := f.(*pakfs.File) 68 | if !ok { 69 | return p, nil // must be root dir 70 | } 71 | return &fusefile{pakfile, p.pakfs}, nil 72 | } 73 | 74 | func (p *fusefs) ReadDir(intr fuse.Intr) ([]fuse.Dirent, fuse.Error) { 75 | entries := p.pakfs.ReadDirRoot() 76 | fuseEntries := make([]fuse.Dirent, len(entries)) 77 | for i, v := range entries { 78 | fuseEntries[i] = fuse.Dirent{ 79 | Name: v.Name(), 80 | } 81 | } 82 | 83 | return fuseEntries, nil 84 | } 85 | 86 | func (p *fusefs) Create(req *fuse.CreateRequest, res *fuse.CreateResponse, intr fuse.Intr) (fuse.Node, fuse.Handle, fuse.Error) { 87 | f, err := p.pakfs.Create(req.Name) 88 | if err != nil { 89 | return nil, nil, errno(err) 90 | } 91 | 92 | file := &fusefile{f, p.pakfs} 93 | return file, file, nil 94 | } 95 | 96 | func (p *fusefs) Remove(req *fuse.RemoveRequest, intr fuse.Intr) fuse.Error { 97 | err := p.pakfs.Remove(req.Name) 98 | if err != nil { 99 | return errno(err) 100 | } 101 | return nil 102 | } 103 | 104 | func (p *fusefs) Rename(req *fuse.RenameRequest, newDir fuse.Node, intr fuse.Intr) fuse.Error { 105 | err := p.pakfs.Rename(req.OldName, req.NewName) 106 | if err != nil { 107 | return errno(err) 108 | } 109 | return nil 110 | } 111 | 112 | // fusefile implements both Node and Handle. 113 | type fusefile struct { 114 | *pakfs.File 115 | 116 | pakfs *pakfs.FS 117 | } 118 | 119 | func (p *fusefile) Attr() fuse.Attr { 120 | return fuse.Attr{ 121 | Mode: p.Mode(), 122 | Mtime: p.ModTime(), 123 | Size: uint64(p.Size()), 124 | } 125 | } 126 | 127 | func (p *fusefile) ReadAll(intr fuse.Intr) ([]byte, fuse.Error) { 128 | b := make([]byte, p.Size()) 129 | _, err := p.ReadAt(b, 0) 130 | if err != io.EOF && err != nil { 131 | return nil, errno(err) 132 | } 133 | return b, nil 134 | } 135 | 136 | // Only WriteAll is supported. Write is not implemented on purpose because it 137 | // might cause unexpected behaviour when appending to a file, since filesize is 138 | // always rounded up to the next page boundary. 139 | func (p *fusefile) WriteAll(data []byte, intr fuse.Intr) fuse.Error { 140 | err := p.pakfs.Truncate(p.File.Name(), int64(len(data))) 141 | if err != nil { 142 | return errno(err) 143 | } 144 | 145 | _, err = p.WriteAt(data, 0) 146 | if err != nil { 147 | return errno(err) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (p *fusefile) Fsync(req *fuse.FsyncRequest, intr fuse.Intr) fuse.Error { 154 | return nil 155 | } 156 | 157 | func errno(err error) fuse.Error { 158 | if errors.Is(err, pakfs.ErrNoSpace) { 159 | return fuse.Errno(syscall.ENOSPC) 160 | } else if errors.Is(err, pakfs.ErrReadOnly) { 161 | return fuse.Errno(syscall.EROFS) 162 | } else if errors.Is(err, pakfs.ErrIsDir) { 163 | return fuse.Errno(syscall.EISDIR) 164 | } else if errors.Is(err, pakfs.ErrNameTooLong) { 165 | return fuse.Errno(syscall.ENAMETOOLONG) 166 | } else if errors.Is(err, fs.ErrInvalid) { 167 | return fuse.Errno(syscall.EINVAL) 168 | } else if errors.Is(err, fs.ErrExist) { 169 | return fuse.Errno(syscall.EEXIST) 170 | } else if errors.Is(err, fs.ErrNotExist) { 171 | return fuse.Errno(syscall.ENOENT) 172 | } else { 173 | return fuse.EIO 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /drivers/rspq/mixer/mixer_test.go: -------------------------------------------------------------------------------- 1 | package mixer_test 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/binary" 7 | "io" 8 | "math" 9 | "slices" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/clktmr/n64/drivers/cartfs" 14 | "github.com/clktmr/n64/drivers/controller" 15 | "github.com/clktmr/n64/drivers/rspq" 16 | "github.com/clktmr/n64/drivers/rspq/mixer" 17 | "github.com/clktmr/n64/rcp/audio" 18 | "github.com/clktmr/n64/rcp/cpu" 19 | "github.com/clktmr/n64/rcp/serial/joybus" 20 | n64testing "github.com/clktmr/n64/testing" 21 | ) 22 | 23 | var ( 24 | //go:embed testdata/sfx_alarm_loop3.pcm_s16be 25 | //go:embed testdata/sfx_wpn_cannon2.pcm_s16be 26 | //go:embed testdata/sfx_wpn_machinegun_loop1.pcm_s16be 27 | _testdata embed.FS 28 | testdata cartfs.FS = cartfs.Embed(_testdata) 29 | ) 30 | 31 | func TestMain(m *testing.M) { n64testing.TestMain(m) } 32 | 33 | func TestResampling(t *testing.T) { 34 | if testing.Short() { 35 | t.Skip("skipping in short mode") 36 | } 37 | 38 | rspq.Reset() 39 | mixer.Init() 40 | 41 | audio.Start(48000) 42 | mixer.SetSampleRate(48000) 43 | sfxAlarm, _ := testdata.Open("testdata/sfx_alarm_loop3.pcm_s16be") 44 | sfxCannon, _ := testdata.Open("testdata/sfx_wpn_cannon2.pcm_s16be") 45 | sfxMachinegun, _ := testdata.Open("testdata/sfx_wpn_machinegun_loop1.pcm_s16be") 46 | sourceAlarm := mixer.NewSource(mixer.Loop(sfxAlarm.(io.ReadSeeker)), 16000) 47 | sourceCannon := mixer.NewSource(mixer.Loop(sfxCannon.(io.ReadSeeker)), 44100) 48 | sourceMachinegun := mixer.NewSource(mixer.Loop(sfxMachinegun.(io.ReadSeeker)), 8000) 49 | 50 | wg := sync.WaitGroup{} 51 | wg.Add(1) 52 | go func() { audio.Buffer.ReadFrom(mixer.Output); wg.Done() }() 53 | 54 | t.Log("Hold C buttons to enable audio sources:") 55 | t.Log(joybus.ButtonCLeft, "= alarm,", joybus.ButtonCDown, "= explosion,", joybus.ButtonCRight, "= machinegun") 56 | t.Log("Press any other button otherwise.") 57 | var states [4]controller.Controller 58 | for { 59 | controller.Poll(&states) 60 | switch { 61 | case states[0].Pressed()&joybus.ButtonCLeft != 0: 62 | mixer.SetSource(0, sourceAlarm) 63 | case states[0].Released()&joybus.ButtonCLeft != 0: 64 | mixer.SetSource(0, nil) 65 | case states[0].Pressed()&joybus.ButtonCDown != 0: 66 | mixer.SetSource(1, sourceCannon) 67 | case states[0].Released()&joybus.ButtonCDown != 0: 68 | mixer.SetSource(1, nil) 69 | case states[0].Pressed()&joybus.ButtonCRight != 0: 70 | mixer.SetSource(2, sourceMachinegun) 71 | case states[0].Released()&joybus.ButtonCRight != 0: 72 | mixer.SetSource(2, nil) 73 | case states[0].Pressed()&joybus.ButtonReset != 0: 74 | goto end 75 | } 76 | s := sourceMachinegun 77 | s.SetVolume(1.0, (float32(states[0].X())/85.0/2.0)+0.5) 78 | pitch := 8000 * ((float32(states[0].Y()) / 85.0) + 1.0) 79 | s.SetSampleRate(uint(min(max(0, pitch), 128000))) 80 | } 81 | end: 82 | audio.Stop() 83 | wg.Wait() 84 | } 85 | 86 | func TestMixing(t *testing.T) { 87 | rspq.Reset() 88 | mixer.Init() 89 | 90 | var sinus, cosinus [32]int16 91 | var expected, result [32]int16 92 | for i := range 32 { 93 | sinus[i] = int16(math.Sin(2*math.Pi*float64(i)/16) * float64(math.MaxInt16/2)) 94 | cosinus[i] = int16(math.Cos(2*math.Pi*float64(i)/16) * float64(math.MaxInt16/2)) 95 | } 96 | for i := range 16 { 97 | expected[i<<1] = (sinus[i] + cosinus[i]) >> 1 // left channel 98 | expected[i<<1+1] = (sinus[i] + cosinus[i]) >> 1 // right channel 99 | } 100 | 101 | sinusBuf := cpu.MakePaddedSliceAligned[byte](64, 16) 102 | _, err := binary.Encode(sinusBuf, binary.BigEndian, sinus) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | cosinusBuf := cpu.MakePaddedSliceAligned[byte](64, 16) 107 | _, err = binary.Encode(cosinusBuf, binary.BigEndian, cosinus) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | cpu.WritebackSlice(sinusBuf) 113 | cpu.WritebackSlice(cosinusBuf) 114 | 115 | mixer.SetSampleRate(8000) 116 | 117 | mixer.SetSource(0, mixer.NewSource(mixer.Loop(bytes.NewReader(sinusBuf)), 8000)) 118 | mixer.SetSource(1, mixer.NewSource(mixer.Loop(bytes.NewReader(cosinusBuf)), 8000)) 119 | 120 | resultBuf := cpu.MakePaddedSliceAligned[byte](8192, 16) 121 | _, err = io.ReadFull(mixer.Output, resultBuf) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | resultBuf = resultBuf[len(resultBuf)-64:] // read from end to minimize one-tap filter effect 127 | _, err = binary.Decode(resultBuf, binary.BigEndian, &result) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | 132 | if !slices.EqualFunc(result[:], expected[:], func(a, b int16) bool { 133 | // allow some error, not sure what causes them 134 | diff := int(a) - int(b) 135 | return max(diff, -diff) <= 8 136 | }) { 137 | t.Error("expected", expected) 138 | t.Error("got", result) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /drivers/controller/pakfs/charmap.go: -------------------------------------------------------------------------------- 1 | package pakfs 2 | 3 | import ( 4 | "errors" 5 | "unicode/utf8" 6 | 7 | "golang.org/x/text/encoding" 8 | "golang.org/x/text/transform" 9 | ) 10 | 11 | const rcd = '�' // decoding replacement character 12 | 13 | var decode = [...]rune{ 14 | '\u0000', rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, rcd, ' ', 15 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 16 | 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 17 | 'W', 'X', 'Y', 'Z', '!', '"', '#', '\'', '*', '+', ',', '-', '.', '/', ':', '=', 18 | '?', '@', '。', '゛', '゜', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ッ', 'ャ', 'ュ', 'ョ', 'ヲ', 'ン', 19 | 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 20 | 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 21 | 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ガ', 'ギ', 'グ', 'ゲ', 22 | 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 23 | 'パ', 'ピ', 'プ', 'ペ', 'ポ', 24 | } 25 | 26 | const rce = 64 // encoding replacement character 27 | 28 | var encode = map[rune]byte{ 29 | '\u0000': 0, ' ': 15, 30 | '0': 16, '1': 17, '2': 18, '3': 19, '4': 20, '5': 21, '6': 22, '7': 23, '8': 24, '9': 25, 'A': 26, 'B': 27, 'C': 28, 'D': 29, 'E': 30, 'F': 31, 31 | 'G': 32, 'H': 33, 'I': 34, 'J': 35, 'K': 36, 'L': 37, 'M': 38, 'N': 39, 'O': 40, 'P': 41, 'Q': 42, 'R': 43, 'S': 44, 'T': 45, 'U': 46, 'V': 47, 32 | 'W': 48, 'X': 49, 'Y': 50, 'Z': 51, '!': 52, '"': 53, '#': 54, '\'': 55, '*': 56, '+': 57, ',': 58, '-': 59, '.': 60, '/': 61, ':': 62, '=': 63, 33 | '?': 64, '@': 65, '。': 66, '゛': 67, '゜': 68, 'ァ': 69, 'ィ': 70, 'ゥ': 71, 'ェ': 72, 'ォ': 73, 'ッ': 74, 'ャ': 75, 'ュ': 76, 'ョ': 77, 'ヲ': 78, 'ン': 79, 34 | 'ア': 80, 'イ': 81, 'ウ': 82, 'エ': 83, 'オ': 84, 'カ': 85, 'キ': 86, 'ク': 87, 'ケ': 88, 'コ': 89, 'サ': 90, 'シ': 91, 'ス': 92, 'セ': 93, 'ソ': 94, 'タ': 95, 35 | 'チ': 96, 'ツ': 97, 'テ': 98, 'ト': 99, 'ナ': 100, 'ニ': 101, 'ヌ': 102, 'ネ': 103, 'ノ': 104, 'ハ': 105, 'ヒ': 106, 'フ': 107, 'ヘ': 108, 'ホ': 109, 'マ': 110, 'ミ': 111, 36 | 'ム': 112, 'メ': 113, 'モ': 114, 'ヤ': 115, 'ユ': 116, 'ヨ': 117, 'ラ': 118, 'リ': 119, 'ル': 120, 'レ': 121, 'ロ': 122, 'ワ': 123, 'ガ': 124, 'ギ': 125, 'グ': 126, 'ゲ': 127, 37 | 'ゴ': 128, 'ザ': 129, 'ジ': 130, 'ズ': 131, 'ゼ': 132, 'ゾ': 133, 'ダ': 134, 'ヂ': 135, 'ヅ': 136, 'デ': 137, 'ド': 138, 'バ': 139, 'ビ': 140, 'ブ': 141, 'ベ': 142, 'ボ': 143, 38 | 'パ': 144, 'ピ': 145, 'プ': 146, 'ペ': 147, 'ポ': 148, 39 | } 40 | 41 | // ErrEncoding is returned by [N64FontCodeStrict] 42 | var ErrEncoding = errors.New("unsupported character") 43 | 44 | type charmap struct { 45 | strict bool // Don't allow replacement characters 46 | } 47 | 48 | // Character encodings used for files on the Controller Pak. 49 | var ( 50 | // Uses replacement characters for unsupported characters. 51 | N64FontCode encoding.Encoding = &charmap{false} 52 | 53 | // Returns an error on unsupported characters. 54 | N64FontCodeStrict encoding.Encoding = &charmap{true} 55 | ) 56 | 57 | func (m *charmap) NewDecoder() *encoding.Decoder { 58 | return &encoding.Decoder{Transformer: &decoder{m.strict}} 59 | } 60 | 61 | func (m *charmap) NewEncoder() *encoding.Encoder { 62 | return &encoding.Encoder{Transformer: &encoder{m.strict}} 63 | } 64 | 65 | type decoder struct { 66 | strict bool // Return error if stream contains illegal character 67 | } 68 | 69 | // N64FontCode to UTF-8 70 | func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 71 | for _, c := range src { 72 | nSrc += 1 73 | var r rune = rcd 74 | if c < byte(len(decode)) { 75 | r = decode[c] 76 | } 77 | if r == rcd && d.strict { 78 | err = ErrEncoding 79 | return 80 | } 81 | rlen := utf8.RuneLen(r) // r is always valid 82 | if rlen > len(dst)-nDst { 83 | err = transform.ErrShortDst 84 | break 85 | } 86 | 87 | nDst += utf8.EncodeRune(dst[nDst:], decode[c]) 88 | } 89 | return 90 | } 91 | 92 | func (d *decoder) Reset() {} 93 | 94 | type encoder struct { 95 | strict bool // Return error if character can't be represented 96 | } 97 | 98 | // UTF-8 to N64FontCode 99 | func (d *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { 100 | for { 101 | r, size := utf8.DecodeRune(src[nSrc:]) 102 | if size < 1 { 103 | break 104 | } else if size == 1 && r == utf8.RuneError { 105 | err = encoding.ErrInvalidUTF8 106 | if !atEOF && !utf8.FullRune(src[nSrc:]) { 107 | err = transform.ErrShortSrc 108 | } 109 | break 110 | } 111 | if nDst >= len(dst) { 112 | err = transform.ErrShortDst 113 | break 114 | } 115 | nSrc += size 116 | 117 | if c, ok := encode[r]; ok { 118 | dst[nDst] = c 119 | } else if !d.strict { 120 | dst[nDst] = rce 121 | } else { 122 | err = ErrEncoding 123 | break 124 | } 125 | nDst += 1 126 | } 127 | return 128 | } 129 | 130 | func (d *encoder) Reset() {} 131 | -------------------------------------------------------------------------------- /drivers/rspq/vec_test.go: -------------------------------------------------------------------------------- 1 | package rspq_test 2 | 3 | import ( 4 | "embed" 5 | "math" 6 | "slices" 7 | "structs" 8 | "testing" 9 | "unsafe" 10 | 11 | "github.com/clktmr/n64/debug" 12 | "github.com/clktmr/n64/drivers/cartfs" 13 | "github.com/clktmr/n64/drivers/rspq" 14 | "github.com/clktmr/n64/rcp/cpu" 15 | "github.com/clktmr/n64/rcp/rsp" 16 | "github.com/clktmr/n64/rcp/rsp/ucode" 17 | ) 18 | 19 | var ( 20 | // rsp_vec microcode from libdragon's examples 21 | // Version: 3feaaadf0 (RSPQ_DEBUG enabled) 22 | // 23 | //go:embed testdata/rsp_vec.ucode 24 | _rspVecFiles embed.FS 25 | rspVecFiles cartfs.FS = cartfs.Embed(_rspVecFiles) 26 | rspVecId uint32 27 | ) 28 | 29 | const ( 30 | cmdLoad rspq.Command = 0x0 31 | cmdStore rspq.Command = 0x1 32 | cmdTrans rspq.Command = 0x2 33 | ) 34 | 35 | type vec4 [4]float32 36 | type mat4 [4]vec4 37 | 38 | func (a vec4) Mul(b vec4) float32 { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] } 39 | func (m mat4) Row(i int) vec4 { return vec4{m[0][i], m[1][i], m[2][i], m[3][i]} } 40 | func (m mat4) Mul(b vec4) vec4 { 41 | return vec4{m.Row(0).Mul(b), m.Row(1).Mul(b), m.Row(2).Mul(b), m.Row(3).Mul(b)} 42 | } 43 | 44 | // vecSlot is the fixed point vector format required by the ucode. One vecSlot 45 | // store two vec4. 46 | type vecSlot struct { 47 | _ structs.HostLayout 48 | i [8]int16 49 | f [8]uint16 50 | } 51 | 52 | type matSlot [2]vecSlot 53 | 54 | func (p *matSlot) Set(m *mat4) { 55 | pvec := (*[2]vecSlot)(p) 56 | for i := range 2 { 57 | pvec[i].Set((*[2]vec4)(m[i<<1:])) 58 | } 59 | } 60 | 61 | func (p *vecSlot) Set(vecs *[2]vec4) { 62 | for i := range 8 { 63 | fixed := uint32(vecs[i>>2][i&0x3] * (1 << 16)) 64 | p.i[i] = int16(fixed & 0xffff_0000 >> 16) 65 | p.f[i] = uint16(fixed & 0x0000_ffff) 66 | } 67 | } 68 | 69 | func (p *vecSlot) Get() (vecs [2]vec4) { 70 | for i := range 8 { 71 | fixed := int32(p.i[i])<<16 | int32(p.f[i]) 72 | vecs[i>>2][i&0x3] = float32(fixed) / (1 << 16) 73 | } 74 | return 75 | } 76 | 77 | func vecLoad(slot int, src []vecSlot) { 78 | debug.Assert(cpu.PhysicalAddressSlice(src)&0xf == 0, "vecSlot alignment") 79 | const size = int(unsafe.Sizeof(vecSlot{})) 80 | rspq.Write(cmdLoad|rspq.Command(rspVecId>>24), 81 | uint32(cpu.PhysicalAddressSlice(src)), 82 | uint32(((((len(src)*size)-1)&0xFFF)<<16)|((slot*size)&0xFF0))) 83 | } 84 | 85 | func vecStore(slot int, dst []vecSlot) { 86 | debug.Assert(cpu.PhysicalAddressSlice(dst)&0xf == 0, "vecSlot alignment") 87 | const size = int(unsafe.Sizeof(vecSlot{})) 88 | rspq.Write(cmdStore|rspq.Command(rspVecId>>24), 89 | uint32(cpu.PhysicalAddressSlice(dst)), 90 | uint32(((((len(dst)*size)-1)&0xFFF)<<16)|((slot*size)&0xFF0))) 91 | } 92 | 93 | func vecTransform(dst, matrix, vec int) { 94 | const size = int(unsafe.Sizeof(vecSlot{})) 95 | rspq.Write(cmdTrans|rspq.Command(rspVecId>>24), 96 | uint32((dst*size)&0xff0), 97 | uint32(((matrix*size)&0xff0)<<16|((vec*size)&0xff0))) 98 | } 99 | 100 | var vecs = [8]vec4{ 101 | {0.1, -0.2, 0.3, -0.4}, {0.5, -0.6, -0.7, 0.8}, 102 | {1.1, -1.2, 1.3, -1.4}, {1.5, -1.6, -1.7, 1.8}, 103 | {2.1, -2.2, 2.3, -2.4}, {2.5, -2.6, -2.7, 2.8}, 104 | {3.1, -3.2, 3.3, -3.4}, {3.5, -3.6, -3.7, 3.8}, 105 | } 106 | var mat = mat4{ 107 | {1.0, 0.0, -0.0, 0.0}, 108 | {0.0, 1.0, -0.0, 0.0}, 109 | {0.0, 0.0, -1.0, 0.0}, 110 | {0.0, 9.0, -0.0, 1.0}, 111 | } 112 | 113 | func TestVecUCode(t *testing.T) { 114 | rspq.Reset() 115 | 116 | r, err := rspVecFiles.Open("testdata/rsp_vec.ucode") 117 | if err != nil { 118 | panic(err) 119 | } 120 | uc, err := ucode.Load(r) 121 | if err != nil { 122 | panic(err) 123 | } 124 | rspVecId = rspq.Register(uc) 125 | 126 | srcPad := cpu.NewPadded[[4]vecSlot, cpu.Align16]() 127 | src := srcPad.Value() 128 | src[0].Set((*[2]vec4)(vecs[0:])) 129 | src[1].Set((*[2]vec4)(vecs[2:])) 130 | src[2].Set((*[2]vec4)(vecs[4:])) 131 | src[3].Set((*[2]vec4)(vecs[6:])) 132 | srcPad.Writeback() 133 | vecLoad(0, src[:]) 134 | 135 | matrix := cpu.NewPadded[matSlot, cpu.Align16]() 136 | matrix.Value().Set(&mat) 137 | matrix.Writeback() 138 | value := matrix.Value() 139 | vecLoad(30, value[:]) 140 | 141 | vecTransform(4, 30, 0) 142 | vecTransform(5, 30, 1) 143 | vecTransform(6, 30, 2) 144 | vecTransform(7, 30, 3) 145 | 146 | dstPad := cpu.NewPadded[[4]vecSlot, cpu.Align16]() 147 | dst := dstPad.Value() 148 | dstPad.Invalidate() 149 | vecStore(4, dst[:]) 150 | 151 | for !rsp.Stopped() { 152 | // wait 153 | } 154 | if rspq.Crashed() { 155 | t.Fatal("rspq crashed") 156 | } 157 | 158 | almostEqual := func(a, b float32) bool { return math.Abs(float64(a-b)) < 1e-3 } 159 | 160 | for i, vec := range vecs { 161 | vecCPU := mat.Mul(vec) 162 | vecRSP := dst[i>>1].Get()[i&0x1] 163 | if !slices.EqualFunc(vecCPU[:], vecRSP[:], almostEqual) { 164 | t.Logf("vec%d: %v != %v", i, vecCPU, vecRSP) 165 | t.Fatal("incorrect transform result") 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tools/rom/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Embedded Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rom 6 | 7 | import ( 8 | "bufio" 9 | "context" 10 | "debug/elf" 11 | "errors" 12 | "flag" 13 | "fmt" 14 | "io" 15 | "log" 16 | "os" 17 | "os/exec" 18 | "strings" 19 | "sync" 20 | "time" 21 | 22 | _ "embed" 23 | 24 | "github.com/aymanbagabas/go-pty" 25 | "github.com/buildkite/shellwords" 26 | ) 27 | 28 | const usageString = `ELF to n64 ROM converter. 29 | 30 | Usage: %s [flags] 31 | 32 | ` 33 | 34 | var ( 35 | flags = flag.NewFlagSet("rom", flag.ExitOnError) 36 | 37 | infile string 38 | format = flags.String("format", "z64", "output format, z64 or uf2") 39 | run = optString{s: "ares"} 40 | ) 41 | 42 | type optString struct { 43 | set bool 44 | s string 45 | } 46 | 47 | func (p *optString) IsBoolFlag() bool { return true } 48 | func (p *optString) IsSet() bool { return p.set } 49 | func (p *optString) String() string { return p.s } 50 | func (p *optString) Set(s string) error { 51 | p.set = true 52 | if s == "true" { 53 | return nil 54 | } 55 | p.s = s 56 | return nil 57 | } 58 | 59 | func usage() { 60 | fmt.Fprintf(flags.Output(), usageString, "rom") 61 | flags.PrintDefaults() 62 | } 63 | 64 | func objcopy(dst io.WriterAt, src *elf.File) error { 65 | for _, s := range src.Sections { 66 | if s.Type != elf.SHT_PROGBITS || s.Flags&elf.SHF_ALLOC == 0 { 67 | continue 68 | } 69 | data, err := s.Data() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if s.Addr < src.Entry { 75 | return errors.New("data before entry point") 76 | } 77 | 78 | _, err = dst.WriteAt(data, int64(s.Addr-src.Entry)) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // libdragon IPL3 r8 (compatibility mode) 88 | // Author: Giovanni Bajo (giovannibajo@gmail.com) 89 | // 90 | //go:embed ipl3_compat.z64 91 | var n64IPL3 []byte 92 | 93 | func n64WriteROMHeader(rom *os.File, gametitle string) error { 94 | copy(n64IPL3[0x20:0x34], fmt.Sprintf("%-20s", gametitle)) // TODO encode in ascii 95 | _, err := rom.WriteAt(n64IPL3, 0) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func Main(args []string) { 104 | flags.Var(&run, "run", "Run the ROM with command") 105 | flags.Usage = usage 106 | flags.Parse(args[1:]) 107 | 108 | switch flags.NArg() { 109 | default: 110 | if !run.IsSet() { 111 | log.Fatalln("too many arguments") 112 | } 113 | log.Println("WARN: Discarding additional args:", flags.Args()[1:]) 114 | fallthrough 115 | case 1: 116 | infile = flags.Arg(0) 117 | case 0: 118 | log.Fatalln("missing elffile arg") 119 | } 120 | 121 | outfile, _ := strings.CutSuffix(infile, ".elf") 122 | outfile += "." + *format 123 | 124 | elffile, err := elf.Open(infile) 125 | if err != nil { 126 | log.Fatalln(err) 127 | } 128 | defer elffile.Close() 129 | 130 | rom, err := os.CreateTemp("", "rom") 131 | if err != nil { 132 | log.Fatalln(err) 133 | } 134 | defer rom.Close() 135 | 136 | err = objcopy(io.NewOffsetWriter(rom, int64(len(n64IPL3))), elffile) 137 | if err != nil { 138 | log.Fatalln("objcopy:", err) 139 | } 140 | 141 | err = n64WriteROMHeader(rom, outfile) 142 | if err != nil { 143 | log.Fatalln("write rom header:", err) 144 | } 145 | 146 | switch *format { 147 | case "z64": 148 | out, err := os.Create(outfile) 149 | if err != nil { 150 | log.Fatalln(err) 151 | } 152 | defer out.Close() 153 | rom.Seek(0, io.SeekStart) 154 | _, err = io.Copy(out, rom) 155 | if err != nil { 156 | log.Fatalln(err) 157 | } 158 | case "uf2": 159 | // TODO pass file to n64WriteUF2 160 | rom, err := io.ReadAll(rom) 161 | if err != nil { 162 | log.Fatalln(err) 163 | } 164 | err = n64WriteUF2(outfile, rom) 165 | if err != nil { 166 | log.Fatalln(err) 167 | } 168 | default: 169 | log.Fatalf("objcopy: %s format not supported", *format) 170 | } 171 | 172 | if run.IsSet() { 173 | runROM(run.String(), outfile) 174 | } 175 | } 176 | 177 | func runROM(cmdpath, rompath string) { 178 | args, err := shellwords.Split(cmdpath) 179 | if err != nil { 180 | log.Fatal("run:", err) 181 | } 182 | args = append(args, rompath) 183 | 184 | pty, err := pty.New() 185 | if err != nil { 186 | log.Fatal("create pty:", err) 187 | } 188 | 189 | ctx, cancel := context.WithCancel(context.Background()) 190 | cmd := pty.CommandContext(ctx, args[0], args[1:]...) 191 | err = cmd.Start() 192 | if err != nil { 193 | log.Fatal("start command:", err) 194 | } 195 | 196 | code := 0 197 | 198 | // copy stdin 199 | go func() { io.Copy(pty, os.Stdin) }() 200 | 201 | // copy and parse stdout 202 | wg := sync.WaitGroup{} 203 | wg.Add(1) 204 | go func() { 205 | defer wg.Done() 206 | scanner := bufio.NewScanner(io.TeeReader(pty, os.Stdout)) 207 | for scanner.Scan() { 208 | line := scanner.Text() 209 | switch { 210 | case strings.HasPrefix(line, "fatal error:"), strings.HasPrefix(line, "panic:"): 211 | fallthrough 212 | case line == "FAIL": 213 | code = 1 214 | fallthrough 215 | case line == "PASS": 216 | // give panic() time to print the stacktrace 217 | time.AfterFunc(500*time.Millisecond, cancel) 218 | } 219 | } 220 | }() 221 | 222 | err = cmd.Wait() 223 | pty.Close() 224 | wg.Wait() 225 | if ctx.Err() == context.Canceled { // ignore error if we killed cmd ourself 226 | os.Exit(code) 227 | } 228 | if err, ok := err.(*exec.ExitError); ok { 229 | os.Exit(err.ExitCode()) 230 | } 231 | if err != nil { 232 | log.Fatal("finish command:", err) 233 | } 234 | } 235 | --------------------------------------------------------------------------------