├── go.sum ├── go.mod ├── doc └── screenshot1.png ├── cmd ├── oberon-emu │ ├── go.mod │ ├── go.sum │ ├── led.go │ ├── internal │ │ └── canvas │ │ │ ├── web │ │ │ ├── index.html.tmpl │ │ │ └── canvas-websocket.js │ │ │ ├── context.go │ │ │ ├── buffer.go │ │ │ ├── serve.go │ │ │ └── event.go │ ├── clipboard.go │ ├── options.go │ ├── main.go │ └── ps2.go ├── oberon-emu-sdl │ ├── go.mod │ ├── go.sum │ ├── led.go │ ├── action.go │ ├── clipboard.go │ ├── options.go │ ├── ps2.go │ └── main.go ├── ob2unix │ └── main.go └── asciidecoder │ └── main.go ├── risc ├── io.go ├── internal │ └── fp │ │ ├── test │ │ ├── gen.go │ │ ├── README.md │ │ ├── flr │ │ │ └── flr.go │ │ ├── flt │ │ │ └── flt.go │ │ ├── div │ │ │ └── div.go │ │ ├── mul │ │ │ └── mul.go │ │ ├── add │ │ │ └── add.go │ │ ├── idiv │ │ │ └── idiv.go │ │ ├── numbers │ │ │ └── numbers.go │ │ └── v2go │ │ │ └── v2go.go │ │ └── fp.go ├── framebuffer.go ├── boot.go └── risc.go ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── LICENSE ├── serial ├── raw.go └── pclink.go ├── NOTICE ├── README.md └── spi └── disk.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fzipp/oberon 2 | 3 | go 1.22.0 4 | -------------------------------------------------------------------------------- /doc/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzipp/oberon/HEAD/doc/screenshot1.png -------------------------------------------------------------------------------- /cmd/oberon-emu/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fzipp/oberon/cmd/oberon-emu 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fzipp/oberon v0.3.0 7 | github.com/gorilla/websocket v1.5.3 8 | ) 9 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fzipp/oberon/cmd/oberon-emu-sdl 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fzipp/oberon v0.3.0 7 | github.com/veandco/go-sdl2 v0.4.40 8 | ) 9 | -------------------------------------------------------------------------------- /cmd/oberon-emu/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fzipp/oberon v0.3.0 h1:SLiLumqPHoK3XDRgMnNEZ0a13h8VgHcWazbePLRU2QY= 2 | github.com/fzipp/oberon v0.3.0/go.mod h1:oYuMFsCTnNCKtYbC9RLFixf1sBXBjrXZ3V0bAOnCBLU= 3 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 4 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fzipp/oberon v0.3.0 h1:SLiLumqPHoK3XDRgMnNEZ0a13h8VgHcWazbePLRU2QY= 2 | github.com/fzipp/oberon v0.3.0/go.mod h1:oYuMFsCTnNCKtYbC9RLFixf1sBXBjrXZ3V0bAOnCBLU= 3 | github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U= 4 | github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= 5 | -------------------------------------------------------------------------------- /cmd/oberon-emu/led.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "fmt" 8 | 9 | type ConsoleLEDs struct{} 10 | 11 | func (led *ConsoleLEDs) Write(value uint32) { 12 | fmt.Print("LEDs: ") 13 | for i := 7; i >= 0; i-- { 14 | if value&(1< 0 { 15 | fmt.Printf("%d", i) 16 | } else { 17 | fmt.Print("-") 18 | } 19 | } 20 | fmt.Println() 21 | } 22 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/led.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "fmt" 8 | 9 | type ConsoleLEDs struct{} 10 | 11 | func (led *ConsoleLEDs) Write(value uint32) { 12 | fmt.Print("LEDs: ") 13 | for i := 7; i >= 0; i-- { 14 | if value&(1< 0 { 15 | fmt.Printf("%d", i) 16 | } else { 17 | fmt.Print("-") 18 | } 19 | } 20 | fmt.Println() 21 | } 22 | -------------------------------------------------------------------------------- /risc/io.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package risc 6 | 7 | type Serial interface { 8 | ReadStatus() uint32 9 | ReadData() uint32 10 | WriteData(value uint32) 11 | } 12 | 13 | type SPI interface { 14 | ReadData() uint32 15 | WriteData(value uint32) 16 | } 17 | 18 | type Clipboard interface { 19 | WriteControl(len uint32) 20 | ReadControl() uint32 21 | WriteData(value uint32) 22 | ReadData() uint32 23 | } 24 | 25 | type LED interface { 26 | Write(value uint32) 27 | } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/cmd/oberon-emu" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "gomod" # See documentation for possible values 13 | directory: "/cmd/oberon-emu-sdl" # Location of package manifests 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Update packages 10 | run: sudo apt-get update 11 | - name: Install dev packages 12 | run: sudo apt-get install -y libsdl2-dev 13 | - uses: actions/checkout@v2 14 | - name: Setup Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: '1.x' 18 | - name: Run tests 19 | run: go test -cover ./... 20 | - name: Run tests for cmd/oberon-emu 21 | working-directory: cmd/oberon-emu 22 | run: go test -cover ./... 23 | - name: Run tests for cmd/oberon-emu-sdl 24 | working-directory: cmd/oberon-emu-sdl 25 | run: go test -cover ./... 26 | -------------------------------------------------------------------------------- /risc/internal/fp/test/gen.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package test 6 | 7 | // The Verilog (.v) files can be downloaded at 8 | // https://people.inf.ethz.ch/wirth/ProjectOberon/index.html 9 | //go:generate go run v2go/v2go.go -o add/add_gen.go FPAdder.v 10 | //go:generate go run v2go/v2go.go -o flr/add_gen.go FPAdder.v 11 | //go:generate go run v2go/v2go.go -o flt/add_gen.go FPAdder.v 12 | //go:generate go run v2go/v2go.go -o mul/mul_gen.go FPMultiplier.v 13 | //go:generate go run v2go/v2go.go -o div/div_gen.go FPDivider.v 14 | //go:generate go run v2go/v2go.go -o idiv/idiv_gen.go Divider.v 15 | 16 | //go:generate go run numbers/numbers.go -o numbers_gen.go 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for 4 | any purpose with or without fee is hereby granted, provided that the 5 | above copyright notice and this permission notice appear in all 6 | copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 9 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 10 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 11 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 13 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /risc/internal/fp/test/README.md: -------------------------------------------------------------------------------- 1 | # Tests for floating-point arithmetic 2 | 3 | This directory contains test infrastructure for the floating-point arithmetic 4 | operations of the RISC emulator. 5 | 6 | The tests are run by transpiling the original Verilog sources to Go code and 7 | then comparing the emulator implementation against the Verilog implementation. 8 | 9 | Set the `VERILOG` environment variable to the directory with the Verilog 10 | sources. You can download the Verilog sources from: 11 | https://people.inf.ethz.ch/wirth/ProjectOberon/index.html 12 | 13 | The following files are required: 14 | 15 | Divider.v FPAdder.v FPDivider.v FPMultiplier.v 16 | 17 | Use `go generate` to transpile the Verilog sources to Go: 18 | 19 | $ go generate 20 | 21 | The tests are implemented as main programs in the following sub-directories: 22 | 23 | add div flr flt idiv mul 24 | 25 | Run the tests: 26 | 27 | $ go run add/*.go 28 | $ go run div/*.go 29 | $ go run flr/*.go 30 | $ go run flt/*.go 31 | $ go run idiv/*.go 32 | $ go run mul/*.go 33 | -------------------------------------------------------------------------------- /risc/internal/fp/test/flr/flr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vAdd(inX, inY uint32) uint32 { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | run = 0 22 | cycle() 23 | run = 1 24 | for { 25 | cycle() 26 | if stall() == 0 { 27 | break 28 | } 29 | } 30 | return uint32(z()) 31 | } 32 | 33 | func main() { 34 | v = 1 35 | count := 0 36 | errors := 0 37 | for _, a := range test.Numbers { 38 | b := uint32(0x4B00 << 16) 39 | want := vAdd(a, b) 40 | got := fp.Add(a, b, false, true) 41 | if got != want { 42 | if errors < 10 { 43 | fmt.Printf("flr: fp.Add(%08x, %08x, false, true) = %08x, want (Verilog): %08x\n", a, b, got, want) 44 | } 45 | errors++ 46 | } 47 | count++ 48 | } 49 | fmt.Printf("flr: errors: %d tests: %d\n", errors, count) 50 | if errors > 0 { 51 | os.Exit(1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /risc/internal/fp/test/flt/flt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vAdd(inX, inY uint32) uint32 { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | run = 0 22 | cycle() 23 | run = 1 24 | for { 25 | cycle() 26 | if stall() == 0 { 27 | break 28 | } 29 | } 30 | return uint32(z()) 31 | } 32 | 33 | func main() { 34 | u = 1 35 | count := 0 36 | errors := 0 37 | for _, a := range test.Numbers { 38 | b := uint32(0x4B00 << 16) 39 | want := vAdd(a, b) 40 | got := fp.Add(a, b, true, false) 41 | if got != want { 42 | if errors < 10 { 43 | fmt.Printf("flt: fp.Add(%08x, %08x, true, false) = %08x, want (Verilog): %08x\n", a, b, got, want) 44 | } 45 | errors++ 46 | } 47 | count++ 48 | } 49 | fmt.Printf("flt: errors: %d tests: %d\n", errors, count) 50 | if errors > 0 { 51 | os.Exit(1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /risc/internal/fp/test/div/div.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vDiv(inX, inY uint32) uint32 { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | run = 0 22 | cycle() 23 | run = 1 24 | for { 25 | cycle() 26 | if stall() == 0 { 27 | break 28 | } 29 | } 30 | return uint32(z()) 31 | } 32 | 33 | func main() { 34 | count := 0 35 | errors := 0 36 | for i, a := range test.Numbers { 37 | for _, b := range test.Numbers { 38 | want := vDiv(a, b) 39 | got := fp.Div(a, b) 40 | if got != want { 41 | if errors < 10 { 42 | fmt.Printf("div: fp.Div(%08x, %08x) = %08x, want (Verilog): %08x\n", a, b, got, want) 43 | } 44 | errors++ 45 | } 46 | count++ 47 | } 48 | if (i % 500) == 0 { 49 | p := count * 100 / len(test.Numbers) / len(test.Numbers) 50 | fmt.Printf("div: %d%% (%d errors)\n", p, errors) 51 | } 52 | } 53 | fmt.Printf("div: errors: %d tests: %d\n", errors, count) 54 | if errors > 0 { 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /risc/internal/fp/test/mul/mul.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vMul(inX, inY uint32) uint32 { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | run = 0 22 | cycle() 23 | run = 1 24 | for { 25 | cycle() 26 | if stall() == 0 { 27 | break 28 | } 29 | } 30 | return uint32(z()) 31 | } 32 | 33 | func main() { 34 | count := 0 35 | errors := 0 36 | for i, a := range test.Numbers { 37 | for _, b := range test.Numbers { 38 | want := vMul(a, b) 39 | got := fp.Mul(a, b) 40 | if got != want { 41 | if errors < 10 { 42 | fmt.Printf("mul: fp.Mul(%08x, %08x) = %08x, want (Verilog): %08x\n", a, b, got, want) 43 | } 44 | errors++ 45 | } 46 | count++ 47 | } 48 | if (i % 500) == 0 { 49 | p := count * 100 / len(test.Numbers) / len(test.Numbers) 50 | fmt.Printf("mul: %d%% (%d errors)\n", p, errors) 51 | } 52 | } 53 | fmt.Printf("mul: errors: %d tests: %d\n", errors, count) 54 | if errors > 0 { 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/web/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | {{.Title}} 11 | 12 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /risc/internal/fp/test/add/add.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vAdd(inX, inY uint32) uint32 { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | run = 0 22 | cycle() 23 | run = 1 24 | for { 25 | cycle() 26 | if stall() == 0 { 27 | break 28 | } 29 | } 30 | return uint32(z()) 31 | } 32 | 33 | func main() { 34 | count := 0 35 | errors := 0 36 | for i, a := range test.Numbers { 37 | for _, b := range test.Numbers { 38 | want := vAdd(a, b) 39 | got := fp.Add(a, b, false, false) 40 | if got != want { 41 | if errors < 10 { 42 | fmt.Printf("add: fp.Add(%08x, %08x, false, false) = %08x, want (Verilog): %08x\n", a, b, got, want) 43 | } 44 | errors++ 45 | } 46 | count++ 47 | } 48 | if (i % 500) == 0 { 49 | p := count * 100 / len(test.Numbers) / len(test.Numbers) 50 | fmt.Printf("add: %d%% (%d errors)\n", p, errors) 51 | } 52 | } 53 | fmt.Printf("add: errors: %d tests: %d\n", errors, count) 54 | if errors > 0 { 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /risc/framebuffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package risc 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | ) 11 | 12 | var ( 13 | colorBlack = color.RGBA{R: 0x65, G: 0x7b, B: 0x83, A: 0xff} 14 | colorWhite = color.RGBA{R: 0xfd, G: 0xf6, B: 0xe3, A: 0xff} 15 | ) 16 | 17 | type Framebuffer struct { 18 | Rect image.Rectangle 19 | Pix []uint32 20 | } 21 | 22 | func (fb *Framebuffer) ColorModel() color.Model { 23 | return color.RGBAModel 24 | } 25 | 26 | func (fb *Framebuffer) Bounds() image.Rectangle { 27 | return fb.Rect 28 | } 29 | 30 | func (fb *Framebuffer) At(x, y int) color.Color { 31 | if !(image.Point{X: x, Y: y}.In(fb.Rect)) { 32 | return color.RGBA{} 33 | } 34 | i, bit := fb.PixOffset(x, y) 35 | if (fb.Pix[i]>>bit)&1 == 0 { 36 | return colorBlack 37 | } 38 | return colorWhite 39 | } 40 | 41 | // PixOffset is a helper method to locate a pixel in the Pix slice. 42 | // It returns the index in Pix for the pixel at coordinates (x, y) and the bit 43 | // position of the pixel within the word, i.e. the pixel value can be isolated 44 | // with (fb.Pix[i]>>bit)&1. 45 | // 46 | // The coordinates (x, y) are interpreted as image coordinates with the 47 | // origin (0, 0) at the top left. 48 | func (fb *Framebuffer) PixOffset(x, y int) (i, bit int) { 49 | return (fb.Rect.Max.Y-y)*(fb.Rect.Max.X/32) + (x / 32), x % 32 50 | } 51 | -------------------------------------------------------------------------------- /risc/internal/fp/test/idiv/idiv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | //go:build ignore 6 | // +build ignore 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | 14 | "github.com/fzipp/oberon/risc/internal/fp" 15 | "github.com/fzipp/oberon/risc/internal/fp/test" 16 | ) 17 | 18 | func vIdiv(inX, inY uint32, signedDiv bool) fp.IdivResult { 19 | x = uint64(inX) 20 | y = uint64(inY) 21 | u = 0 22 | if signedDiv { 23 | u = 1 24 | } 25 | run = 0 26 | cycle() 27 | run = 1 28 | for { 29 | cycle() 30 | if stall() == 0 { 31 | break 32 | } 33 | } 34 | return fp.IdivResult{Quot: uint32(quot()), Rem: uint32(rem())} 35 | } 36 | 37 | func main() { 38 | count := 0 39 | errors := 0 40 | for _, s := range []bool{false, true} { 41 | for i, a := range test.Numbers { 42 | for _, b := range test.Numbers { 43 | want := vIdiv(a, b, s) 44 | got := fp.Idiv(a, b, s) 45 | if got != want { 46 | if errors < 20 { 47 | fmt.Printf("idiv: fp.Idiv(%08x, %d, %v) = %v, want (Verilog): %v\n", a, b, s, got, want) 48 | } 49 | errors++ 50 | } 51 | count++ 52 | } 53 | if (i % 500) == 0 { 54 | p := count * 100 / len(test.Numbers) / len(test.Numbers) / 2 55 | fmt.Printf("idiv: %d%% (%d errors)\n", p, errors) 56 | } 57 | } 58 | } 59 | fmt.Printf("idiv: errors: %d tests: %d\n", errors, count) 60 | if errors > 0 { 61 | os.Exit(1) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/oberon-emu/clipboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "math" 9 | "strings" 10 | 11 | "github.com/fzipp/oberon/cmd/oberon-emu/internal/canvas" 12 | ) 13 | 14 | type Clipboard struct { 15 | ctx *canvas.Context 16 | state clipboardState 17 | text string 18 | data string 19 | dataLen uint32 20 | } 21 | 22 | type clipboardState int 23 | 24 | const ( 25 | clipIdle clipboardState = iota 26 | clipGet 27 | clipPut 28 | ) 29 | 30 | func (c *Clipboard) reset() { 31 | c.state = clipIdle 32 | c.data = "" 33 | c.dataLen = 0 34 | } 35 | 36 | func (c *Clipboard) setText(data string) { 37 | c.text = data 38 | } 39 | 40 | func (c *Clipboard) ReadControl() uint32 { 41 | c.reset() 42 | c.data = c.text 43 | c.data = strings.ReplaceAll(c.data, "\r\n", "\r") 44 | c.data = strings.ReplaceAll(c.data, "\n", "\r") 45 | if len(c.data) > math.MaxUint32 { 46 | c.reset() 47 | return 0 48 | } 49 | c.dataLen = uint32(len(c.data)) 50 | c.state = clipGet 51 | return c.dataLen 52 | } 53 | 54 | func (c *Clipboard) WriteControl(length uint32) { 55 | c.reset() 56 | c.state = clipPut 57 | c.dataLen = length 58 | } 59 | 60 | func (c *Clipboard) ReadData() uint32 { 61 | if c.state != clipGet { 62 | return 0 63 | } 64 | if len(c.data) == 0 { 65 | c.reset() 66 | return 0 67 | } 68 | result := uint32(c.data[0]) 69 | c.data = c.data[1:] 70 | return result 71 | } 72 | 73 | func (c *Clipboard) WriteData(value uint32) { 74 | if c.state != clipPut { 75 | return 76 | } 77 | ch := byte(value) 78 | if ch == '\r' { 79 | ch = '\n' 80 | } 81 | c.data += string(rune(ch)) 82 | if len(c.data) == int(c.dataLen) { 83 | c.ctx.ClipboardWriteText(c.data) 84 | c.reset() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/action.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "github.com/veandco/go-sdl2/sdl" 8 | 9 | type action int 10 | 11 | const ( 12 | actionOberonInput action = iota 13 | actionQuit 14 | actionReset 15 | actionToggleFullscreen 16 | actionFakeMouse1 17 | actionFakeMouse2 18 | actionFakeMouse3 19 | ) 20 | 21 | type keyMapping struct { 22 | state uint8 23 | sym sdl.Keycode 24 | mod1, mod2 sdl.Keymod 25 | action action 26 | } 27 | 28 | var keyMap = []keyMapping{ 29 | {sdl.PRESSED, sdl.K_F4, sdl.KMOD_ALT, 0, actionQuit}, 30 | {sdl.PRESSED, sdl.K_F12, 0, 0, actionReset}, 31 | {sdl.PRESSED, sdl.K_DELETE, sdl.KMOD_CTRL, sdl.KMOD_SHIFT, actionReset}, 32 | {sdl.PRESSED, sdl.K_F11, 0, 0, actionToggleFullscreen}, 33 | {sdl.PRESSED, sdl.K_RETURN, sdl.KMOD_ALT, 0, actionToggleFullscreen}, 34 | {sdl.PRESSED, sdl.K_f, sdl.KMOD_GUI, sdl.KMOD_CTRL, actionToggleFullscreen}, // Mac fullscreen shortcut 35 | {sdl.PRESSED, sdl.K_LALT, 0, 0, actionFakeMouse2}, 36 | {sdl.RELEASED, sdl.K_LALT, 0, 0, actionFakeMouse2}, 37 | 38 | {sdl.PRESSED, sdl.K_LCTRL, 0, 0, actionFakeMouse1}, 39 | {sdl.RELEASED, sdl.K_LCTRL, 0, 0, actionFakeMouse1}, 40 | {sdl.PRESSED, sdl.K_LGUI, 0, 0, actionFakeMouse3}, 41 | {sdl.RELEASED, sdl.K_LGUI, 0, 0, actionFakeMouse3}, 42 | } 43 | 44 | func mapKeyboardEvent(event *sdl.KeyboardEvent) action { 45 | for i := range keyMap { 46 | if (event.State == keyMap[i].state) && 47 | (event.Keysym.Sym == keyMap[i].sym) && 48 | ((keyMap[i].mod1 == 0) || (sdl.Keymod(event.Keysym.Mod)&keyMap[i].mod1) > 0) && 49 | ((keyMap[i].mod2 == 0) || (sdl.Keymod(event.Keysym.Mod)&keyMap[i].mod2) > 0) { 50 | 51 | return keyMap[i].action 52 | } 53 | } 54 | return actionOberonInput 55 | } 56 | -------------------------------------------------------------------------------- /serial/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package serial 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "os" 12 | ) 13 | 14 | type Raw struct { 15 | in *os.File 16 | r *bufio.Reader 17 | w io.WriteCloser 18 | } 19 | 20 | func Open(filenameIn, filenameOut string) (*Raw, error) { 21 | if filenameIn == "" { 22 | filenameIn = os.DevNull 23 | } 24 | if filenameOut == "" { 25 | filenameOut = os.DevNull 26 | } 27 | 28 | raw := &Raw{} 29 | 30 | in, err := os.Open(filenameIn) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to open serial input file: %w", err) 33 | } 34 | raw.in = in 35 | raw.r = bufio.NewReader(in) 36 | 37 | out, err := os.OpenFile(filenameOut, os.O_RDWR, 0o666) 38 | if err != nil { 39 | _ = in.Close() 40 | return nil, fmt.Errorf("failed to open serial output file: %w", err) 41 | } 42 | raw.w = out 43 | 44 | return raw, nil 45 | } 46 | 47 | func (r *Raw) ReadStatus() uint32 { 48 | _, err := r.r.ReadByte() 49 | if err != nil { 50 | return 2 51 | } 52 | err = r.r.UnreadByte() 53 | if err != nil { 54 | return 2 55 | } 56 | return 3 57 | } 58 | 59 | func (r *Raw) ReadData() uint32 { 60 | var buf [1]byte 61 | _, err := r.r.Read(buf[:]) 62 | if err != nil { 63 | _, _ = fmt.Fprintf(os.Stderr, "can't read serial data: %s\n", err) 64 | } 65 | return uint32(buf[0]) 66 | } 67 | 68 | func (r *Raw) WriteData(value uint32) { 69 | buf := [1]byte{byte(value)} 70 | _, err := r.w.Write(buf[:]) 71 | if err != nil { 72 | _, _ = fmt.Fprintf(os.Stderr, "can't write serial data: %s\n", err) 73 | } 74 | } 75 | 76 | func (r *Raw) Close() error { 77 | var err error 78 | err = r.in.Close() 79 | if err != nil { 80 | return err 81 | } 82 | err = r.w.Close() 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Ported to Go from Peter De Wachter's Oberon RISC Emulator: 2 | https://github.com/pdewacht/oberon-risc-emu/ 3 | 4 | Copyright © 2014 Peter De Wachter 5 | 6 | Permission to use, copy, modify, and/or distribute this software for 7 | any purpose with or without fee is hereby granted, provided that the 8 | above copyright notice and this permission notice appear in all 9 | copies. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 16 | PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | PERFORMANCE OF THIS SOFTWARE. 19 | 20 | --- 21 | 22 | Project Oberon, Revised Edition 2013: 23 | https://people.inf.ethz.ch/wirth/ProjectOberon/index.html 24 | 25 | Book copyright (C)2013 Niklaus Wirth and Juerg Gutknecht; 26 | software copyright (C)2013 Niklaus Wirth (NW), Juerg Gutknecht (JG), Paul 27 | Reed (PR/PDR). 28 | 29 | Permission to use, copy, modify, and/or distribute this software and its 30 | accompanying documentation (the "Software") for any purpose with or 31 | without fee is hereby granted, provided that the above copyright notice 32 | and this permission notice appear in all copies. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES 35 | WITH REGARD TO THE SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF 36 | MERCHANTABILITY, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS BE LIABLE FOR ANY CLAIM, SPECIAL, DIRECT, INDIRECT, OR 38 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES OR LIABILITY WHATSOEVER, WHETHER IN 39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 40 | CONNECTION WITH THE DEALINGS IN OR USE OR PERFORMANCE OF THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/clipboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "math" 10 | "os" 11 | "strings" 12 | 13 | "github.com/veandco/go-sdl2/sdl" 14 | ) 15 | 16 | type SDLClipboard struct { 17 | state clipboardState 18 | data string 19 | dataLen uint32 20 | } 21 | 22 | type clipboardState int 23 | 24 | const ( 25 | clipIdle clipboardState = iota 26 | clipGet 27 | clipPut 28 | ) 29 | 30 | func (c *SDLClipboard) reset() { 31 | c.state = clipIdle 32 | c.data = "" 33 | c.dataLen = 0 34 | } 35 | 36 | func (c *SDLClipboard) ReadControl() uint32 { 37 | c.reset() 38 | data, err := sdl.GetClipboardText() 39 | if err != nil { 40 | _, _ = fmt.Fprintf(os.Stderr, "can't get clipboard text: %s", err) 41 | } 42 | c.data = strings.ReplaceAll(data, "\r\n", "\r") 43 | c.data = strings.ReplaceAll(c.data, "\n", "\r") 44 | if len(c.data) > math.MaxUint32 { 45 | c.reset() 46 | return 0 47 | } 48 | c.dataLen = uint32(len(c.data)) 49 | c.state = clipGet 50 | return c.dataLen 51 | } 52 | 53 | func (c *SDLClipboard) WriteControl(length uint32) { 54 | c.reset() 55 | c.state = clipPut 56 | c.dataLen = length 57 | } 58 | 59 | func (c *SDLClipboard) ReadData() uint32 { 60 | if c.state != clipGet { 61 | return 0 62 | } 63 | if len(c.data) == 0 { 64 | c.reset() 65 | return 0 66 | } 67 | result := uint32(c.data[0]) 68 | c.data = c.data[1:] 69 | return result 70 | } 71 | 72 | func (c *SDLClipboard) WriteData(value uint32) { 73 | if c.state != clipPut { 74 | return 75 | } 76 | ch := byte(value) 77 | if ch == '\r' { 78 | ch = '\n' 79 | } 80 | c.data += string(rune(ch)) 81 | if len(c.data) == int(c.dataLen) { 82 | err := sdl.SetClipboardText(c.data) 83 | if err != nil { 84 | _, _ = fmt.Fprintf(os.Stderr, "can't set clipboard text: %s", err) 85 | } 86 | c.reset() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/context.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/fzipp/oberon/risc" 7 | ) 8 | 9 | type Context struct { 10 | config config 11 | draws chan<- []byte 12 | events <-chan Event 13 | buf buffer 14 | } 15 | 16 | func newContext(draws chan<- []byte, events <-chan Event, config config) *Context { 17 | return &Context{ 18 | config: config, 19 | draws: draws, 20 | events: events, 21 | } 22 | } 23 | 24 | func (ctx *Context) Events() <-chan Event { 25 | return ctx.events 26 | } 27 | 28 | func (ctx *Context) CanvasWidth() int { 29 | return ctx.config.width 30 | } 31 | 32 | func (ctx *Context) CanvasHeight() int { 33 | return ctx.config.height 34 | } 35 | 36 | const ( 37 | colorBlack = 0x657b83ff 38 | colorWhite = 0xfdf6e3ff 39 | ) 40 | 41 | const ( 42 | bUpdateDisplay byte = 1 + iota 43 | bClipboardWriteText 44 | ) 45 | 46 | func (ctx *Context) UpdateDisplay(fb *risc.Framebuffer, r image.Rectangle) { 47 | if r.Min.Y > r.Max.Y { 48 | return 49 | } 50 | 51 | cw := ctx.config.width 52 | ch := ctx.config.height 53 | 54 | x := uint32(r.Min.X * 32) 55 | y := uint32(ch - r.Max.Y - 1) 56 | w := uint32((r.Max.X - r.Min.X + 1) * 32) 57 | h := uint32(r.Max.Y - r.Min.Y + 1) 58 | 59 | ctx.buf.addByte(bUpdateDisplay) 60 | ctx.buf.addUint32(x) 61 | ctx.buf.addUint32(y) 62 | ctx.buf.addUint32(w) 63 | ctx.buf.addUint32(h) 64 | 65 | for line := r.Max.Y; line >= r.Min.Y; line-- { 66 | lineStart := line * (cw / 32) 67 | for col := r.Min.X; col <= r.Max.X; col++ { 68 | pixels := fb.Pix[lineStart+col] 69 | for range 32 { 70 | var color uint32 71 | if pixels&1 > 0 { 72 | color = colorWhite 73 | } else { 74 | color = colorBlack 75 | } 76 | ctx.buf.addUint32(color) 77 | pixels >>= 1 78 | } 79 | } 80 | } 81 | ctx.Flush() 82 | } 83 | 84 | func (ctx *Context) ClipboardWriteText(text string) { 85 | ctx.buf.addByte(bClipboardWriteText) 86 | ctx.buf.addString(text) 87 | ctx.Flush() 88 | } 89 | 90 | func (ctx *Context) Flush() { 91 | ctx.draws <- ctx.buf.bytes 92 | ctx.buf.reset() 93 | } 94 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. 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 canvas 6 | 7 | import ( 8 | "encoding/binary" 9 | "math" 10 | ) 11 | 12 | type buffer struct { 13 | bytes []byte 14 | error error 15 | } 16 | 17 | var byteOrder = binary.BigEndian 18 | 19 | func (buf *buffer) addByte(b byte) { 20 | buf.bytes = append(buf.bytes, b) 21 | } 22 | 23 | func (buf *buffer) addUint32(i uint32) { 24 | buf.bytes = append(buf.bytes, 0, 0, 0, 0) 25 | byteOrder.PutUint32(buf.bytes[len(buf.bytes)-4:], i) 26 | } 27 | 28 | func (buf *buffer) addString(s string) { 29 | buf.addUint32(uint32(len(s))) 30 | buf.bytes = append(buf.bytes, []byte(s)...) 31 | } 32 | 33 | func (buf *buffer) readByte() byte { 34 | if len(buf.bytes) < 1 { 35 | buf.dataTooShort() 36 | return 0 37 | } 38 | b := buf.bytes[0] 39 | buf.bytes = buf.bytes[1:] 40 | return b 41 | } 42 | 43 | func (buf *buffer) readUint32() uint32 { 44 | if len(buf.bytes) < 4 { 45 | buf.dataTooShort() 46 | return 0 47 | } 48 | i := byteOrder.Uint32(buf.bytes) 49 | buf.bytes = buf.bytes[4:] 50 | return i 51 | } 52 | 53 | func (buf *buffer) readUint64() uint64 { 54 | if len(buf.bytes) < 8 { 55 | buf.dataTooShort() 56 | return 0 57 | } 58 | i := byteOrder.Uint64(buf.bytes) 59 | buf.bytes = buf.bytes[8:] 60 | return i 61 | } 62 | 63 | func (buf *buffer) readFloat64() float64 { 64 | return math.Float64frombits(buf.readUint64()) 65 | } 66 | 67 | func (buf *buffer) readString() string { 68 | length := int(buf.readUint32()) 69 | if len(buf.bytes) < length { 70 | buf.dataTooShort() 71 | return "" 72 | } 73 | s := string(buf.bytes[:length]) 74 | buf.bytes = buf.bytes[length:] 75 | return s 76 | } 77 | 78 | func (buf *buffer) reset() { 79 | buf.bytes = make([]byte, 0, cap(buf.bytes)) 80 | } 81 | 82 | func (buf *buffer) dataTooShort() { 83 | buf.reset() 84 | buf.error = errDataTooShort{} 85 | } 86 | 87 | type errDataTooShort struct{} 88 | 89 | func (err errDataTooShort) Error() string { 90 | return "data too short" 91 | } 92 | -------------------------------------------------------------------------------- /risc/internal/fp/test/numbers/numbers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command numbers generates a Go file containing a slice of uint32 numbers 6 | // as test input for the Oberon RISC emulator's floating-point arithmetic 7 | // operations. 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "math/rand/v2" 14 | "os" 15 | ) 16 | 17 | func usage() { 18 | fail(`Generates a Go file containing a slice of uint32 numbers as test 19 | input for the Oberon RISC emulator's floating-point arithmetic operations. 20 | 21 | Usage: 22 | numbers [-o go_file] [-n count] 23 | 24 | Flags: 25 | -o The output file (Go). Default: standard output 26 | -n The length of the generated slice of numbers. Default: 25000`) 27 | } 28 | 29 | func main() { 30 | nFlag := flag.Int("n", 25000, "`count`") 31 | oFlag := flag.String("o", "", "output `file` (Go)") 32 | flag.Usage = usage 33 | flag.Parse() 34 | 35 | var err error 36 | count := *nFlag 37 | 38 | w := os.Stdout 39 | if *oFlag != "" { 40 | w, err = os.Create(*oFlag) 41 | check(err) 42 | } 43 | 44 | var numbers []uint32 45 | for e := range uint32(256) { 46 | numbers = append(numbers, e<<23) 47 | numbers = append(numbers, (e<<23)|1) 48 | numbers = append(numbers, (e<<23)|0x7fffff) 49 | numbers = append(numbers, (e<<23)|0x7ffffe) 50 | } 51 | for _, x := range numbers { 52 | numbers = append(numbers, x|0x80000000) 53 | } 54 | restCount := count - len(numbers) 55 | for range restCount { 56 | numbers = append(numbers, rand.Uint32()) 57 | } 58 | 59 | _, err = fmt.Fprint(w, `// Code generated by numbers.go; DO NOT EDIT. 60 | 61 | package test 62 | 63 | var Numbers = []uint32{ 64 | `) 65 | check(err) 66 | for _, x := range numbers { 67 | _, err = fmt.Fprintf(w, "\t0x%08X,\n", x) 68 | check(err) 69 | } 70 | _, err = fmt.Fprintln(w, "}") 71 | check(err) 72 | } 73 | 74 | func check(err error) { 75 | if err != nil { 76 | fail(err) 77 | } 78 | } 79 | 80 | func fail(msg any) { 81 | _, _ = fmt.Fprintln(os.Stderr, msg) 82 | os.Exit(1) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "image" 12 | 13 | "github.com/fzipp/oberon/risc" 14 | ) 15 | 16 | const ( 17 | maxHeight = 2048 18 | maxWidth = 2048 19 | ) 20 | 21 | type options struct { 22 | fullscreen bool 23 | zoom float64 24 | leds bool 25 | mem int 26 | size string 27 | sizeRect image.Rectangle 28 | bootFromSerial bool 29 | serialIn string 30 | serialOut string 31 | diskImageFile string 32 | } 33 | 34 | func optionsFromFlags() (*options, error) { 35 | fullscreen := flag.Bool("fullscreen", false, "Start the emulator in full screen mode") 36 | zoom := flag.Float64("zoom", 0, "Scale the display in windowed mode by the given factor") 37 | leds := flag.Bool("leds", false, "Log LED state on stdout") 38 | mem := flag.Int("mem", 0, "Set memory size in `MEGS`") 39 | size := flag.String("size", "", "Set framebuffer size to `WIDTHxHEIGHT`") 40 | bootFromSerial := flag.Bool("boot-from-serial", false, "Boot from serial line (disk image not required)") 41 | serialIn := flag.String("serial-in", "", "Read serial input from `FILE`") 42 | serialOut := flag.String("serial-out", "", "Read serial input from `FILE`") 43 | 44 | flag.Parse() 45 | 46 | diskImageFile := "" 47 | if !*bootFromSerial { 48 | if flag.NArg() < 1 { 49 | return nil, errors.New("missing argument") 50 | } 51 | diskImageFile = flag.Arg(0) 52 | } 53 | 54 | sizeRect := image.Rect(0, 0, risc.FramebufferWidth, risc.FramebufferHeight) 55 | 56 | if *size != "" { 57 | var w, h int 58 | _, err := fmt.Sscanf(*size, "%dx%d", &w, &h) 59 | if err != nil { 60 | return nil, errors.New("invalid size") 61 | } 62 | sizeRect = image.Rect(0, 0, 63 | clamp(w, 32, maxWidth)&^31, 64 | clamp(h, 32, maxHeight), 65 | ) 66 | } 67 | 68 | return &options{ 69 | fullscreen: *fullscreen, 70 | zoom: *zoom, 71 | leds: *leds, 72 | mem: *mem, 73 | size: *size, 74 | sizeRect: sizeRect, 75 | bootFromSerial: *bootFromSerial, 76 | serialIn: *serialIn, 77 | serialOut: *serialOut, 78 | diskImageFile: diskImageFile, 79 | }, nil 80 | } 81 | 82 | func clamp(x, min, max int) int { 83 | if x < min { 84 | return min 85 | } 86 | if x > max { 87 | return max 88 | } 89 | return x 90 | } 91 | -------------------------------------------------------------------------------- /cmd/oberon-emu/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "image" 12 | 13 | "github.com/fzipp/oberon/risc" 14 | ) 15 | 16 | const ( 17 | maxHeight = 2048 18 | maxWidth = 2048 19 | ) 20 | 21 | type options struct { 22 | http string 23 | open bool 24 | fullscreen bool 25 | zoom float64 26 | leds bool 27 | mem int 28 | size string 29 | sizeRect image.Rectangle 30 | bootFromSerial bool 31 | serialIn string 32 | serialOut string 33 | diskImageFile string 34 | } 35 | 36 | func optionsFromFlags() (*options, error) { 37 | http := flag.String("http", ":8080", "HTTP service address (e.g., '127.0.0.1:8080' or just ':8080')") 38 | fullscreen := flag.Bool("fullscreen", false, "Start the emulator in full screen mode") 39 | zoom := flag.Float64("zoom", 0, "Scale the display in windowed mode by the given factor") 40 | leds := flag.Bool("leds", false, "Log LED state on stdout") 41 | mem := flag.Int("mem", 0, "Set memory size in `MEGS`") 42 | size := flag.String("size", "", "Set framebuffer size to `WIDTHxHEIGHT`") 43 | bootFromSerial := flag.Bool("boot-from-serial", false, "Boot from serial line (disk image not required)") 44 | serialIn := flag.String("serial-in", "", "Read serial input from `FILE`") 45 | serialOut := flag.String("serial-out", "", "Read serial input from `FILE`") 46 | open := flag.Bool("open", true, "Try to open browser") 47 | 48 | flag.Parse() 49 | 50 | diskImageFile := "" 51 | if !*bootFromSerial { 52 | if flag.NArg() < 1 { 53 | return nil, errors.New("missing argument") 54 | } 55 | diskImageFile = flag.Arg(0) 56 | } 57 | 58 | sizeRect := image.Rect(0, 0, risc.FramebufferWidth, risc.FramebufferHeight) 59 | 60 | if *size != "" { 61 | var w, h int 62 | _, err := fmt.Sscanf(*size, "%dx%d", &w, &h) 63 | if err != nil { 64 | return nil, errors.New("invalid size") 65 | } 66 | sizeRect = image.Rect(0, 0, 67 | clamp(w, 32, maxWidth)&^31, 68 | clamp(h, 32, maxHeight), 69 | ) 70 | } 71 | 72 | return &options{ 73 | http: *http, 74 | open: *open, 75 | fullscreen: *fullscreen, 76 | zoom: *zoom, 77 | leds: *leds, 78 | mem: *mem, 79 | size: *size, 80 | sizeRect: sizeRect, 81 | bootFromSerial: *bootFromSerial, 82 | serialIn: *serialIn, 83 | serialOut: *serialOut, 84 | diskImageFile: diskImageFile, 85 | }, nil 86 | } 87 | 88 | func clamp(x, min, max int) int { 89 | if x < min { 90 | return min 91 | } 92 | if x > max { 93 | return max 94 | } 95 | return x 96 | } 97 | -------------------------------------------------------------------------------- /cmd/ob2unix/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command ob2unix converts the content of Oberon texts (.Text, .Mod, .Tool) 6 | // to plain text with Unix-style line endings. 7 | // 8 | // - Drops the file header 9 | // - Converts line endings from CR to LF 10 | // - Replaces each tab character by two spaces 11 | // - Removes all other control characters 12 | // - Converts extended Latin characters with diacritics to UTF-8 13 | // 14 | // Usage: 15 | // 16 | // ob2unix [oberon_text_file] 17 | // 18 | // If no file is specified the command reads its input from the standard input. 19 | // The converted plain text is written to the standard output. 20 | // If the input is not an Oberon text it is written to the output unchanged. 21 | package main 22 | 23 | import ( 24 | "bufio" 25 | "flag" 26 | "fmt" 27 | "io" 28 | "os" 29 | ) 30 | 31 | func usage() { 32 | fail(`Converts the content of Oberon texts (.Text, .Mod, .Tool) to plain text 33 | with Unix-style line endings. 34 | 35 | - Drops the file header 36 | - Converts line endings from CR to LF 37 | - Replaces each tab character by two spaces 38 | - Removes all other control characters 39 | - Converts extended Latin characters with diacritics to UTF-8 40 | 41 | Usage: 42 | ob2unix [oberon_text_file] 43 | 44 | If no file is specified the command reads its input from the standard input. 45 | The converted plain text is written to the standard output. 46 | If the input is not an Oberon text it is written to the output unchanged.`) 47 | } 48 | 49 | var charMapping = [...]rune{ 50 | 'Ä', 'Ö', 'Ü', 51 | 'ä', 'ö', 'ü', 52 | 'â', 'ê', 'î', 'ô', 'û', 53 | 'à', 'è', 'ì', 'ò', 'ù', 54 | 'é', 'ë', 'ï', 'ç', 'á', 'ñ', 55 | } 56 | 57 | func main() { 58 | var err error 59 | 60 | flag.Usage = usage 61 | flag.Parse() 62 | 63 | in := os.Stdin 64 | if flag.NArg() > 0 { 65 | in, err = os.Open(flag.Arg(0)) 66 | check(err) 67 | defer in.Close() 68 | } 69 | out := bufio.NewWriter(os.Stdout) 70 | 71 | p, err := io.ReadAll(in) 72 | check(err) 73 | 74 | if !isOberon(p) { 75 | // Not an Oberon text file: copy input to output 76 | _, err = out.Write(p) 77 | check(err) 78 | err = out.Flush() 79 | check(err) 80 | return 81 | } 82 | 83 | p = skipHeader(p) 84 | for _, b := range p { 85 | switch { 86 | case b == '\r', b == '\n': 87 | err = out.WriteByte('\n') 88 | case b == '\t': 89 | _, err = out.Write([]byte(" ")) 90 | case b < 32: 91 | continue 92 | case 0x80 <= b && b <= 0x95: 93 | _, err = out.WriteRune(charMapping[b-0x80]) 94 | case b == 0xAB: 95 | _, err = out.WriteRune('ß') 96 | default: 97 | err = out.WriteByte(b) 98 | } 99 | check(err) 100 | } 101 | err = out.Flush() 102 | check(err) 103 | } 104 | 105 | func isOberon(p []byte) bool { 106 | return len(p) >= 6 && (p[0] == 240 && p[1] == 1) || (p[0] == 1 && p[1] == 240) 107 | } 108 | 109 | func skipHeader(p []byte) []byte { 110 | headerSize := uint64(p[2]) | uint64(p[3])<<8 | uint64(p[4])<<16 | uint64(p[5])<<24 111 | return p[headerSize:] 112 | } 113 | 114 | func check(err error) { 115 | if err != nil { 116 | fail(err) 117 | } 118 | } 119 | 120 | func fail(message any) { 121 | _, _ = fmt.Fprintln(os.Stderr, message) 122 | os.Exit(1) 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Oberon RISC Emulator 2 | 3 | This is an emulator for the 4 | [Project Oberon (Revised Edition 2013)](https://people.inf.ethz.ch/wirth/ProjectOberon/index.html) 5 | RISC processor, 6 | written in the [Go programming language](https://go.dev). 7 | It is a port of 8 | [Peter De Wachter's C-based emulator](https://github.com/pdewacht/oberon-risc-emu) 9 | to Go. 10 | 11 | Project Oberon is a design for a complete desktop computer system from scratch, 12 | created by [Niklaus Wirth](https://people.inf.ethz.ch/wirth/) 13 | and [Jürg Gutknecht](https://en.wikipedia.org/wiki/J%C3%BCrg_Gutknecht). 14 | Its simplicity and clarity enable a single person 15 | to understand and implement the entire system, 16 | making Project Oberon an excellent educational tool. 17 | It consists of: 18 | - A RISC CPU design. 19 | - A programming language with a compiler written in itself. 20 | - An operating system with a text-oriented, 21 | mouse-controlled graphical user interface, 22 | written in the Oberon programming language. 23 | 24 | If you find this project interesting, 25 | you should also explore 26 | [this Oberon compiler in Go](https://github.com/fzipp/oberon-compiler), 27 | which is a direct port of Wirth's compiler for the RISC architecture 28 | from Oberon to the Go programming language. 29 | Additionally, you can still use the original compiler 30 | within the emulator itself. 31 | 32 | ## Install 33 | 34 | ``` 35 | $ go install github.com/fzipp/oberon/cmd/oberon-emu@latest 36 | ``` 37 | 38 | ## Run 39 | 40 | First, download an Oberon disk image (.dsk file), e.g. from 41 | [this repository](https://github.com/pdewacht/oberon-risc-emu/tree/master/DiskImage). 42 | 43 | Next, initiate the emulator by providing the disk image file 44 | as a command-line argument: 45 | 46 | ``` 47 | $ oberon-emu Oberon-2020-08-18.dsk 48 | Visit http://localhost:8080 in a web browser 49 | ``` 50 | Open the following link in your web browser: http://localhost:8080. 51 | 52 | This is the Project Oberon user interface right after starting: 53 | 54 | ![Project Oberon](doc/screenshot1.png?raw=true "Project Oberon directly after start") 55 | 56 | ## Using Oberon 57 | 58 | [How to use the Oberon System](https://people.inf.ethz.ch/wirth/ProjectOberon/UsingOberon.pdf) (PDF) 59 | 60 | Oberon's user interface is designed for use with a three-button mouse, 61 | but the emulator also provides the option to 62 | simulate all three mouse buttons via the keyboard. 63 | 64 | | Mouse button | Function | Mac keyboard | PC keyboard | 65 | |--------------|--------------------|--------------|-------------| 66 | | Left | Set caret (cursor) | ⌃ control | Ctrl | 67 | | Middle | Execute command | ⌥ option | Alt | 68 | | Right | Select text | ⌘ command | Meta (Win) | 69 | 70 | 71 | | Key | Function | 72 | |-------|---------------------| 73 | | Esc | Undo all selections | 74 | | F1 | Set global marker | 75 | 76 | ## About the Oberon language 77 | 78 | Oberon is the latest programming language 79 | in Wirth's series of language designs, 80 | with predecessors like Pascal and Modula. 81 | Additionally, there are various versions and dialects 82 | of these three languages. 83 | Wirth's goal has always been 84 | to simplify the language whenever possible. 85 | 86 | Oberon's grammar is designed in a way that allows for 87 | implementation with a single-pass compiler. 88 | Oberon code is organized into modules 89 | that can be compiled separately. 90 | The language is statically typed 91 | and incorporates a garbage collector for memory management. 92 | Access to low-level and unsafe facilities is possible 93 | through a designated SYSTEM module. 94 | 95 | Furthermore, Oberon's influence extends to some aspects of Go, 96 | as Robert Griesemer, 97 | one of the original creators of Go, 98 | [explains in this GopherCon 2015 talk](https://www.youtube.com/watch?v=0ReKdcpNyQg&t=1070s) 99 | "The Evolution of Go". 100 | 101 | ## License 102 | 103 | This project is free and open source software licensed under the 104 | [ISC License](LICENSE). 105 | -------------------------------------------------------------------------------- /serial/pclink.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package serial 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | const ( 14 | recName = "PCLink.REC" // e.g. echo Test.Mod > PCLink.REC 15 | sndName = "PCLink.SND" 16 | ) 17 | 18 | const ( 19 | modeACK = 0x10 20 | modeREC = 0x21 21 | modeSND = 0x22 22 | ) 23 | 24 | type PCLink struct { 25 | txCount int 26 | rxCount int 27 | filename string 28 | filenameLen int 29 | mode uint32 30 | f *os.File 31 | fileLen int64 32 | buf [257]byte 33 | } 34 | 35 | func (p *PCLink) ReadStatus() uint32 { 36 | var status uint32 = 3 37 | if p.mode != 0 { 38 | return status 39 | } 40 | status = 2 41 | 42 | err := p.getJob(recName) 43 | if err == nil { 44 | info, err := os.Stat(p.filename) 45 | if err != nil { 46 | return status 47 | } 48 | defer cleanUp(recName, err) 49 | if info.Size() < 0 || info.Size() >= 0x1000000 { 50 | return status 51 | } 52 | p.f, err = os.Open(p.filename) 53 | if err != nil { 54 | return status 55 | } 56 | p.fileLen = info.Size() 57 | p.mode = modeREC 58 | fmt.Printf("PCLink REC Filename: %s size %d\n", p.filename, p.fileLen) 59 | return status 60 | } 61 | 62 | err = p.getJob(sndName) 63 | if err == nil { 64 | p.f, err = os.Create(p.filename) 65 | if err != nil { 66 | return status 67 | } 68 | defer cleanUp(sndName, err) 69 | p.fileLen = -1 70 | p.mode = modeSND 71 | fmt.Printf("PCLink SND Filename: %s\n", p.filename) 72 | return status 73 | } 74 | 75 | return status 76 | } 77 | 78 | func (p *PCLink) ReadData() uint32 { 79 | defer func() { p.rxCount++ }() 80 | if p.mode == 0 { 81 | return 0 82 | } 83 | var ch uint32 84 | if p.rxCount == 0 { 85 | ch = p.mode 86 | } else if p.rxCount < p.filenameLen { 87 | ch = uint32(p.filename[p.rxCount-1]) 88 | } else if p.rxCount == p.filenameLen { 89 | ch = 0 90 | } else if p.mode == modeSND { 91 | ch = uint32(modeACK) 92 | if p.fileLen == 0 { 93 | p.mode = 0 94 | cleanUp(sndName, nil) 95 | } 96 | } else { 97 | pos := (p.rxCount - p.filenameLen - 1) % 256 98 | if pos == 0 || p.fileLen == 0 { 99 | if p.fileLen > 255 { 100 | ch = 255 101 | } else { 102 | ch = uint32(byte(p.fileLen)) 103 | if p.fileLen == 0 { 104 | p.mode = 0 105 | cleanUp(recName, nil) 106 | } 107 | } 108 | } else { 109 | var buf [1]byte 110 | _, err := p.f.Read(buf[:]) 111 | if err != nil { 112 | _, _ = fmt.Fprintf(os.Stderr, "can't read from file: %s", err) 113 | } 114 | ch = uint32(buf[0]) 115 | p.fileLen-- 116 | } 117 | } 118 | return ch 119 | } 120 | 121 | func (p *PCLink) WriteData(value uint32) { 122 | defer func() { p.txCount++ }() 123 | if p.mode == 0 { 124 | return 125 | } 126 | if p.txCount == 0 { 127 | if value == uint32(modeACK) { 128 | return 129 | } 130 | p.f.Close() 131 | p.f = nil 132 | if p.mode == modeSND { 133 | cleanUp(p.filename, nil) // file not found, delete file created 134 | cleanUp(sndName, nil) 135 | } else { 136 | cleanUp(recName, nil) 137 | } 138 | p.mode = 0 139 | } else if p.mode == modeSND { 140 | var lim int 141 | pos := (p.txCount - 1) % 256 142 | p.buf[pos] = byte(value) 143 | lim = int(p.buf[0]) 144 | if pos == lim { 145 | _, err := p.f.Write(p.buf[1 : lim+1]) 146 | if err != nil { 147 | _, _ = fmt.Fprintf(os.Stderr, "can't write to file: %s", err) 148 | } 149 | if lim < 255 { 150 | p.fileLen = 0 151 | p.f.Close() 152 | } 153 | } 154 | } 155 | } 156 | 157 | func (p *PCLink) getJob(jobName string) error { 158 | info, err := os.Stat(jobName) 159 | if err != nil { 160 | return err 161 | } 162 | if info.Size() <= 0 || info.Size() > 33 { 163 | return errors.New("file size must be > 0 and <= 33 bytes") 164 | } 165 | f, err := os.Open(jobName) 166 | if err != nil { 167 | return cleanUp(jobName, err) 168 | } 169 | defer f.Close() 170 | _, err = fmt.Fscanf(f, "%31s", &p.filename) 171 | if err != nil { 172 | return err 173 | } 174 | p.txCount = 0 175 | p.rxCount = 0 176 | p.filenameLen = len(p.filename) + 1 177 | return nil 178 | } 179 | 180 | func cleanUp(filename string, err error) error { 181 | err2 := os.Remove(filename) 182 | if err2 != nil { 183 | return err2 184 | } 185 | return err 186 | } 187 | -------------------------------------------------------------------------------- /spi/disk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package spi 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os" 11 | ) 12 | 13 | type Disk struct { 14 | state diskState 15 | file *os.File 16 | offset uint32 17 | 18 | rxBuf [128]uint32 19 | rxIdx int 20 | 21 | txBuf [128 + 2]uint32 22 | txCnt int 23 | txIdx int 24 | } 25 | 26 | type diskState int 27 | 28 | const ( 29 | diskCommand diskState = iota 30 | diskRead 31 | diskWrite 32 | diskWriting 33 | ) 34 | 35 | func NewDisk(filename string) (*Disk, error) { 36 | disk := &Disk{ 37 | state: diskCommand, 38 | } 39 | 40 | if filename == "" { 41 | return disk, nil 42 | } 43 | 44 | var err error 45 | disk.file, err = os.OpenFile(filename, os.O_RDWR, 0o666) 46 | if err != nil { 47 | return nil, fmt.Errorf("can't open file \"%s\": %w", filename, err) 48 | } 49 | 50 | // Check for filesystem-only image, starting directly at sector 1 (DiskAdr 29) 51 | err = readSector(disk.file, disk.txBuf[:]) 52 | if err != nil { 53 | return nil, fmt.Errorf("can't read sector: %w", err) 54 | } 55 | if disk.txBuf[0] == 0x9B1EA38D { 56 | disk.offset = 0x80002 57 | } 58 | 59 | return disk, nil 60 | } 61 | 62 | func (d *Disk) WriteData(value uint32) { 63 | d.txIdx++ 64 | switch d.state { 65 | case diskCommand: 66 | if uint8(value) != 0xFF || d.rxIdx != 0 { 67 | d.rxBuf[d.rxIdx] = value 68 | d.rxIdx++ 69 | if d.rxIdx == 6 { 70 | err := d.runCommand() 71 | if err != nil { 72 | _, _ = fmt.Fprintf(os.Stderr, "can't run disk command: %s", err) 73 | } 74 | d.rxIdx = 0 75 | } 76 | } 77 | case diskRead: 78 | if d.txIdx == d.txCnt { 79 | d.state = diskCommand 80 | d.txCnt = 0 81 | d.txIdx = 0 82 | } 83 | case diskWrite: 84 | if value == 254 { 85 | d.state = diskWriting 86 | } 87 | case diskWriting: 88 | if d.rxIdx < 128 { 89 | d.rxBuf[d.rxIdx] = value 90 | } 91 | d.rxIdx++ 92 | if d.rxIdx == 128 { 93 | err := writeSector(d.file, d.rxBuf[:]) 94 | if err != nil { 95 | _, _ = fmt.Fprintf(os.Stderr, "can't write to disk: %s", err) 96 | } 97 | } 98 | if d.rxIdx == 130 { 99 | d.txBuf[0] = 5 100 | d.txCnt = 1 101 | d.txIdx = -1 102 | d.rxIdx = 0 103 | d.state = diskCommand 104 | } 105 | } 106 | } 107 | 108 | func (d *Disk) ReadData() uint32 { 109 | if d.txIdx >= 0 && d.txIdx < d.txCnt { 110 | return d.txBuf[d.txIdx] 111 | } 112 | return 255 113 | } 114 | 115 | func (d *Disk) runCommand() error { 116 | cmd := d.rxBuf[0] 117 | arg := d.rxBuf[1]<<24 | 118 | (d.rxBuf[2] << 16) | 119 | (d.rxBuf[3] << 8) | 120 | d.rxBuf[4] 121 | 122 | switch cmd { 123 | case 81: 124 | d.state = diskRead 125 | d.txBuf[0] = 0 126 | d.txBuf[1] = 254 127 | err := seekSector(d.file, arg-d.offset) 128 | if err != nil { 129 | return fmt.Errorf("can't seek sector for read: %w", err) 130 | } 131 | err = readSector(d.file, d.txBuf[2:]) 132 | if err != nil { 133 | return fmt.Errorf("can't read sector: %w", err) 134 | } 135 | d.txCnt = 2 + 128 136 | case 88: 137 | d.state = diskWrite 138 | err := seekSector(d.file, arg-d.offset) 139 | if err != nil { 140 | return fmt.Errorf("can't seek sector for write: %w", err) 141 | } 142 | d.txBuf[0] = 0 143 | d.txCnt = 1 144 | default: 145 | d.txBuf[0] = 0 146 | d.txCnt = 1 147 | } 148 | d.txIdx = -1 149 | return nil 150 | } 151 | 152 | func seekSector(s io.Seeker, secnum uint32) error { 153 | _, err := s.Seek(int64(secnum)*512, io.SeekStart) 154 | if err != nil { 155 | return fmt.Errorf("can't seek sector %d: %w", secnum, err) 156 | } 157 | return nil 158 | } 159 | 160 | func readSector(r io.Reader, buf []uint32) error { 161 | var bytes [512]byte 162 | _, err := r.Read(bytes[:]) 163 | if err != nil && err != io.EOF { 164 | return fmt.Errorf("can't read bytes: %w", err) 165 | } 166 | for i := range 128 { 167 | buf[i] = uint32(bytes[i*4+0]) | 168 | (uint32(bytes[i*4+1]) << 8) | 169 | (uint32(bytes[i*4+2]) << 16) | 170 | (uint32(bytes[i*4+3]) << 24) 171 | } 172 | return nil 173 | } 174 | 175 | func writeSector(w io.Writer, buf []uint32) error { 176 | var bytes [512]byte 177 | for i := range 128 { 178 | bytes[i*4+0] = uint8(buf[i]) 179 | bytes[i*4+1] = uint8(buf[i] >> 8) 180 | bytes[i*4+2] = uint8(buf[i] >> 16) 181 | bytes[i*4+3] = uint8(buf[i] >> 24) 182 | } 183 | _, err := w.Write(bytes[:]) 184 | if err != nil { 185 | return fmt.Errorf("can write bytes: %w", err) 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/serve.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "html/template" 7 | "image" 8 | "image/color" 9 | "log" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | ) 16 | 17 | var ( 18 | //go:embed web/canvas-websocket.js 19 | javaScriptCode []byte 20 | 21 | //go:embed web/index.html.tmpl 22 | indexHTMLCode string 23 | indexHTMLTemplate = template.Must(template.New("index.html.tmpl").Parse(indexHTMLCode)) 24 | ) 25 | 26 | func ListenAndServe(addr string, run func(*Context), size image.Rectangle) error { 27 | config := config{ 28 | title: "Project Oberon RISC Emulator", 29 | width: size.Dx(), 30 | height: size.Dy(), 31 | backgroundColor: color.Black, 32 | eventMask: maskMouseMove | maskMouseDown | maskMouseUp | maskKeyDown | maskKeyUp | maskTouchMove | maskClipboardChange, 33 | cursorDisabled: true, 34 | contextMenuDisabled: true, 35 | } 36 | return http.ListenAndServe(addr, newServeMux(run, config)) 37 | } 38 | 39 | func newServeMux(run func(*Context), config config) *http.ServeMux { 40 | mux := http.NewServeMux() 41 | mux.Handle("GET /", &htmlHandler{ 42 | config: config, 43 | }) 44 | mux.HandleFunc("GET /canvas-websocket.js", javaScriptHandler) 45 | mux.Handle("GET /draw", &drawHandler{ 46 | config: config, 47 | draw: run, 48 | }) 49 | return mux 50 | } 51 | 52 | type htmlHandler struct { 53 | config config 54 | } 55 | 56 | func (h *htmlHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) { 57 | model := map[string]any{ 58 | "DrawURL": template.URL("draw"), 59 | "Width": h.config.width, 60 | "Height": h.config.height, 61 | "Title": h.config.title, 62 | "BackgroundColor": template.CSS(rgbaString(h.config.backgroundColor)), 63 | "EventMask": h.config.eventMask, 64 | "CursorDisabled": h.config.cursorDisabled, 65 | "ContextMenuDisabled": h.config.contextMenuDisabled, 66 | "FullPage": h.config.fullPage, 67 | "ReconnectInterval": int64(h.config.reconnectInterval / time.Millisecond), 68 | } 69 | err := indexHTMLTemplate.Execute(w, model) 70 | if err != nil { 71 | log.Println(err) 72 | return 73 | } 74 | } 75 | 76 | func rgbaString(c color.Color) string { 77 | clr := color.RGBAModel.Convert(c).(color.RGBA) 78 | return fmt.Sprintf("rgba(%d, %d, %d, %g)", clr.R, clr.G, clr.B, float64(clr.A)/255) 79 | } 80 | 81 | func javaScriptHandler(w http.ResponseWriter, _ *http.Request) { 82 | w.Header().Add("Content-Type", "text/javascript") 83 | _, err := w.Write(javaScriptCode) 84 | if err != nil { 85 | log.Println(err) 86 | return 87 | } 88 | } 89 | 90 | var upgrader = websocket.Upgrader{ 91 | ReadBufferSize: 1024, 92 | WriteBufferSize: 1024, 93 | } 94 | 95 | type drawHandler struct { 96 | config config 97 | draw func(*Context) 98 | } 99 | 100 | func (h *drawHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 101 | conn, err := upgrader.Upgrade(w, r, nil) 102 | if err != nil { 103 | log.Println(err) 104 | return 105 | } 106 | 107 | events := make(chan Event) 108 | defer close(events) 109 | draws := make(chan []byte) 110 | defer close(draws) 111 | 112 | wg := sync.WaitGroup{} 113 | wg.Add(2) 114 | go readMessages(conn, events, &wg) 115 | go writeMessages(conn, draws, &wg) 116 | 117 | ctx := newContext(draws, events, h.config) 118 | go func() { 119 | defer wg.Done() 120 | h.draw(ctx) 121 | }() 122 | 123 | wg.Wait() 124 | wg.Add(1) 125 | events <- CloseEvent{} 126 | wg.Wait() 127 | } 128 | 129 | func writeMessages(conn *websocket.Conn, messages <-chan []byte, wg *sync.WaitGroup) { 130 | defer wg.Done() 131 | for { 132 | message := <-messages 133 | err := conn.WriteMessage(websocket.BinaryMessage, message) 134 | if err != nil { 135 | break 136 | } 137 | } 138 | } 139 | 140 | func readMessages(conn *websocket.Conn, events chan<- Event, wg *sync.WaitGroup) { 141 | defer wg.Done() 142 | for { 143 | messageType, p, err := conn.ReadMessage() 144 | if err != nil { 145 | break 146 | } 147 | if messageType != websocket.BinaryMessage { 148 | continue 149 | } 150 | event, err := decodeEvent(p) 151 | if err != nil { 152 | continue 153 | } 154 | events <- event 155 | } 156 | } 157 | 158 | type config struct { 159 | title string 160 | width int 161 | height int 162 | backgroundColor color.Color 163 | eventMask eventMask 164 | cursorDisabled bool 165 | contextMenuDisabled bool 166 | fullPage bool 167 | reconnectInterval time.Duration 168 | } 169 | -------------------------------------------------------------------------------- /risc/internal/fp/fp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Package fp implements floating-point arithmetic operations for the 6 | // Oberon RISC emulator. 7 | // 8 | // See https://people.inf.ethz.ch/wirth/ProjectOberon/PO.Computer.pdf 9 | // section 16.3. "Floating-point arithmetic". 10 | package fp 11 | 12 | func Add(x, y uint32, u, v bool) uint32 { 13 | xs := (x & 0x80000000) != 0 14 | var xe uint32 15 | var x0 int32 16 | if !u { 17 | xe = (x >> 23) & 0xFF 18 | xm := ((x & 0x7FFFFF) << 1) | 0x1000000 19 | if xs { 20 | x0 = int32(-xm) 21 | } else { 22 | x0 = int32(xm) 23 | } 24 | } else { 25 | xe = 150 26 | x0 = int32(x&0x00FFFFFF) << 8 >> 7 27 | } 28 | 29 | ys := (y & 0x80000000) != 0 30 | ye := (y >> 23) & 0xFF 31 | ym := (y & 0x7FFFFF) << 1 32 | if !u && !v { 33 | ym |= 0x1000000 34 | } 35 | var y0 int32 36 | if ys { 37 | y0 = int32(-ym) 38 | } else { 39 | y0 = int32(ym) 40 | } 41 | 42 | var e0 uint32 43 | var x3, y3 int32 44 | if ye > xe { 45 | shift := ye - xe 46 | e0 = ye 47 | if shift > 31 { 48 | x3 = x0 >> 31 49 | } else { 50 | x3 = x0 >> shift 51 | } 52 | y3 = y0 53 | } else { 54 | shift := xe - ye 55 | e0 = xe 56 | x3 = x0 57 | if shift > 31 { 58 | y3 = y0 >> 31 59 | } else { 60 | y3 = y0 >> shift 61 | } 62 | } 63 | 64 | xs_ := b2i(xs) 65 | ys_ := b2i(ys) 66 | sum := ((xs_ << 26) | (xs_ << 25) | uint32(x3&0x01FFFFFF)) + 67 | ((ys_ << 26) | (ys_ << 25) | uint32(y3&0x01FFFFFF)) 68 | 69 | var s uint32 70 | if (sum & (1 << 26)) > 0 { 71 | s = -sum 72 | } else { 73 | s = sum 74 | } 75 | s = (s + 1) & 0x07FFFFFF 76 | 77 | e1 := e0 + 1 78 | t3 := s >> 1 79 | if (s & 0x3FFFFFC) != 0 { 80 | for (t3 & (1 << 24)) == 0 { 81 | t3 <<= 1 82 | e1-- 83 | } 84 | } else { 85 | t3 <<= 24 86 | e1 -= 24 87 | } 88 | 89 | xn := (x & 0x7FFFFFFF) == 0 90 | yn := (y & 0x7FFFFFFF) == 0 91 | 92 | if v { 93 | return uint32(int32(sum<<5) >> 6) 94 | } else if xn { 95 | if u || yn { 96 | return 0 97 | } 98 | return y 99 | } else if yn { 100 | return x 101 | } else if (t3&0x01FFFFFF) == 0 || (e1&0x100) != 0 { 102 | return 0 103 | } else { 104 | return ((sum & 0x04000000) << 5) | (e1 << 23) | ((t3 >> 1) & 0x7FFFFF) 105 | } 106 | } 107 | 108 | func Mul(x, y uint32) uint32 { 109 | sign := (x ^ y) & 0x80000000 110 | xe := (x >> 23) & 0xFF 111 | ye := (y >> 23) & 0xFF 112 | 113 | xm := (x & 0x7FFFFF) | 0x800000 114 | ym := (y & 0x7FFFFF) | 0x800000 115 | m := uint64(xm) * uint64(ym) 116 | 117 | e1 := (xe + ye) - 127 118 | var z0 uint32 119 | if (m & (1 << 47)) != 0 { 120 | e1++ 121 | z0 = uint32(((m >> 23) + 1) & 0xFFFFFF) 122 | } else { 123 | z0 = uint32(((m >> 22) + 1) & 0xFFFFFF) 124 | } 125 | 126 | if xe == 0 || ye == 0 { 127 | return 0 128 | } else if (e1 & 0x100) == 0 { 129 | return sign | ((e1 & 0xFF) << 23) | (z0 >> 1) 130 | } else if (e1 & 0x80) == 0 { 131 | return sign | (0xFF << 23) | (z0 >> 1) 132 | } else { 133 | return 0 134 | } 135 | } 136 | 137 | func Div(x, y uint32) uint32 { 138 | sign := (x ^ y) & 0x80000000 139 | xe := (x >> 23) & 0xFF 140 | ye := (y >> 23) & 0xFF 141 | 142 | xm := (x & 0x7FFFFF) | 0x800000 143 | ym := (y & 0x7FFFFF) | 0x800000 144 | q1 := uint32(uint64(xm) * (1 << 25) / uint64(ym)) 145 | 146 | e1 := (xe - ye) + 126 147 | var q2 uint32 148 | if (q1 & (1 << 25)) != 0 { 149 | e1++ 150 | q2 = (q1 >> 1) & 0xFFFFFF 151 | } else { 152 | q2 = q1 & 0xFFFFFF 153 | } 154 | q3 := q2 + 1 155 | 156 | if xe == 0 { 157 | return 0 158 | } else if ye == 0 { 159 | return sign | (0xFF << 23) 160 | } else if (e1 & 0x100) == 0 { 161 | return sign | ((e1 & 0xFF) << 23) | (q3 >> 1) 162 | } else if (e1 & 0x80) == 0 { 163 | return sign | (0xFF << 23) | (q2 >> 1) 164 | } else { 165 | return 0 166 | } 167 | } 168 | 169 | type IdivResult struct { 170 | Quot, Rem uint32 171 | } 172 | 173 | func Idiv(x, y uint32, signedDiv bool) IdivResult { 174 | sign := (int32(x) < 0) && signedDiv 175 | var x0 uint32 176 | if sign { 177 | x0 = -x 178 | } else { 179 | x0 = x 180 | } 181 | 182 | RQ := uint64(x0) 183 | for range 32 { 184 | w0 := uint32(RQ >> 31) 185 | w1 := w0 - y 186 | if int32(w1) < 0 { 187 | RQ = (uint64(w0) << 32) | ((RQ & 0x7FFFFFFF) << 1) 188 | } else { 189 | RQ = (uint64(w1) << 32) | ((RQ & 0x7FFFFFFF) << 1) | 1 190 | } 191 | } 192 | 193 | d := IdivResult{Quot: uint32(RQ), Rem: uint32(RQ >> 32)} 194 | if sign { 195 | d.Quot = -d.Quot 196 | if d.Rem > 0 { 197 | d.Quot-- 198 | d.Rem = y - d.Rem 199 | } 200 | } 201 | return d 202 | } 203 | 204 | func b2i(b bool) uint32 { 205 | if b { 206 | return 1 207 | } 208 | return 0 209 | } 210 | -------------------------------------------------------------------------------- /risc/boot.go: -------------------------------------------------------------------------------- 1 | package risc 2 | 3 | var bootloader = [romWords]uint32{ 4 | 0xE7000151, 0x00000000, 0x00000000, 0x00000000, 5 | 0x00000000, 0x00000000, 0x00000000, 0x00000000, 6 | 0x4EE90014, 0xAFE00000, 0xA0E00004, 0x40000000, 7 | 0xA0E00008, 0x40000004, 0xA0E00010, 0x80E00010, 8 | 0x40090001, 0xA0E00010, 0x5000FFCC, 0x80000000, 9 | 0x40030001, 0xE8FFFFFC, 0x5000FFC8, 0x80000000, 10 | 0xA0E0000C, 0x80E00008, 0x81E0000C, 0x00080001, 11 | 0x40030008, 0xA0E00008, 0x80E00010, 0xE9FFFFEF, 12 | 0x80E00008, 0x81E00004, 0xA0100000, 0x8FE00000, 13 | 0x4EE80014, 0xC700000F, 0x4EE90010, 0xAFE00000, 14 | 0x40E80004, 0xF7FFFFDE, 0x80E00004, 0x40090000, 15 | 0xE6000012, 0x40E80008, 0xF7FFFFD9, 0x40E8000C, 16 | 0xF7FFFFD7, 0x80E00008, 0x81E0000C, 0xA1000000, 17 | 0x80E00008, 0x40080004, 0xA0E00008, 0x80E00004, 18 | 0x40090004, 0xA0E00004, 0x80E00004, 0xE9FFFFF3, 19 | 0x40E80004, 0xF7FFFFCA, 0xE7FFFFEB, 0x8FE00000, 20 | 0x4EE80010, 0xC700000F, 0x4EE90008, 0xAFE00000, 21 | 0xA0E00004, 0x5000FFD4, 0x41000000, 0xA1000000, 22 | 0x80E00004, 0x40090000, 0xE600000B, 0x80E00004, 23 | 0x40090001, 0xA0E00004, 0x5000FFD0, 0x5100FFFF, 24 | 0xA1000000, 0x5000FFD4, 0x80000000, 0x40030001, 25 | 0xE8FFFFFC, 0xE7FFFFF2, 0x8FE00000, 0x4EE80008, 26 | 0xC700000F, 0x4EE90008, 0xAFE00000, 0xA0E00004, 27 | 0x5000FFD4, 0x41000001, 0xA1000000, 0x5000FFD0, 28 | 0x81E00004, 0xA1000000, 0x5000FFD4, 0x80000000, 29 | 0x40030001, 0xE8FFFFFC, 0x8FE00000, 0x4EE80008, 30 | 0xC700000F, 0x4EE90018, 0xAFE00000, 0xA0E00004, 31 | 0xA1E00008, 0x40000001, 0xF7FFFFD3, 0x5000FFD0, 32 | 0x80000000, 0xA0E00010, 0x80E00010, 0x400900FF, 33 | 0xE9FFFFF8, 0x400000FF, 0xF7FFFFE2, 0x5000FFD0, 34 | 0x80000000, 0xA0E00010, 0x80E00010, 0x400900FF, 35 | 0xE9FFFFF8, 0x80E00004, 0x40090008, 0xE9000003, 36 | 0x40000087, 0xA0E00014, 0xE7000007, 0x80E00004, 37 | 0xE9000003, 0x40000095, 0xA0E00014, 0xE7000002, 38 | 0x400000FF, 0xA0E00014, 0x80E00004, 0x4004003F, 39 | 0x40080040, 0xF7FFFFCB, 0x40000018, 0x41090000, 40 | 0xE5000008, 0xA0E0000C, 0x80E00008, 0x81E0000C, 41 | 0x00030001, 0xF7FFFFC3, 0x80E0000C, 0x5008FFF8, 42 | 0xE7FFFFF6, 0x80E00014, 0xF7FFFFBE, 0x40000020, 43 | 0xA0E0000C, 0x400000FF, 0xF7FFFFBA, 0x5000FFD0, 44 | 0x80000000, 0xA0E00010, 0x80E0000C, 0x40090001, 45 | 0xA0E0000C, 0x80E00010, 0x40090080, 0xE5000002, 46 | 0x80E0000C, 0xE9FFFFF3, 0x8FE00000, 0x4EE80018, 47 | 0xC700000F, 0x4EE9000C, 0xAFE00000, 0x40000009, 48 | 0xF7FFFF91, 0x40000000, 0x41000000, 0xF7FFFFB5, 49 | 0x40000008, 0x410001AA, 0xF7FFFFB2, 0x5000FFFF, 50 | 0xF7FFFFA0, 0x5000FFFF, 0xF7FFFF9E, 0x5000FFFF, 51 | 0xF7FFFF9C, 0x40000037, 0x41000000, 0xF7FFFFA9, 52 | 0x40000029, 0x41000001, 0x4111001E, 0xF7FFFFA5, 53 | 0x5000FFD0, 0x80000000, 0xA0E00004, 0x5000FFFF, 54 | 0xF7FFFF90, 0x5000FFFF, 0xF7FFFF8E, 0x5000FFFF, 55 | 0xF7FFFF8C, 0x40002710, 0xF7FFFF73, 0x80E00004, 56 | 0xE9FFFFEC, 0x40000010, 0x41000200, 0xF7FFFF95, 57 | 0x40000001, 0xF7FFFF6C, 0x8FE00000, 0x4EE8000C, 58 | 0xC700000F, 0x4EE9000C, 0xAFE00000, 0xA0E00004, 59 | 0x4000003A, 0x41000000, 0xF7FFFF8A, 0x5000FFD0, 60 | 0x80000000, 0xA0E00008, 0x5000FFFF, 0xF7FFFF75, 61 | 0x80E00008, 0xE9000004, 0x5000FFD0, 0x80000000, 62 | 0x40030007, 0xE0000005, 0x80E00004, 0x80000000, 63 | 0x40010009, 0x81E00004, 0xA0100000, 0x5000FFFF, 64 | 0xF7FFFF68, 0x5000FFFF, 0xF7FFFF66, 0x40000001, 65 | 0xF7FFFF4D, 0x8FE00000, 0x4EE8000C, 0xC700000F, 66 | 0x4EE90014, 0xAFE00000, 0xA0E00004, 0xA1E00008, 67 | 0x40E80004, 0xF7FFFFDB, 0x40000011, 0x81E00004, 68 | 0xF7FFFF68, 0x40000000, 0xA0E0000C, 0x5000FFFF, 69 | 0xF7FFFF54, 0x5000FFD0, 0x80000000, 0xA0E00010, 70 | 0x80E0000C, 0x40080001, 0xA0E0000C, 0x80E00010, 71 | 0x400900FE, 0xE9FFFFF5, 0x5000FFD4, 0x41000005, 72 | 0xA1000000, 0x40000000, 0x410901FC, 0xEE000014, 73 | 0xA0E0000C, 0x5000FFD0, 0x5100FFFF, 0xA1000000, 74 | 0x5000FFD4, 0x80000000, 0x40030001, 0xE8FFFFFC, 75 | 0x5000FFD0, 0x80000000, 0xA0E00010, 0x80E00008, 76 | 0x81E00010, 0xA1000000, 0x80E00008, 0x40080004, 77 | 0xA0E00008, 0x80E0000C, 0x40080004, 0xE7FFFFEA, 78 | 0x400000FF, 0xF7FFFF2F, 0x400000FF, 0xF7FFFF2D, 79 | 0x40000001, 0xF7FFFF14, 0x8FE00000, 0x4EE80014, 80 | 0xC700000F, 0x4EE90014, 0xAFE00000, 0x60000008, 81 | 0x40060004, 0xA0E00004, 0x80E00004, 0x41000000, 82 | 0xF7FFFFBF, 0x40000010, 0x80000000, 0xA0E00010, 83 | 0x80E00004, 0x40080001, 0xA0E00004, 0x40000200, 84 | 0xA0E00008, 0x80E00008, 0x81E00010, 0x00090001, 85 | 0xED00000A, 0x80E00004, 0x81E00008, 0xF7FFFFB0, 86 | 0x80E00004, 0x40080001, 0xA0E00004, 0x80E00008, 87 | 0x40080200, 0xA0E00008, 0xE7FFFFF2, 0x8FE00000, 88 | 0x4EE80014, 0xC700000F, 0x4D000000, 0x5E00FFC0, 89 | 0x6E000008, 0x4C000020, 0x0000000F, 0x40090000, 90 | 0xE9000012, 0x40000080, 0x5100FFC4, 0xA0100000, 91 | 0xF7FFFF50, 0x5000FFC4, 0x80000000, 0x40030001, 92 | 0xE8000005, 0x40000081, 0x5100FFC4, 0xA0100000, 93 | 0xF7FFFEC1, 0xE7000004, 0x40000082, 0x5100FFC4, 94 | 0xA0100000, 0xF7FFFFC7, 0xE7000008, 0x5000FFC4, 95 | 0x80000000, 0x40030001, 0xE8000004, 0x40000081, 96 | 0x5100FFC4, 0xA0100000, 0xF7FFFEB3, 0x4000000C, 97 | 0x6100000E, 0x41167EF0, 0xA1000000, 0x40000018, 98 | 0x61000008, 0xA1000000, 0x40000084, 0x5100FFC4, 99 | 0xA0100000, 0x40000000, 0xC7000000, 100 | } 101 | -------------------------------------------------------------------------------- /cmd/asciidecoder/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command asciidecoder extracts the files from an Oberon AsciiCoder archive, 6 | // like the Oberon command AsciiCoder.DecodeFiles would. 7 | // 8 | // Usage: 9 | // asciidecoder [-v] [-C dir] [archive_file] 10 | // 11 | // Flags: 12 | // -v Verbose output: prints the name of each extracted file. 13 | // -C Output directory, created if it does not exist yet. 14 | package main 15 | 16 | import ( 17 | "bufio" 18 | "bytes" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | ) 26 | 27 | func usage() { 28 | fail("Usage: asciidecoder [-v] [-C dir] [file]") 29 | } 30 | 31 | func main() { 32 | var err error 33 | 34 | verbose := flag.Bool("v", false, "verbose output: prints the name of each extracted file") 35 | directory := flag.String("C", "", "output `directory`, created if it does not exist yet") 36 | flag.Usage = usage 37 | flag.Parse() 38 | 39 | in := os.Stdin 40 | if flag.NArg() > 0 { 41 | in, err = os.Open(flag.Arg(0)) 42 | check(err) 43 | defer in.Close() 44 | } 45 | 46 | if *directory != "" { 47 | err = os.MkdirAll(*directory, os.ModePerm) 48 | check(err) 49 | } 50 | 51 | r := bufio.NewReader(in) 52 | filenames, compressed, err := readHeader(r) 53 | check(err) 54 | 55 | for _, filename := range filenames { 56 | if *verbose { 57 | fmt.Println(filename) 58 | } 59 | path := filename 60 | if *directory != "" { 61 | path = filepath.Join(*directory, filename) 62 | } 63 | err = extractFile(r, path, compressed) 64 | check(err) 65 | } 66 | } 67 | 68 | var errInvalidArchive = errors.New("not an AsciiCoder.DecodeFiles archive") 69 | 70 | func readHeader(r *bufio.Reader) (filenames []string, compressed bool, err error) { 71 | command, err := r.ReadString('~') 72 | if err != nil { 73 | return nil, false, errInvalidArchive 74 | } 75 | args := strings.Fields(command) 76 | if len(args) < 1 || args[0] != "AsciiCoder.DecodeFiles" { 77 | return nil, false, errInvalidArchive 78 | } 79 | for _, arg := range args[1:] { 80 | if arg == "%" { 81 | compressed = true 82 | continue 83 | } 84 | if arg == "~" { 85 | break 86 | } 87 | filenames = append(filenames, arg) 88 | } 89 | return filenames, compressed, nil 90 | } 91 | 92 | func extractFile(r *bufio.Reader, path string, compressed bool) error { 93 | data, err := decode(r) 94 | if err != nil { 95 | return fmt.Errorf("could not decode file: %w", err) 96 | } 97 | if compressed { 98 | data, err = decompress(bufio.NewReader(bytes.NewBuffer(data))) 99 | if err != nil { 100 | return fmt.Errorf("could not decompress file: %w", err) 101 | } 102 | } 103 | err = os.WriteFile(path, data, 0o666) 104 | if err != nil { 105 | return fmt.Errorf("could not write file: %w", err) 106 | } 107 | return nil 108 | } 109 | 110 | func decode(r *bufio.Reader) (data []byte, err error) { 111 | const base = 48 112 | bits := 0 113 | var buf uint32 114 | for { 115 | b, err := r.ReadByte() 116 | if err != nil { 117 | return nil, err 118 | } 119 | if b <= 32 { 120 | continue 121 | } 122 | if base <= b && b < (base+64) { 123 | buf |= uint32(b-base) << bits 124 | bits += 6 125 | if bits >= 8 { 126 | data = append(data, byte(buf&0xFF)) 127 | buf >>= 8 128 | bits -= 8 129 | } 130 | continue 131 | } 132 | if (b == '#' && bits == 0) || (b == '%' && bits == 2) || (b == '$' && bits == 4) { 133 | return data, nil 134 | } 135 | return nil, nil 136 | } 137 | } 138 | 139 | func decompress(r *bufio.Reader) ([]byte, error) { 140 | size, err := readNumber(r) 141 | if err != nil { 142 | return nil, fmt.Errorf("could not read size: %w", err) 143 | } 144 | if size < 0 { 145 | return nil, fmt.Errorf("negative size") 146 | } 147 | 148 | const N = 16384 149 | var table [N]byte 150 | var vec []byte 151 | hash := 0 152 | var buf uint32 153 | bits := 0 154 | 155 | for range size { 156 | if bits == 0 { 157 | b, err := r.ReadByte() 158 | if err != nil { 159 | return nil, err 160 | } 161 | buf = uint32(b) 162 | bits = 8 163 | } 164 | 165 | misprediction := (buf & 1) != 0 166 | buf >>= 1 167 | bits-- 168 | 169 | var data byte 170 | if !misprediction { 171 | data = table[hash] 172 | } else { 173 | b, err := r.ReadByte() 174 | if err != nil { 175 | return nil, err 176 | } 177 | buf |= uint32(b) << bits 178 | data = byte(buf & 0xFF) 179 | buf >>= 8 180 | table[hash] = data 181 | } 182 | vec = append(vec, data) 183 | hash = (16*hash + int(data)) % N 184 | } 185 | return vec, nil 186 | } 187 | 188 | func readNumber(r *bufio.Reader) (int, error) { 189 | var n int 190 | bits := 0 191 | for { 192 | b, err := r.ReadByte() 193 | if err != nil { 194 | return 0, err 195 | } 196 | if b >= 0x80 { 197 | n |= int(b-0x80) << bits 198 | bits += 7 199 | if bits >= 32 { 200 | return 0, errors.New("invalid bits") 201 | } 202 | } else { 203 | n |= ((int(b) ^ 0x40) - 0x40) << bits 204 | return n, nil 205 | } 206 | } 207 | } 208 | 209 | func check(err error) { 210 | if err != nil { 211 | fail(err) 212 | } 213 | } 214 | 215 | func fail(message any) { 216 | _, _ = fmt.Fprintln(os.Stderr, message) 217 | os.Exit(1) 218 | } 219 | -------------------------------------------------------------------------------- /cmd/oberon-emu/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command oberon-emu is an emulator for the Project Oberon RISC machine. 6 | // It starts a WebSocket server to render the screen in a web browser on an 7 | // HTML canvas. 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "flag" 13 | "fmt" 14 | "log" 15 | "os" 16 | "os/exec" 17 | "runtime" 18 | "time" 19 | 20 | "github.com/fzipp/oberon/risc" 21 | "github.com/fzipp/oberon/serial" 22 | "github.com/fzipp/oberon/spi" 23 | 24 | "github.com/fzipp/oberon/cmd/oberon-emu/internal/canvas" 25 | ) 26 | 27 | const ( 28 | cpuHz = 25000000 29 | fps = 60 30 | ) 31 | 32 | func main() { 33 | opt, err := optionsFromFlags() 34 | if err != nil { 35 | flag.Usage() 36 | os.Exit(1) 37 | } 38 | 39 | url := httpLink(opt.http) 40 | if opt.open && startBrowser(url) { 41 | fmt.Println("Listening on " + url) 42 | } else { 43 | fmt.Println("Visit " + url + " in a web browser") 44 | } 45 | 46 | err = canvas.ListenAndServe(opt.http, func(ctx *canvas.Context) { 47 | run(ctx, opt) 48 | }, opt.sizeRect) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | func run(ctx *canvas.Context, opt *options) { 55 | r := risc.New() 56 | r.SetSerial(&serial.PCLink{}) 57 | clipboard := &Clipboard{ctx: ctx} 58 | r.SetClipboard(clipboard) 59 | 60 | if opt.leds { 61 | r.SetLEDs(&ConsoleLEDs{}) 62 | } 63 | 64 | if opt.bootFromSerial { 65 | r.SetSwitches(1) 66 | } 67 | 68 | if opt.mem > 0 || opt.size != "" { 69 | r.ConfigureMemory(opt.mem, opt.sizeRect.Dx(), opt.sizeRect.Dy()) 70 | } 71 | 72 | disk, err := spi.NewDisk(opt.diskImageFile) 73 | if err != nil { 74 | _, _ = fmt.Fprintf(os.Stderr, "can't use disk image: %s", err) 75 | return 76 | } 77 | r.SetSPI(1, disk) 78 | 79 | if opt.serialIn != "" || opt.serialOut != "" { 80 | raw, err := serial.Open(opt.serialIn, opt.serialOut) 81 | if err != nil { 82 | _, _ = fmt.Fprintf(os.Stderr, "can't open serial I/O: %s", err) 83 | return 84 | } 85 | r.SetSerial(raw) 86 | } 87 | 88 | fb := r.Framebuffer() 89 | 90 | riscStart := getTicks() 91 | for { 92 | frameStart := getTicks() 93 | select { 94 | case event := <-ctx.Events(): 95 | if _, ok := event.(canvas.CloseEvent); ok { 96 | return 97 | } 98 | handleEvent(event, r, ctx, clipboard) 99 | default: 100 | r.SetTime(uint32(frameStart - riscStart)) 101 | err := r.Run(cpuHz / fps) 102 | if err != nil { 103 | var riscErr *risc.Error 104 | if errors.As(err, &riscErr) { 105 | _, _ = fmt.Fprintf(os.Stderr, "%s (PC=0x%08X)\n", riscErr, riscErr.PC) 106 | } else { 107 | _, _ = fmt.Fprintln(os.Stderr, err) 108 | } 109 | } 110 | 111 | ctx.UpdateDisplay(fb, r.GetFramebufferDamageAndReset()) 112 | 113 | frameEnd := getTicks() 114 | delay := frameStart + 1000/fps - frameEnd 115 | time.Sleep(time.Duration(delay) * time.Millisecond) 116 | } 117 | } 118 | } 119 | 120 | func handleEvent(e canvas.Event, r *risc.RISC, ctx *canvas.Context, clipboard *Clipboard) { 121 | switch ev := e.(type) { 122 | case canvas.MouseMoveEvent: 123 | r.MouseMoved(ev.X, ctx.CanvasHeight()-ev.Y) 124 | case canvas.MouseDownEvent: 125 | if ev.AltKey() { 126 | r.MouseButton(2, true) 127 | break 128 | } 129 | if ev.MetaKey() { 130 | r.MouseButton(3, true) 131 | break 132 | } 133 | if ev.Buttons&canvas.ButtonPrimary > 0 { 134 | r.MouseButton(1, true) 135 | } 136 | if ev.Buttons&canvas.ButtonAuxiliary > 0 { 137 | r.MouseButton(2, true) 138 | } 139 | if ev.Buttons&canvas.ButtonSecondary > 0 { 140 | r.MouseButton(3, true) 141 | } 142 | case canvas.MouseUpEvent: 143 | r.MouseButton(1, false) 144 | r.MouseButton(2, false) 145 | r.MouseButton(3, false) 146 | case canvas.KeyDownEvent: 147 | if ev.Key == "Control" { 148 | r.MouseButton(1, true) 149 | return 150 | } 151 | if ev.Key == "Alt" { 152 | r.MouseButton(2, true) 153 | return 154 | } 155 | if ev.Key == "Meta" { 156 | r.MouseButton(3, true) 157 | return 158 | } 159 | r.KeyboardInput(ps2Encode(ev.KeyboardEvent, true)) 160 | case canvas.KeyUpEvent: 161 | if ev.Key == "Control" { 162 | r.MouseButton(1, false) 163 | return 164 | } 165 | if ev.Key == "Alt" { 166 | r.MouseButton(2, false) 167 | return 168 | } 169 | if ev.Key == "Meta" { 170 | r.MouseButton(3, false) 171 | return 172 | } 173 | r.KeyboardInput(ps2Encode(ev.KeyboardEvent, false)) 174 | case canvas.ClipboardChangeEvent: 175 | clipboard.setText(ev.Data) 176 | } 177 | } 178 | 179 | func getTicks() int64 { 180 | return time.Now().UnixNano() / int64(time.Millisecond) 181 | } 182 | 183 | func httpLink(addr string) string { 184 | if addr[0] == ':' { 185 | addr = "localhost" + addr 186 | } 187 | return "http://" + addr 188 | } 189 | 190 | // startBrowser tries to open the URL in a browser 191 | // and reports whether it succeeds. 192 | func startBrowser(url string) bool { 193 | // try to start the browser 194 | var args []string 195 | switch runtime.GOOS { 196 | case "darwin": 197 | args = []string{"open"} 198 | case "windows": 199 | args = []string{"cmd", "/c", "start"} 200 | default: 201 | args = []string{"xdg-open"} 202 | } 203 | cmd := exec.Command(args[0], append(args[1:], url)...) 204 | return cmd.Start() == nil 205 | } 206 | -------------------------------------------------------------------------------- /cmd/oberon-emu/ps2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "github.com/fzipp/oberon/cmd/oberon-emu/internal/canvas" 8 | 9 | type kInfo struct { 10 | code byte 11 | typ kType 12 | } 13 | 14 | type kType int 15 | 16 | const ( 17 | kUnknown kType = iota 18 | kNormal 19 | kExtended 20 | kShift 21 | ) 22 | 23 | // ps2Encode translates a canvas keyboard event into a PS/2 keyboard command 24 | // sequence. See https://wiki.osdev.org/PS/2_Keyboard for a list of commands. 25 | // The 'make' parameter indicates if the key is pressed (true) or released 26 | // (false). 27 | func ps2Encode(e canvas.KeyboardEvent, make bool) []byte { 28 | var out []byte 29 | info := keymap[e.Key] 30 | switch info.typ { 31 | case kUnknown: 32 | break 33 | case kNormal: 34 | if !make { 35 | out = append(out, 0xF0) 36 | } 37 | out = append(out, info.code) 38 | case kExtended: 39 | out = append(out, 0xE0) 40 | if !make { 41 | out = append(out, 0xF0) 42 | } 43 | out = append(out, info.code) 44 | case kShift: 45 | // This assumes Num Lock is always active 46 | if make { 47 | // fake shift press 48 | out = append(out, 0xE0) 49 | out = append(out, 0x12) 50 | out = append(out, 0xE0) 51 | out = append(out, info.code) 52 | // fake shift release 53 | out = append(out, 0xE0) 54 | out = append(out, 0xF0) 55 | out = append(out, 0x12) 56 | } else { 57 | out = append(out, 0xE0) 58 | out = append(out, 0xF0) 59 | out = append(out, info.code) 60 | // fake shift release 61 | out = append(out, 0xE0) 62 | out = append(out, 0xF0) 63 | out = append(out, 0x12) 64 | } 65 | } 66 | return out 67 | } 68 | 69 | var keymap = map[string]kInfo{ 70 | "a": {0x1C, kNormal}, 71 | "b": {0x32, kNormal}, 72 | "c": {0x21, kNormal}, 73 | "d": {0x23, kNormal}, 74 | "e": {0x24, kNormal}, 75 | "f": {0x2B, kNormal}, 76 | "g": {0x34, kNormal}, 77 | "h": {0x33, kNormal}, 78 | "i": {0x43, kNormal}, 79 | "j": {0x3B, kNormal}, 80 | "k": {0x42, kNormal}, 81 | "l": {0x4B, kNormal}, 82 | "m": {0x3A, kNormal}, 83 | "n": {0x31, kNormal}, 84 | "o": {0x44, kNormal}, 85 | "p": {0x4D, kNormal}, 86 | "q": {0x15, kNormal}, 87 | "r": {0x2D, kNormal}, 88 | "s": {0x1B, kNormal}, 89 | "t": {0x2C, kNormal}, 90 | "u": {0x3C, kNormal}, 91 | "v": {0x2A, kNormal}, 92 | "w": {0x1D, kNormal}, 93 | "x": {0x22, kNormal}, 94 | "y": {0x35, kNormal}, 95 | "z": {0x1A, kNormal}, 96 | 97 | "A": {0x1C, kShift}, 98 | "B": {0x32, kShift}, 99 | "C": {0x21, kShift}, 100 | "D": {0x23, kShift}, 101 | "E": {0x24, kShift}, 102 | "F": {0x2B, kShift}, 103 | "G": {0x34, kShift}, 104 | "H": {0x33, kShift}, 105 | "I": {0x43, kShift}, 106 | "J": {0x3B, kShift}, 107 | "K": {0x42, kShift}, 108 | "L": {0x4B, kShift}, 109 | "M": {0x3A, kShift}, 110 | "N": {0x31, kShift}, 111 | "O": {0x44, kShift}, 112 | "P": {0x4D, kShift}, 113 | "Q": {0x15, kShift}, 114 | "R": {0x2D, kShift}, 115 | "S": {0x1B, kShift}, 116 | "T": {0x2C, kShift}, 117 | "U": {0x3C, kShift}, 118 | "V": {0x2A, kShift}, 119 | "W": {0x1D, kShift}, 120 | "X": {0x22, kShift}, 121 | "Y": {0x35, kShift}, 122 | "Z": {0x1A, kShift}, 123 | 124 | "1": {0x16, kNormal}, 125 | "2": {0x1E, kNormal}, 126 | "3": {0x26, kNormal}, 127 | "4": {0x25, kNormal}, 128 | "5": {0x2E, kNormal}, 129 | "6": {0x36, kNormal}, 130 | "7": {0x3D, kNormal}, 131 | "8": {0x3E, kNormal}, 132 | "9": {0x46, kNormal}, 133 | "0": {0x45, kNormal}, 134 | 135 | "Enter": {0x5A, kNormal}, 136 | "Escape": {0x76, kNormal}, 137 | "Backspace": {0x66, kNormal}, 138 | "Tab": {0x0D, kNormal}, 139 | " ": {0x29, kNormal}, 140 | 141 | "-": {0x4E, kNormal}, 142 | "=": {0x55, kNormal}, 143 | "[": {0x54, kNormal}, 144 | "]": {0x5B, kNormal}, 145 | `\`: {0x5D, kNormal}, 146 | ";": {0x4C, kNormal}, 147 | "'": {0x52, kNormal}, 148 | "`": {0x0E, kNormal}, 149 | ",": {0x41, kNormal}, 150 | ".": {0x49, kNormal}, 151 | "/": {0x4A, kNormal}, 152 | 153 | "<": {0x41, kShift}, 154 | ">": {0x49, kShift}, 155 | ":": {0x4C, kShift}, 156 | "_": {0x4E, kShift}, 157 | "#": {0x26, kShift}, 158 | "~": {0x0E, kShift}, 159 | "@": {0x1E, kShift}, 160 | "|": {0x5D, kShift}, 161 | "+": {0x55, kShift}, 162 | "?": {0x4A, kShift}, 163 | "(": {0x46, kShift}, 164 | ")": {0x45, kShift}, 165 | "&": {0x3D, kShift}, 166 | "%": {0x2E, kShift}, 167 | "$": {0x25, kShift}, 168 | `"`: {0x52, kShift}, 169 | "!": {0x16, kShift}, 170 | "^": {0x36, kShift}, 171 | "*": {0x3E, kShift}, 172 | "{": {0x54, kShift}, 173 | "}": {0x5B, kShift}, 174 | 175 | "F1": {0x05, kNormal}, 176 | "F2": {0x06, kNormal}, 177 | "F3": {0x04, kNormal}, 178 | "F4": {0x0C, kNormal}, 179 | "F5": {0x03, kNormal}, 180 | "F6": {0x0B, kNormal}, 181 | "F7": {0x83, kNormal}, 182 | "F8": {0x0A, kNormal}, 183 | "F9": {0x01, kNormal}, 184 | "F10": {0x09, kNormal}, 185 | "F11": {0x78, kNormal}, 186 | "F12": {0x07, kNormal}, 187 | 188 | /* 189 | // Most of the keys below are not used by Oberon 190 | 191 | sdl.SCANCODE_INSERT: {0x70, kNumLockHack}, 192 | sdl.SCANCODE_HOME: {0x6C, kNumLockHack}, 193 | sdl.SCANCODE_PAGEUP: {0x7D, kNumLockHack}, 194 | sdl.SCANCODE_DELETE: {0x71, kNumLockHack}, 195 | sdl.SCANCODE_END: {0x69, kNumLockHack}, 196 | sdl.SCANCODE_PAGEDOWN: {0x7A, kNumLockHack}, 197 | sdl.SCANCODE_RIGHT: {0x74, kNumLockHack}, 198 | sdl.SCANCODE_LEFT: {0x68, kNumLockHack}, 199 | sdl.SCANCODE_DOWN: {0x72, kNumLockHack}, 200 | sdl.SCANCODE_UP: {0x75, kNumLockHack}, 201 | 202 | sdl.SCANCODE_NONUSBACKSLASH: {0x61, kNormal}, 203 | sdl.SCANCODE_APPLICATION: {0x2F, kExtended}, 204 | 205 | sdl.SCANCODE_LCTRL: {0x14, kNormal}, 206 | sdl.SCANCODE_LSHIFT: {0x12, kNormal}, 207 | sdl.SCANCODE_LALT: {0x11, kNormal}, 208 | sdl.SCANCODE_LGUI: {0x1F, kExtended}, 209 | sdl.SCANCODE_RCTRL: {0x14, kExtended}, 210 | sdl.SCANCODE_RSHIFT: {0x59, kNormal}, 211 | sdl.SCANCODE_RALT: {0x11, kExtended}, 212 | sdl.SCANCODE_RGUI: {0x27, kExtended}, 213 | */ 214 | } 215 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/ps2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "github.com/veandco/go-sdl2/sdl" 8 | 9 | type kInfo struct { 10 | code byte 11 | typ kType 12 | } 13 | 14 | type kType int 15 | 16 | const ( 17 | kUnknown kType = iota 18 | kNormal 19 | kExtended 20 | kNumLockHack 21 | kShiftHack 22 | ) 23 | 24 | // ps2Encode translates an SDL keyboard scancode into a PS/2 keyboard command 25 | // sequence. See https://wiki.osdev.org/PS/2_Keyboard for a list of commands. 26 | // The 'make' parameter indicates if the key is pressed (true) or released 27 | // (false). 28 | func ps2Encode(sdlScancode sdl.Scancode, make bool) []byte { 29 | var out []byte 30 | info := keymap[sdlScancode] 31 | switch info.typ { 32 | case kUnknown: 33 | break 34 | case kNormal: 35 | if !make { 36 | out = append(out, 0xF0) 37 | } 38 | out = append(out, info.code) 39 | case kExtended: 40 | out = append(out, 0xE0) 41 | if !make { 42 | out = append(out, 0xF0) 43 | } 44 | out = append(out, info.code) 45 | case kNumLockHack: 46 | // This assumes Num Lock is always active 47 | if make { 48 | // fake shift press 49 | out = append(out, 0xE0) 50 | out = append(out, 0x12) 51 | out = append(out, 0xE0) 52 | out = append(out, info.code) 53 | } else { 54 | out = append(out, 0xE0) 55 | out = append(out, 0xF0) 56 | out = append(out, info.code) 57 | // fake shift release 58 | out = append(out, 0xE0) 59 | out = append(out, 0xF0) 60 | out = append(out, 0x12) 61 | } 62 | case kShiftHack: 63 | mod := sdl.GetModState() 64 | if make { 65 | // fake shift release 66 | if mod&sdl.KMOD_LSHIFT > 0 { 67 | out = append(out, 0xE0) 68 | out = append(out, 0xF0) 69 | out = append(out, 0x12) 70 | } 71 | if mod&sdl.KMOD_RSHIFT > 0 { 72 | out = append(out, 0xE0) 73 | out = append(out, 0xF0) 74 | out = append(out, 0x59) 75 | } 76 | out = append(out, 0xE0) 77 | out = append(out, info.code) 78 | } else { 79 | out = append(out, 0xE0) 80 | out = append(out, 0xF0) 81 | out = append(out, info.code) 82 | // fake shift press 83 | if mod&sdl.KMOD_RSHIFT > 0 { 84 | out = append(out, 0xE0) 85 | out = append(out, 0x59) 86 | } 87 | if mod&sdl.KMOD_LSHIFT > 0 { 88 | out = append(out, 0xE0) 89 | out = append(out, 0x12) 90 | } 91 | } 92 | } 93 | return out 94 | } 95 | 96 | var keymap = [sdl.NUM_SCANCODES]kInfo{ 97 | sdl.SCANCODE_A: {0x1C, kNormal}, 98 | sdl.SCANCODE_B: {0x32, kNormal}, 99 | sdl.SCANCODE_C: {0x21, kNormal}, 100 | sdl.SCANCODE_D: {0x23, kNormal}, 101 | sdl.SCANCODE_E: {0x24, kNormal}, 102 | sdl.SCANCODE_F: {0x2B, kNormal}, 103 | sdl.SCANCODE_G: {0x34, kNormal}, 104 | sdl.SCANCODE_H: {0x33, kNormal}, 105 | sdl.SCANCODE_I: {0x43, kNormal}, 106 | sdl.SCANCODE_J: {0x3B, kNormal}, 107 | sdl.SCANCODE_K: {0x42, kNormal}, 108 | sdl.SCANCODE_L: {0x4B, kNormal}, 109 | sdl.SCANCODE_M: {0x3A, kNormal}, 110 | sdl.SCANCODE_N: {0x31, kNormal}, 111 | sdl.SCANCODE_O: {0x44, kNormal}, 112 | sdl.SCANCODE_P: {0x4D, kNormal}, 113 | sdl.SCANCODE_Q: {0x15, kNormal}, 114 | sdl.SCANCODE_R: {0x2D, kNormal}, 115 | sdl.SCANCODE_S: {0x1B, kNormal}, 116 | sdl.SCANCODE_T: {0x2C, kNormal}, 117 | sdl.SCANCODE_U: {0x3C, kNormal}, 118 | sdl.SCANCODE_V: {0x2A, kNormal}, 119 | sdl.SCANCODE_W: {0x1D, kNormal}, 120 | sdl.SCANCODE_X: {0x22, kNormal}, 121 | sdl.SCANCODE_Y: {0x35, kNormal}, 122 | sdl.SCANCODE_Z: {0x1A, kNormal}, 123 | 124 | sdl.SCANCODE_1: {0x16, kNormal}, 125 | sdl.SCANCODE_2: {0x1E, kNormal}, 126 | sdl.SCANCODE_3: {0x26, kNormal}, 127 | sdl.SCANCODE_4: {0x25, kNormal}, 128 | sdl.SCANCODE_5: {0x2E, kNormal}, 129 | sdl.SCANCODE_6: {0x36, kNormal}, 130 | sdl.SCANCODE_7: {0x3D, kNormal}, 131 | sdl.SCANCODE_8: {0x3E, kNormal}, 132 | sdl.SCANCODE_9: {0x46, kNormal}, 133 | sdl.SCANCODE_0: {0x45, kNormal}, 134 | 135 | sdl.SCANCODE_RETURN: {0x5A, kNormal}, 136 | sdl.SCANCODE_ESCAPE: {0x76, kNormal}, 137 | sdl.SCANCODE_BACKSPACE: {0x66, kNormal}, 138 | sdl.SCANCODE_TAB: {0x0D, kNormal}, 139 | sdl.SCANCODE_SPACE: {0x29, kNormal}, 140 | 141 | sdl.SCANCODE_MINUS: {0x4E, kNormal}, 142 | sdl.SCANCODE_EQUALS: {0x55, kNormal}, 143 | sdl.SCANCODE_LEFTBRACKET: {0x54, kNormal}, 144 | sdl.SCANCODE_RIGHTBRACKET: {0x5B, kNormal}, 145 | sdl.SCANCODE_BACKSLASH: {0x5D, kNormal}, 146 | sdl.SCANCODE_NONUSHASH: {0x5D, kNormal}, // same key as BACKSLASH 147 | 148 | sdl.SCANCODE_SEMICOLON: {0x4C, kNormal}, 149 | sdl.SCANCODE_APOSTROPHE: {0x52, kNormal}, 150 | sdl.SCANCODE_GRAVE: {0x0E, kNormal}, 151 | sdl.SCANCODE_COMMA: {0x41, kNormal}, 152 | sdl.SCANCODE_PERIOD: {0x49, kNormal}, 153 | sdl.SCANCODE_SLASH: {0x4A, kNormal}, 154 | 155 | sdl.SCANCODE_F1: {0x05, kNormal}, 156 | sdl.SCANCODE_F2: {0x06, kNormal}, 157 | sdl.SCANCODE_F3: {0x04, kNormal}, 158 | sdl.SCANCODE_F4: {0x0C, kNormal}, 159 | sdl.SCANCODE_F5: {0x03, kNormal}, 160 | sdl.SCANCODE_F6: {0x0B, kNormal}, 161 | sdl.SCANCODE_F7: {0x83, kNormal}, 162 | sdl.SCANCODE_F8: {0x0A, kNormal}, 163 | sdl.SCANCODE_F9: {0x01, kNormal}, 164 | sdl.SCANCODE_F10: {0x09, kNormal}, 165 | sdl.SCANCODE_F11: {0x78, kNormal}, 166 | sdl.SCANCODE_F12: {0x07, kNormal}, 167 | 168 | // Most of the keys below are not used by Oberon 169 | 170 | sdl.SCANCODE_INSERT: {0x70, kNumLockHack}, 171 | sdl.SCANCODE_HOME: {0x6C, kNumLockHack}, 172 | sdl.SCANCODE_PAGEUP: {0x7D, kNumLockHack}, 173 | sdl.SCANCODE_DELETE: {0x71, kNumLockHack}, 174 | sdl.SCANCODE_END: {0x69, kNumLockHack}, 175 | sdl.SCANCODE_PAGEDOWN: {0x7A, kNumLockHack}, 176 | sdl.SCANCODE_RIGHT: {0x74, kNumLockHack}, 177 | sdl.SCANCODE_LEFT: {0x68, kNumLockHack}, 178 | sdl.SCANCODE_DOWN: {0x72, kNumLockHack}, 179 | sdl.SCANCODE_UP: {0x75, kNumLockHack}, 180 | 181 | sdl.SCANCODE_KP_DIVIDE: {0x4A, kShiftHack}, 182 | sdl.SCANCODE_KP_MULTIPLY: {0x7C, kNormal}, 183 | sdl.SCANCODE_KP_MINUS: {0x7B, kNormal}, 184 | sdl.SCANCODE_KP_PLUS: {0x79, kNormal}, 185 | sdl.SCANCODE_KP_ENTER: {0x5A, kExtended}, 186 | sdl.SCANCODE_KP_1: {0x69, kNormal}, 187 | sdl.SCANCODE_KP_2: {0x72, kNormal}, 188 | sdl.SCANCODE_KP_3: {0x7A, kNormal}, 189 | sdl.SCANCODE_KP_4: {0x6B, kNormal}, 190 | sdl.SCANCODE_KP_5: {0x73, kNormal}, 191 | sdl.SCANCODE_KP_6: {0x74, kNormal}, 192 | sdl.SCANCODE_KP_7: {0x6C, kNormal}, 193 | sdl.SCANCODE_KP_8: {0x75, kNormal}, 194 | sdl.SCANCODE_KP_9: {0x7D, kNormal}, 195 | sdl.SCANCODE_KP_0: {0x70, kNormal}, 196 | sdl.SCANCODE_KP_PERIOD: {0x71, kNormal}, 197 | 198 | sdl.SCANCODE_NONUSBACKSLASH: {0x61, kNormal}, 199 | sdl.SCANCODE_APPLICATION: {0x2F, kExtended}, 200 | 201 | sdl.SCANCODE_LCTRL: {0x14, kNormal}, 202 | sdl.SCANCODE_LSHIFT: {0x12, kNormal}, 203 | sdl.SCANCODE_LALT: {0x11, kNormal}, 204 | sdl.SCANCODE_LGUI: {0x1F, kExtended}, 205 | sdl.SCANCODE_RCTRL: {0x14, kExtended}, 206 | sdl.SCANCODE_RSHIFT: {0x59, kNormal}, 207 | sdl.SCANCODE_RALT: {0x11, kExtended}, 208 | sdl.SCANCODE_RGUI: {0x27, kExtended}, 209 | } 210 | -------------------------------------------------------------------------------- /cmd/oberon-emu-sdl/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command oberon-emu-sdl is an emulator for the Project Oberon RISC machine. 6 | // It uses SDL to render the screen. 7 | package main 8 | 9 | import ( 10 | "encoding/binary" 11 | "errors" 12 | "flag" 13 | "fmt" 14 | "image" 15 | "log" 16 | "math" 17 | "os" 18 | "unsafe" 19 | 20 | "github.com/fzipp/oberon/risc" 21 | "github.com/fzipp/oberon/serial" 22 | "github.com/fzipp/oberon/spi" 23 | 24 | "github.com/veandco/go-sdl2/sdl" 25 | ) 26 | 27 | const ( 28 | cpuHz = 25000000 29 | fps = 60 30 | ) 31 | 32 | const ( 33 | colorBlack = 0x657b83 34 | colorWhite = 0xfdf6e3 35 | ) 36 | 37 | func main() { 38 | opt, err := optionsFromFlags() 39 | if err != nil { 40 | flag.Usage() 41 | os.Exit(1) 42 | } 43 | 44 | r := risc.New() 45 | r.SetSerial(&serial.PCLink{}) 46 | r.SetClipboard(&SDLClipboard{}) 47 | 48 | if opt.leds { 49 | r.SetLEDs(&ConsoleLEDs{}) 50 | } 51 | 52 | if opt.bootFromSerial { 53 | r.SetSwitches(1) 54 | } 55 | 56 | if opt.mem > 0 || opt.size != "" { 57 | r.ConfigureMemory(opt.mem, opt.sizeRect.Dx(), opt.sizeRect.Dy()) 58 | } 59 | 60 | disk, err := spi.NewDisk(opt.diskImageFile) 61 | check(err) 62 | r.SetSPI(1, disk) 63 | 64 | if opt.serialIn != "" || opt.serialOut != "" { 65 | raw, err := serial.Open(opt.serialIn, opt.serialOut) 66 | if err != nil { 67 | _, _ = fmt.Fprintf(os.Stderr, "can't open serial I/O: %s", err) 68 | return 69 | } 70 | r.SetSerial(raw) 71 | } 72 | 73 | riscRect := sdl.Rect{ 74 | W: int32(opt.sizeRect.Dx()), 75 | H: int32(opt.sizeRect.Dy()), 76 | } 77 | 78 | if err := sdl.Init(sdl.INIT_VIDEO); err != nil { 79 | log.Fatal(err) 80 | } 81 | defer sdl.Quit() 82 | sdl.EnableScreenSaver() 83 | _, err = sdl.ShowCursor(0) 84 | check(err) 85 | sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "best") 86 | 87 | windowFlags := sdl.WINDOW_HIDDEN 88 | display := 0 89 | if opt.fullscreen { 90 | windowFlags |= sdl.WINDOW_FULLSCREEN_DESKTOP 91 | display, err = bestDisplay(riscRect) 92 | check(err) 93 | } 94 | if opt.zoom == 0 { 95 | bounds, err := sdl.GetDisplayBounds(display) 96 | check(err) 97 | if bounds.H >= riscRect.H*2 && bounds.W >= riscRect.W*2 { 98 | opt.zoom = 2 99 | } else { 100 | opt.zoom = 1 101 | } 102 | } 103 | window, err := sdl.CreateWindow("Project Oberon", 104 | sdl.WINDOWPOS_UNDEFINED, 105 | sdl.WINDOWPOS_UNDEFINED, 106 | int32(float64(riscRect.W)*(opt.zoom)), 107 | int32(float64(riscRect.H)*(opt.zoom)), 108 | uint32(windowFlags)) 109 | check(err) 110 | renderer, err := sdl.CreateRenderer(window, -1, 0) 111 | check(err) 112 | texture, err := renderer.CreateTexture( 113 | sdl.PIXELFORMAT_ARGB8888, 114 | sdl.TEXTUREACCESS_STREAMING, 115 | riscRect.W, 116 | riscRect.H, 117 | ) 118 | check(err) 119 | 120 | fb := r.Framebuffer() 121 | displayRect, displayScale := scaleDisplay(window, riscRect) 122 | err = updateTexture(fb, r.GetFramebufferDamageAndReset(), texture, riscRect) 123 | check(err) 124 | window.Show() 125 | err = renderer.Clear() 126 | check(err) 127 | err = renderer.Copy(texture, &riscRect, &displayRect) 128 | check(err) 129 | renderer.Present() 130 | 131 | done := false 132 | mouseWasOffscreen := false 133 | for !done { 134 | frameStart := sdl.GetTicks64() 135 | 136 | for { 137 | event := sdl.PollEvent() 138 | if event == nil { 139 | break 140 | } 141 | switch event.GetType() { 142 | case sdl.QUIT: 143 | done = true 144 | 145 | case sdl.WINDOWEVENT: 146 | ev := event.(*sdl.WindowEvent) 147 | if ev.Event == sdl.WINDOWEVENT_RESIZED { 148 | displayRect, displayScale = scaleDisplay(window, riscRect) 149 | } 150 | 151 | case sdl.MOUSEMOTION: 152 | ev := event.(*sdl.MouseMotionEvent) 153 | scaledX := int(math.Round(float64(ev.X-displayRect.X) / displayScale)) 154 | scaledY := int(math.Round(float64(ev.Y-displayRect.Y) / displayScale)) 155 | x := clamp(scaledX, 0, int(riscRect.W)-1) 156 | y := clamp(scaledY, 0, int(riscRect.H)-1) 157 | mouseIsOffscreen := x != scaledX || y != scaledY 158 | if mouseIsOffscreen != mouseWasOffscreen { 159 | var toggle int 160 | if mouseIsOffscreen { 161 | toggle = sdl.ENABLE 162 | } else { 163 | toggle = sdl.DISABLE 164 | } 165 | _, err = sdl.ShowCursor(toggle) 166 | check(err) 167 | mouseWasOffscreen = mouseIsOffscreen 168 | } 169 | r.MouseMoved(x, int(riscRect.H)-y-1) 170 | 171 | case sdl.MOUSEBUTTONDOWN, sdl.MOUSEBUTTONUP: 172 | ev := event.(*sdl.MouseButtonEvent) 173 | down := ev.State == sdl.PRESSED 174 | r.MouseButton(int(ev.Button), down) 175 | 176 | case sdl.KEYDOWN, sdl.KEYUP: 177 | ev := event.(*sdl.KeyboardEvent) 178 | down := ev.State == sdl.PRESSED 179 | switch mapKeyboardEvent(ev) { 180 | case actionReset: 181 | r.Reset() 182 | case actionToggleFullscreen: 183 | opt.fullscreen = !opt.fullscreen 184 | if opt.fullscreen { 185 | err = window.SetFullscreen(sdl.WINDOW_FULLSCREEN_DESKTOP) 186 | } else { 187 | err = window.SetFullscreen(0) 188 | } 189 | check(err) 190 | case actionQuit: 191 | _, err = sdl.PushEvent(&sdl.QuitEvent{ 192 | Type: sdl.QUIT, 193 | Timestamp: uint32(sdl.GetTicks64()), 194 | }) 195 | check(err) 196 | case actionFakeMouse1: 197 | r.MouseButton(1, down) 198 | case actionFakeMouse2: 199 | r.MouseButton(2, down) 200 | case actionFakeMouse3: 201 | r.MouseButton(3, down) 202 | case actionOberonInput: 203 | r.KeyboardInput(ps2Encode(ev.Keysym.Scancode, down)) 204 | } 205 | } 206 | } 207 | 208 | r.SetTime(uint32(frameStart)) 209 | err = r.Run(cpuHz / fps) 210 | if err != nil { 211 | var riscErr *risc.Error 212 | if errors.As(err, &riscErr) { 213 | _, _ = fmt.Fprintf(os.Stderr, "%s (PC=0x%08X)\n", riscErr, riscErr.PC) 214 | } else { 215 | _, _ = fmt.Fprintln(os.Stderr, err) 216 | } 217 | } 218 | 219 | err = updateTexture(fb, r.GetFramebufferDamageAndReset(), texture, riscRect) 220 | check(err) 221 | err = renderer.Clear() 222 | check(err) 223 | err = renderer.Copy(texture, &riscRect, &displayRect) 224 | check(err) 225 | renderer.Present() 226 | 227 | frameEnd := sdl.GetTicks64() 228 | delay := int(frameStart) + 1000/fps - int(frameEnd) 229 | if delay > 0 { 230 | sdl.Delay(uint32(delay)) 231 | } 232 | } 233 | } 234 | 235 | func scaleDisplay(window *sdl.Window, riscRect sdl.Rect) (sdl.Rect, float64) { 236 | winW, winH := window.GetSize() 237 | oberonAspect := float64(riscRect.W) / float64(riscRect.H) 238 | windowAspect := float64(winW) / float64(winH) 239 | 240 | var scale float64 241 | if oberonAspect > windowAspect { 242 | scale = float64(winW) / float64(riscRect.W) 243 | } else { 244 | scale = float64(winH) / float64(riscRect.H) 245 | } 246 | 247 | w := int32(math.Ceil(float64(riscRect.W) * scale)) 248 | h := int32(math.Ceil(float64(riscRect.H) * scale)) 249 | return sdl.Rect{ 250 | W: w, 251 | H: h, 252 | X: (winW - w) / 2, 253 | Y: (winH - h) / 2, 254 | }, scale 255 | } 256 | 257 | // Only used in update_texture(), but some systems complain if you 258 | // allocate three megabyte on the stack. 259 | var pixelBuf [maxWidth * maxHeight * 4]byte 260 | 261 | func updateTexture(fb *risc.Framebuffer, damage image.Rectangle, texture *sdl.Texture, riscRect sdl.Rect) error { 262 | if damage.Min.Y > damage.Max.Y { 263 | return nil 264 | } 265 | 266 | var outIdx uint32 267 | 268 | for line := damage.Max.Y; line >= damage.Min.Y; line-- { 269 | lineStart := line * (int(riscRect.W) / 32) 270 | for col := damage.Min.X; col <= damage.Max.X; col++ { 271 | pixels := fb.Pix[lineStart+col] 272 | for range 32 { 273 | var color uint32 274 | if pixels&1 > 0 { 275 | color = colorWhite 276 | } else { 277 | color = colorBlack 278 | } 279 | binary.LittleEndian.PutUint32(pixelBuf[outIdx*4:], color) 280 | pixels >>= 1 281 | outIdx++ 282 | } 283 | } 284 | } 285 | 286 | rect := sdl.Rect{ 287 | X: int32(damage.Min.X) * 32, 288 | Y: riscRect.H - int32(damage.Max.Y) - 1, 289 | W: int32((damage.Max.X - damage.Min.X + 1) * 32), 290 | H: int32(damage.Max.Y - damage.Min.Y + 1), 291 | } 292 | return texture.Update(&rect, unsafe.Pointer(&pixelBuf), int(rect.W)*4) 293 | } 294 | 295 | func bestDisplay(rect sdl.Rect) (int, error) { 296 | best := 0 297 | displayCnt, err := sdl.GetNumVideoDisplays() 298 | if err != nil { 299 | return best, err 300 | } 301 | for i := range displayCnt { 302 | bounds, err := sdl.GetDisplayBounds(i) 303 | if err != nil { 304 | return best, err 305 | } 306 | if bounds.H == rect.H && bounds.W >= rect.W { 307 | best = i 308 | if bounds.W == rect.W { 309 | break // exact match 310 | } 311 | } 312 | } 313 | return best, nil 314 | } 315 | 316 | func check(err error) { 317 | if err != nil { 318 | fail(err) 319 | } 320 | } 321 | 322 | func fail(message any) { 323 | _, _ = fmt.Fprintln(os.Stderr, message) 324 | os.Exit(1) 325 | } 326 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/web/canvas-websocket.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp. 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 | document.addEventListener("DOMContentLoaded", function () { 6 | "use strict"; 7 | 8 | const canvases = document.getElementsByTagName("canvas"); 9 | for (let i = 0; i < canvases.length; i++) { 10 | const canvas = canvases[i]; 11 | const config = configFrom(canvas.dataset); 12 | if (config.drawUrl) { 13 | webSocketCanvas(canvas, config); 14 | if (config.contextMenuDisabled) { 15 | disableContextMenu(canvas); 16 | } 17 | } 18 | } 19 | 20 | function pollClipboardChange(onChange) { 21 | let clipboardText = ''; 22 | return setInterval(function() { 23 | navigator.clipboard.readText().then(function(clipText) { 24 | if (clipText !== clipboardText) { 25 | clipboardText = clipText; 26 | onChange({data: clipText}); 27 | } 28 | }); 29 | }, 1000); 30 | } 31 | 32 | function configFrom(dataset) { 33 | return { 34 | drawUrl: absoluteWebSocketUrl(dataset["websocketDrawUrl"]), 35 | eventMask: parseInt(dataset["websocketEventMask"], 10) || 0, 36 | reconnectInterval: parseInt(dataset["websocketReconnectInterval"], 10) || 0, 37 | contextMenuDisabled: (dataset["disableContextMenu"] === "true") 38 | }; 39 | } 40 | 41 | function absoluteWebSocketUrl(url) { 42 | if (!url) { 43 | return null; 44 | } 45 | if (url.indexOf("ws://") === 0 || url.indexOf("wss://") === 0) { 46 | return url; 47 | } 48 | const wsUrl = new URL(url, window.location.href); 49 | wsUrl.protocol = wsUrl.protocol.replace("http", "ws"); 50 | return wsUrl.href; 51 | } 52 | 53 | function webSocketCanvas(canvas, config) { 54 | const ctx = canvas.getContext("2d"); 55 | const webSocket = new WebSocket(config.drawUrl); 56 | let handlers = {}; 57 | webSocket.binaryType = "arraybuffer"; 58 | webSocket.addEventListener("open", function () { 59 | handlers = addEventListeners(canvas, config.eventMask, webSocket); 60 | }); 61 | webSocket.addEventListener("error", function () { 62 | webSocket.close(); 63 | }); 64 | webSocket.addEventListener("close", function () { 65 | removeEventListeners(canvas, handlers); 66 | if (!config.reconnectInterval) { 67 | return; 68 | } 69 | setTimeout(function () { 70 | webSocketCanvas(canvas, config); 71 | }, config.reconnectInterval); 72 | }); 73 | webSocket.addEventListener("message", function (event) { 74 | draw(ctx, new DataView(event.data)); 75 | }); 76 | } 77 | 78 | function addEventListeners(canvas, eventMask, webSocket) { 79 | const handlers = {}; 80 | 81 | if (eventMask & 1) { 82 | handlers["mousemove"] = sendMouseEvent(1); 83 | } 84 | if (eventMask & 2) { 85 | handlers["mousedown"] = sendMouseEvent(2); 86 | } 87 | if (eventMask & 4) { 88 | handlers["mouseup"] = sendMouseEvent(3); 89 | } 90 | if (eventMask & 8) { 91 | handlers["keydown"] = sendKeyEvent(4); 92 | } 93 | if (eventMask & 16) { 94 | handlers["keyup"] = sendKeyEvent(5); 95 | } 96 | if (eventMask & 32) { 97 | handlers["click"] = sendMouseEvent(6); 98 | } 99 | if (eventMask & 64) { 100 | handlers["dblclick"] = sendMouseEvent(7); 101 | } 102 | if (eventMask & 128) { 103 | handlers["auxclick"] = sendMouseEvent(8); 104 | } 105 | if (eventMask & 256) { 106 | handlers["wheel"] = sendWheelEvent(9); 107 | } 108 | if (eventMask & 512) { 109 | handlers["touchstart"] = sendTouchEvent(10); 110 | } 111 | if (eventMask & 1024) { 112 | handlers["touchmove"] = sendTouchEvent(11); 113 | } 114 | if (eventMask & 2048) { 115 | handlers["touchend"] = sendTouchEvent(12); 116 | } 117 | if (eventMask & 4096) { 118 | handlers["touchcancel"] = sendTouchEvent(13); 119 | } 120 | if (eventMask & 8192) { 121 | pollClipboardChange(sendClipboardEvent(14)); 122 | } 123 | 124 | Object.keys(handlers).forEach(function (type) { 125 | const target = targetFor(type, canvas); 126 | target.addEventListener(type, handlers[type], {passive: false}); 127 | }); 128 | 129 | const rect = canvas.getBoundingClientRect(); 130 | 131 | const mouseMoveThreshold = 25; 132 | let lastMouseMoveTime = -1; 133 | 134 | function sendMouseEvent(eventType) { 135 | return function (event) { 136 | event.preventDefault(); 137 | if (eventType === 1) { 138 | const now = new Date().getTime(); 139 | if ((now - lastMouseMoveTime) < mouseMoveThreshold) { 140 | return; 141 | } 142 | lastMouseMoveTime = now; 143 | } 144 | const eventMessage = new ArrayBuffer(11); 145 | const dataView = new DataView(eventMessage); 146 | setMouseEvent(dataView, eventType, event); 147 | webSocket.send(eventMessage); 148 | }; 149 | } 150 | 151 | function sendWheelEvent(eventType) { 152 | return function (event) { 153 | event.preventDefault(); 154 | const eventMessage = new ArrayBuffer(36); 155 | const dataView = new DataView(eventMessage); 156 | setMouseEvent(dataView, eventType, event); 157 | dataView.setFloat64(11, event.deltaX); 158 | dataView.setFloat64(19, event.deltaY); 159 | dataView.setFloat64(27, event.deltaZ); 160 | dataView.setUint8(35, event.deltaMode); 161 | webSocket.send(eventMessage); 162 | }; 163 | } 164 | 165 | function setMouseEvent(dataView, eventType, event) { 166 | dataView.setUint8(0, eventType); 167 | dataView.setUint8(1, event.buttons); 168 | dataView.setUint32(2, ((event.clientX - rect.left) / canvas.offsetWidth) * canvas.width); 169 | dataView.setUint32(6, ((event.clientY - rect.top) / canvas.offsetHeight) * canvas.height); 170 | dataView.setUint8(10, encodeModifierKeys(event)); 171 | } 172 | 173 | function sendTouchEvent(eventType) { 174 | return function (event) { 175 | event.preventDefault(); 176 | const touchBytes = 12; 177 | const eventMessage = new ArrayBuffer(1 + 178 | 1 + (event.touches.length * touchBytes) + 179 | 1 + (event.changedTouches.length * touchBytes) + 180 | 1 + (event.targetTouches.length * touchBytes) + 181 | 1); 182 | const dataView = new DataView(eventMessage); 183 | let offset = 0; 184 | dataView.setUint8(offset, eventType); 185 | offset++; 186 | offset = setTouches(dataView, offset, event.touches); 187 | offset = setTouches(dataView, offset, event.changedTouches); 188 | offset = setTouches(dataView, offset, event.targetTouches); 189 | dataView.setUint8(offset, encodeModifierKeys(event)); 190 | webSocket.send(eventMessage); 191 | }; 192 | } 193 | 194 | function setTouches(dataView, offset, touches) { 195 | const len = touches.length; 196 | dataView.setUint8(offset, len); 197 | offset++; 198 | for (let i = 0; i < len; i++) { 199 | const touch = touches[i]; 200 | dataView.setUint32(offset, touch.identifier); 201 | offset += 4; 202 | dataView.setUint32(offset, ((touch.clientX - rect.left) / canvas.offsetWidth) * canvas.width); 203 | offset += 4; 204 | dataView.setUint32(offset, ((touch.clientY - rect.top) / canvas.offsetHeight) * canvas.height); 205 | offset += 4; 206 | } 207 | return offset; 208 | } 209 | 210 | function sendKeyEvent(eventType) { 211 | return function (event) { 212 | event.preventDefault(); 213 | const keyBytes = new TextEncoder().encode(event.key); 214 | const eventMessage = new ArrayBuffer(6 + keyBytes.byteLength); 215 | const data = new DataView(eventMessage); 216 | data.setUint8(0, eventType); 217 | data.setUint8(1, encodeModifierKeys(event)); 218 | data.setUint32(2, keyBytes.byteLength); 219 | for (let i = 0; i < keyBytes.length; i++) { 220 | data.setUint8(6 + i, keyBytes[i]); 221 | } 222 | webSocket.send(eventMessage); 223 | }; 224 | } 225 | 226 | function sendClipboardEvent(eventType) { 227 | return function (event) { 228 | const dataBytes = new TextEncoder().encode(event.data); 229 | const eventMessage = new ArrayBuffer(5 + dataBytes.byteLength); 230 | const data = new DataView(eventMessage); 231 | data.setUint8(0, eventType); 232 | data.setUint32(1, dataBytes.byteLength); 233 | for (let i = 0; i < dataBytes.length; i++) { 234 | data.setUint8(5 + i, dataBytes[i]); 235 | } 236 | webSocket.send(eventMessage); 237 | }; 238 | } 239 | 240 | return handlers; 241 | } 242 | 243 | function removeEventListeners(canvas, handlers) { 244 | Object.keys(handlers).forEach(function (type) { 245 | const target = targetFor(type, canvas); 246 | target.removeEventListener(type, handlers[type]); 247 | }); 248 | } 249 | 250 | function targetFor(eventType, canvas) { 251 | if ((eventType.indexOf("key") !== 0) && (eventType.indexOf("composition") !== 0)) { 252 | return canvas; 253 | } 254 | return document; 255 | } 256 | 257 | function disableContextMenu(canvas) { 258 | canvas.addEventListener("contextmenu", function (e) { 259 | e.preventDefault(); 260 | }, false); 261 | } 262 | 263 | function encodeModifierKeys(event) { 264 | let modifiers = 0; 265 | if (event.altKey) { 266 | modifiers |= 1; 267 | } 268 | if (event.shiftKey) { 269 | modifiers |= 2; 270 | } 271 | if (event.ctrlKey) { 272 | modifiers |= 4; 273 | } 274 | if (event.metaKey) { 275 | modifiers |= 8; 276 | } 277 | return modifiers; 278 | } 279 | 280 | function draw(ctx, data) { 281 | switch (data.getUint8(0)) { 282 | case 1: 283 | const x = data.getUint32(1); 284 | const y = data.getUint32(5); 285 | const width = data.getUint32(9); 286 | const height = data.getUint32(13); 287 | const len = width * height * 4; 288 | const bufferOffset = data.byteOffset + 17; 289 | const buffer = data.buffer.slice(bufferOffset, bufferOffset + len); 290 | const array = new Uint8ClampedArray(buffer); 291 | const imageData = new ImageData(array, width, height); 292 | ctx.putImageData(imageData, x, y); 293 | return 17 + len; 294 | case 2: 295 | const text = getString(data, 1); 296 | navigator.clipboard.writeText(text.value); 297 | return 1 + text.byteLen; 298 | } 299 | return 1; 300 | } 301 | }); 302 | 303 | function getString(data, offset) { 304 | const stringLen = data.getUint32(offset); 305 | const stringBegin = data.byteOffset + offset + 4; 306 | const stringEnd = stringBegin + stringLen; 307 | return { 308 | value: new TextDecoder().decode(data.buffer.slice(stringBegin, stringEnd)), 309 | byteLen: 4 + stringLen 310 | }; 311 | } 312 | -------------------------------------------------------------------------------- /cmd/oberon-emu/internal/canvas/event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Frederik Zipp. 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 canvas 6 | 7 | import "fmt" 8 | 9 | // Event is an interface implemented by all event subtypes. Events can be 10 | // received from the channel returned by Context.Events. Use a type switch 11 | // to distinguish between different event types. 12 | type Event interface { 13 | mask() eventMask 14 | } 15 | 16 | // The CloseEvent is fired when the WebSocket connection to the client is 17 | // closed. It is not necessary to enable the CloseEvent with the EnableEvents 18 | // option, it is always enabled. Animation loops should handle the CloseEvent 19 | // to quit the loop. 20 | type CloseEvent struct{} 21 | 22 | func (e CloseEvent) mask() eventMask { return 0 } 23 | 24 | // MouseEvent represents events that occur due to the user interacting with a 25 | // pointing device (such as a mouse). 26 | type MouseEvent struct { 27 | // Buttons encodes the buttons being depressed (if any) when the mouse 28 | // event was fired. 29 | Buttons MouseButtons 30 | // The X coordinate of the mouse pointer. 31 | X int 32 | // The Y coordinate of the mouse pointer. 33 | Y int 34 | modifierKeys 35 | } 36 | 37 | func (e MouseEvent) mask() eventMask { 38 | return maskMouseMove | maskMouseUp | maskMouseDown | maskClick | maskDblClick | maskAuxClick 39 | } 40 | 41 | // The MouseMoveEvent is fired when a pointing device (usually a mouse) is 42 | // moved. 43 | type MouseMoveEvent struct{ MouseEvent } 44 | 45 | func (e MouseMoveEvent) mask() eventMask { return maskMouseMove } 46 | 47 | // The MouseDownEvent is fired when a pointing device button is pressed. 48 | // 49 | // Note: This differs from the ClickEvent in that click is fired after a full 50 | // click action occurs; that is, the mouse button is pressed and released 51 | // while the pointer remains inside the canvas. MouseDownEvent is fired 52 | // the moment the button is initially pressed. 53 | type MouseDownEvent struct{ MouseEvent } 54 | 55 | func (e MouseDownEvent) mask() eventMask { return maskMouseDown } 56 | 57 | // The MouseUpEvent is fired when a button on a pointing device (such as a 58 | // mouse or trackpad) is released. It is the counterpoint to the 59 | // MouseDownEvent. 60 | type MouseUpEvent struct{ MouseEvent } 61 | 62 | func (e MouseUpEvent) mask() eventMask { return maskMouseUp } 63 | 64 | // The ClickEvent is fired when a pointing device button (such as a mouse's 65 | // primary mouse button) is both pressed and released while the pointer is 66 | // located inside the canvas. 67 | type ClickEvent struct{ MouseEvent } 68 | 69 | func (e ClickEvent) mask() eventMask { return maskClick } 70 | 71 | // The DblClickEvent is fired when a pointing device button (such as a mouse's 72 | // primary button) is double-clicked; that is, when it's rapidly clicked twice 73 | // on the canvas within a very short span of time. 74 | // 75 | // DblClickEvent fires after two ClickEvents (and by extension, after two pairs 76 | // of MouseDownEvents and MouseUpEvents). 77 | type DblClickEvent struct{ MouseEvent } 78 | 79 | func (e DblClickEvent) mask() eventMask { return maskDblClick } 80 | 81 | // The AuxClickEvent is fired when a non-primary pointing device button (any 82 | // mouse button other than the primary—usually leftmost—button) has been 83 | // pressed and released both within the same element. 84 | type AuxClickEvent struct{ MouseEvent } 85 | 86 | func (e AuxClickEvent) mask() eventMask { return maskAuxClick } 87 | 88 | // The WheelEvent is fired due to the user moving a mouse wheel or similar 89 | // input device. 90 | type WheelEvent struct { 91 | MouseEvent 92 | // DeltaX represents the horizontal scroll amount. 93 | DeltaX float64 94 | // DeltaY represents the vertical scroll amount. 95 | DeltaY float64 96 | // DeltaZ represents the scroll amount for the z-axis. 97 | DeltaZ float64 98 | // DeltaMode represents the unit of the delta values' scroll amount. 99 | DeltaMode DeltaMode 100 | } 101 | 102 | func (e WheelEvent) mask() eventMask { 103 | return maskWheel 104 | } 105 | 106 | // DeltaMode represents the unit of the delta values' scroll amount. 107 | type DeltaMode byte 108 | 109 | const ( 110 | // DeltaPixel means the delta values are specified in pixels. 111 | DeltaPixel DeltaMode = iota 112 | // DeltaLine means the delta values are specified in lines. 113 | DeltaLine 114 | // DeltaPage means the delta values are specified in pages. 115 | DeltaPage 116 | ) 117 | 118 | // KeyboardEvent objects describe a user interaction with the keyboard; each 119 | // event describes a single interaction between the user and a key (or 120 | // combination of a key with modifier keys) on the keyboard. 121 | type KeyboardEvent struct { 122 | // Key represents the key value of the key represented by the event. 123 | Key string 124 | modifierKeys 125 | } 126 | 127 | func (e KeyboardEvent) mask() eventMask { 128 | return maskKeyDown | maskKeyUp 129 | } 130 | 131 | // The KeyDownEvent is fired when a key is pressed. 132 | type KeyDownEvent struct{ KeyboardEvent } 133 | 134 | func (e KeyDownEvent) mask() eventMask { return maskKeyDown } 135 | 136 | // The KeyUpEvent is fired when a key is released. 137 | type KeyUpEvent struct{ KeyboardEvent } 138 | 139 | func (e KeyUpEvent) mask() eventMask { return maskKeyUp } 140 | 141 | // The TouchEvent is fired when the state of contacts with a touch-sensitive 142 | // surface changes. This surface can be a touch screen or trackpad, for 143 | // example. The event can describe one or more points of contact with the 144 | // screen and includes support for detecting movement, addition and removal of 145 | // contact points, and so forth. 146 | // 147 | // Touches are represented by the Touch object; each touch is described by a 148 | // position, size and shape, amount of pressure, and target element. Lists of 149 | // touches are represented by TouchList objects. 150 | type TouchEvent struct { 151 | // Touches is a TouchList of all the Touch objects representing all current 152 | // points of contact with the surface, regardless of target or changed 153 | // status. 154 | Touches TouchList 155 | // ChangedTouches is a TouchList of all the Touch objects representing 156 | // individual points of contact whose states changed between the previous 157 | // touch event and this one. 158 | ChangedTouches TouchList 159 | // TargetTouches is a TouchList of all the Touch objects that are both 160 | // currently in contact with the touch surface and were also started on the 161 | // same element that is the target of the event. 162 | TargetTouches TouchList 163 | modifierKeys 164 | } 165 | 166 | func (e TouchEvent) mask() eventMask { 167 | return maskTouchStart | maskTouchMove | maskTouchEnd | maskTouchCancel 168 | } 169 | 170 | // TouchList represents a list of contact points on a touch surface. For 171 | // example, if the user has three fingers on the touch surface (such as a 172 | // screen or trackpad), the corresponding TouchList object would have one 173 | // Touch object for each finger, for a total of three entries. 174 | type TouchList []Touch 175 | 176 | // Touch represents a single contact point on a touch-sensitive device. 177 | // The contact point is commonly a finger or stylus and the device may be a 178 | // touchscreen or trackpad. 179 | type Touch struct { 180 | // Identifier is a unique identifier for this Touch object. A given touch 181 | // point (say, by a finger) will have the same identifier for the duration 182 | // of its movement around the surface. This lets you ensure that you're 183 | // tracking the same touch all the time. 184 | Identifier uint32 185 | // The X coordinate of the touch point. 186 | X int 187 | // The Y coordinate of the touch point. 188 | Y int 189 | } 190 | 191 | // The TouchStartEvent is fired when one or more touch points are placed on 192 | // the touch surface. 193 | type TouchStartEvent struct{ TouchEvent } 194 | 195 | func (e TouchStartEvent) mask() eventMask { return maskTouchStart } 196 | 197 | // The TouchMoveEvent is fired when one or more touch points are moved along 198 | // the touch surface. 199 | type TouchMoveEvent struct{ TouchEvent } 200 | 201 | func (e TouchMoveEvent) mask() eventMask { return maskTouchMove } 202 | 203 | // The TouchEndEvent is fired when one or more touch points are removed from 204 | // the touch surface. 205 | type TouchEndEvent struct{ TouchEvent } 206 | 207 | func (e TouchEndEvent) mask() eventMask { return maskTouchEnd } 208 | 209 | // The TouchCancelEvent is fired when one or more touch points have been 210 | // disrupted in an implementation-specific manner (for example, too many touch 211 | // points are created). 212 | type TouchCancelEvent struct{ TouchEvent } 213 | 214 | func (e TouchCancelEvent) mask() eventMask { return maskTouchCancel } 215 | 216 | type ClipboardEvent struct { 217 | Data string 218 | } 219 | 220 | type ClipboardChangeEvent struct{ ClipboardEvent } 221 | 222 | func (e ClipboardChangeEvent) mask() eventMask { return maskClipboardChange } 223 | 224 | type modifierKeys byte 225 | 226 | const ( 227 | modKeyAlt modifierKeys = 1 << iota 228 | modKeyShift 229 | modKeyCtrl 230 | modKeyMeta 231 | ) 232 | 233 | // AltKey returns true if the Alt (Option or ⌥ on OS X) key was active when 234 | // the event was generated. 235 | func (m modifierKeys) AltKey() bool { 236 | return m.isPressed(modKeyAlt) 237 | } 238 | 239 | // ShiftKey returns true if the Shift key was active when the event was 240 | // generated. 241 | func (m modifierKeys) ShiftKey() bool { 242 | return m.isPressed(modKeyShift) 243 | } 244 | 245 | // CtrlKey returns true if the Ctrl key was active when the event was 246 | // generated. 247 | func (m modifierKeys) CtrlKey() bool { 248 | return m.isPressed(modKeyCtrl) 249 | } 250 | 251 | // MetaKey returns true if the Meta key (on Mac keyboards, the ⌘ Command key; 252 | // on Windows keyboards, the Windows key (⊞)) was active when the event 253 | // was generated. 254 | func (m modifierKeys) MetaKey() bool { 255 | return m.isPressed(modKeyMeta) 256 | } 257 | 258 | func (m modifierKeys) isPressed(k modifierKeys) bool { 259 | return m&k != 0 260 | } 261 | 262 | type eventMask int 263 | 264 | const ( 265 | maskMouseMove eventMask = 1 << iota 266 | maskMouseDown 267 | maskMouseUp 268 | maskKeyDown 269 | maskKeyUp 270 | maskClick 271 | maskDblClick 272 | maskAuxClick 273 | maskWheel 274 | maskTouchStart 275 | maskTouchMove 276 | maskTouchEnd 277 | maskTouchCancel 278 | maskClipboardChange 279 | ) 280 | 281 | // MouseButtons is a number representing one or more buttons. For more than 282 | // one button pressed simultaneously, the values are combined (e.g., 3 is 283 | // ButtonPrimary + ButtonSecondary). 284 | type MouseButtons int 285 | 286 | const ( 287 | // ButtonPrimary is the primary button (usually the left button). 288 | ButtonPrimary MouseButtons = 1 << iota 289 | // ButtonSecondary is the secondary button (usually the right button). 290 | ButtonSecondary 291 | // ButtonAuxiliary is the auxiliary button (usually the mouse wheel button 292 | // or middle button) 293 | ButtonAuxiliary 294 | // Button4th is the 4th button (typically the "Browser Back" button). 295 | Button4th 296 | // Button5th is the 5th button (typically the "Browser Forward" button). 297 | Button5th 298 | // ButtonNone stands for no button or un-initialized. 299 | ButtonNone MouseButtons = 0 300 | ) 301 | 302 | const ( 303 | evMouseMove byte = 1 + iota 304 | evMouseDown 305 | evMouseUp 306 | evKeyDown 307 | evKeyUp 308 | evClick 309 | evDblClick 310 | evAuxClick 311 | evWheel 312 | evTouchStart 313 | evTouchMove 314 | evTouchEnd 315 | evTouchCancel 316 | evClipboardChange 317 | ) 318 | 319 | func decodeEvent(p []byte) (Event, error) { 320 | buf := &buffer{bytes: p} 321 | event, err := decodeEventBuf(buf) 322 | if buf.error != nil { 323 | return nil, buf.error 324 | } 325 | return event, err 326 | } 327 | 328 | func decodeEventBuf(buf *buffer) (Event, error) { 329 | eventType := buf.readByte() 330 | switch eventType { 331 | case evMouseMove: 332 | return MouseMoveEvent{decodeMouseEvent(buf)}, nil 333 | case evMouseDown: 334 | return MouseDownEvent{decodeMouseEvent(buf)}, nil 335 | case evMouseUp: 336 | return MouseUpEvent{decodeMouseEvent(buf)}, nil 337 | case evKeyDown: 338 | return KeyDownEvent{decodeKeyboardEvent(buf)}, nil 339 | case evKeyUp: 340 | return KeyUpEvent{decodeKeyboardEvent(buf)}, nil 341 | case evClick: 342 | return ClickEvent{decodeMouseEvent(buf)}, nil 343 | case evDblClick: 344 | return DblClickEvent{decodeMouseEvent(buf)}, nil 345 | case evAuxClick: 346 | return AuxClickEvent{decodeMouseEvent(buf)}, nil 347 | case evWheel: 348 | return decodeWheelEvent(buf), nil 349 | case evTouchStart: 350 | return TouchStartEvent{decodeTouchEvent(buf)}, nil 351 | case evTouchMove: 352 | return TouchMoveEvent{decodeTouchEvent(buf)}, nil 353 | case evTouchEnd: 354 | return TouchEndEvent{decodeTouchEvent(buf)}, nil 355 | case evTouchCancel: 356 | return TouchCancelEvent{decodeTouchEvent(buf)}, nil 357 | case evClipboardChange: 358 | return ClipboardChangeEvent{decodeClipboardEvent(buf)}, nil 359 | } 360 | return nil, errUnknownEventType{unknownType: eventType} 361 | } 362 | 363 | func decodeMouseEvent(buf *buffer) MouseEvent { 364 | return MouseEvent{ 365 | Buttons: MouseButtons(buf.readByte()), 366 | X: int(buf.readUint32()), 367 | Y: int(buf.readUint32()), 368 | modifierKeys: modifierKeys(buf.readByte()), 369 | } 370 | } 371 | 372 | func decodeKeyboardEvent(buf *buffer) KeyboardEvent { 373 | return KeyboardEvent{ 374 | modifierKeys: modifierKeys(buf.readByte()), 375 | Key: buf.readString(), 376 | } 377 | } 378 | 379 | func decodeWheelEvent(buf *buffer) WheelEvent { 380 | return WheelEvent{ 381 | MouseEvent: decodeMouseEvent(buf), 382 | DeltaX: buf.readFloat64(), 383 | DeltaY: buf.readFloat64(), 384 | DeltaZ: buf.readFloat64(), 385 | DeltaMode: DeltaMode(buf.readByte()), 386 | } 387 | } 388 | 389 | func decodeTouchEvent(buf *buffer) TouchEvent { 390 | return TouchEvent{ 391 | Touches: decodeTouchList(buf), 392 | ChangedTouches: decodeTouchList(buf), 393 | TargetTouches: decodeTouchList(buf), 394 | modifierKeys: modifierKeys(buf.readByte()), 395 | } 396 | } 397 | 398 | func decodeTouchList(buf *buffer) TouchList { 399 | length := buf.readByte() 400 | list := make(TouchList, length) 401 | for i := range list { 402 | list[i] = decodeTouch(buf) 403 | } 404 | return list 405 | } 406 | 407 | func decodeTouch(buf *buffer) Touch { 408 | return Touch{ 409 | Identifier: buf.readUint32(), 410 | X: int(buf.readUint32()), 411 | Y: int(buf.readUint32()), 412 | } 413 | } 414 | 415 | func decodeClipboardEvent(buf *buffer) ClipboardEvent { 416 | return ClipboardEvent{ 417 | Data: buf.readString(), 418 | } 419 | } 420 | 421 | type errUnknownEventType struct { 422 | unknownType byte 423 | } 424 | 425 | func (err errUnknownEventType) Error() string { 426 | return fmt.Sprintf("unknown event type: %#x", err.unknownType) 427 | } 428 | -------------------------------------------------------------------------------- /risc/risc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Package risc implements emulation of the Project Oberon RISC processor. 6 | package risc 7 | 8 | import ( 9 | "image" 10 | 11 | "github.com/fzipp/oberon/risc/internal/fp" 12 | ) 13 | 14 | const ( 15 | FramebufferWidth = 1024 16 | FramebufferHeight = 768 17 | ) 18 | 19 | // Our memory layout is slightly different from the FPGA implementation: 20 | // The FPGA uses a 20-bit address bus and thus ignores the top 12 bits, 21 | // while we use all 32 bits. This allows us to have more than 1 megabyte 22 | // of RAM. 23 | // 24 | // In the default configuration, the emulator is compatible with the 25 | // FPGA system. But If the user requests more memory, we move the 26 | // framebuffer to make room for a larger Oberon heap. This requires a 27 | // custom Display.Mod. 28 | 29 | const ( 30 | defaultMemSize = 0x00100000 // 1 MiB 31 | defaultDisplayStart = 0x000E7F00 32 | ) 33 | 34 | const ( 35 | romStart = 0xFFFFF800 36 | romWords = 512 37 | ioStart = 0xFFFFFFC0 38 | ) 39 | 40 | type RISC struct { 41 | PC uint32 // Program counter 42 | R [16]uint32 // General registers R0..R15 43 | H uint32 // Auxiliary register for high 32 bits of multiplication or remainder of division 44 | Z bool // Zero flag 45 | N bool // Negative flag 46 | C bool // Carry flag 47 | V bool // Overflow flag 48 | 49 | displayStart uint32 50 | 51 | progress uint32 52 | millisecondCounter uint32 53 | mouse uint32 54 | keyBuf []byte 55 | switches uint32 56 | 57 | leds LED 58 | serial Serial 59 | spiSelected uint32 60 | spi [4]SPI 61 | clipboard Clipboard 62 | 63 | framebuffer Framebuffer 64 | damage image.Rectangle 65 | 66 | Mem []uint32 // Memory 67 | rom [romWords]uint32 68 | } 69 | 70 | // RISC instructions set 71 | const ( 72 | // Moving and shifting 73 | opMOV = iota 74 | opLSL 75 | opASR 76 | opROR 77 | // Logical operations 78 | opAND 79 | opANN 80 | opIOR 81 | opXOR 82 | // Integer arithmetic 83 | opADD 84 | opSUB 85 | opMUL 86 | opDIV 87 | // Floating-point arithmetic 88 | opFAD 89 | opFSB 90 | opFML 91 | opFDV 92 | ) 93 | 94 | func New() *RISC { 95 | screenWidth := FramebufferWidth 96 | screenHeight := FramebufferHeight 97 | r := &RISC{ 98 | displayStart: defaultDisplayStart, 99 | } 100 | columns := screenWidth / 32 101 | r.damage = image.Rect(0, 0, columns-1, screenHeight-1) 102 | r.Mem = make([]uint32, defaultMemSize/4) 103 | r.framebuffer = Framebuffer{ 104 | Rect: image.Rect(0, 0, screenWidth, screenHeight), 105 | Pix: r.Mem[r.displayStart/4:], 106 | } 107 | r.rom = bootloader 108 | r.Reset() 109 | return r 110 | } 111 | 112 | func (r *RISC) ConfigureMemory(megabytesRAM, screenWidth, screenHeight int) { 113 | megabytesRAM = clamp(megabytesRAM, 1, 32) 114 | 115 | r.displayStart = uint32(megabytesRAM << 20) 116 | columns := screenWidth / 32 117 | r.damage = image.Rect(0, 0, columns-1, screenHeight-1) 118 | 119 | memSize := r.displayStart + uint32((screenWidth*screenHeight)/8) 120 | r.Mem = make([]uint32, memSize/4) 121 | r.framebuffer = Framebuffer{ 122 | Rect: image.Rect(0, 0, screenWidth, screenHeight), 123 | Pix: r.Mem[r.displayStart/4:], 124 | } 125 | 126 | // Patch the new constants in the bootloader. 127 | memLim := r.displayStart - 16 128 | r.rom[372] = 0x61000000 + (memLim >> 16) 129 | r.rom[373] = 0x41160000 + (memLim & 0x0000FFFF) 130 | stackOrg := r.displayStart / 2 131 | r.rom[376] = 0x61000000 + (stackOrg >> 16) 132 | 133 | // Inform the display driver of the framebuffer layout. 134 | // This isn't a very pretty mechanism, but this way our disk images 135 | // should still boot on the standard FPGA system. 136 | r.Mem[defaultDisplayStart/4] = 0x53697A67 137 | r.Mem[defaultDisplayStart/4+1] = uint32(screenWidth) 138 | r.Mem[defaultDisplayStart/4+2] = uint32(screenHeight) 139 | r.Mem[defaultDisplayStart/4+3] = r.displayStart 140 | 141 | r.Reset() 142 | } 143 | 144 | func (r *RISC) SetLEDs(l LED) { 145 | r.leds = l 146 | } 147 | 148 | func (r *RISC) SetSerial(s Serial) { 149 | r.serial = s 150 | } 151 | 152 | func (r *RISC) SetSPI(index int, spi SPI) { 153 | if index == 1 || index == 2 { 154 | r.spi[index] = spi 155 | } 156 | } 157 | 158 | func (r *RISC) SetClipboard(c Clipboard) { 159 | r.clipboard = c 160 | } 161 | 162 | func (r *RISC) SetSwitches(s int) { 163 | r.switches = uint32(s) 164 | } 165 | 166 | func (r *RISC) Reset() { 167 | r.PC = romStart / 4 168 | } 169 | 170 | func (r *RISC) Run(cycles int) error { 171 | r.progress = 20 172 | // The progress value is used to detect that the RISC cpu is busy 173 | // waiting on the millisecond counter or on the keyboard ready 174 | // bit. In that case it's better to just pause emulation until the 175 | // next frame. 176 | for i := 0; i < cycles && r.progress > 0; i++ { 177 | err := r.singleStep() 178 | if err != nil { 179 | r.Reset() 180 | return err 181 | } 182 | } 183 | return nil 184 | } 185 | 186 | func (r *RISC) singleStep() error { 187 | var IR uint32 // Instruction register 188 | if r.PC < uint32(len(r.Mem)) { 189 | IR = r.Mem[r.PC] 190 | } else if r.PC >= romStart/4 && r.PC < romStart/4+romWords { 191 | IR = r.rom[r.PC-romStart/4] 192 | } else { 193 | return &Error{PC: r.PC, message: "branched into the void"} 194 | } 195 | r.PC++ 196 | 197 | const ( 198 | pBit = 0x80000000 199 | qBit = 0x40000000 200 | uBit = 0x20000000 201 | vBit = 0x10000000 202 | ) 203 | 204 | if (IR & pBit) == 0 { 205 | // Register instructions (formats F0 and F1) 206 | 207 | a := (IR & 0x0F000000) >> 24 208 | b := (IR & 0x00F00000) >> 20 209 | op := (IR & 0x000F0000) >> 16 210 | 211 | var n uint32 212 | if (IR & qBit) == 0 { 213 | // F0 214 | c := IR & 0x0000000F 215 | Rc := r.R[c] 216 | n = Rc 217 | } else { 218 | // F1 219 | im := IR & 0x0000FFFF 220 | if (IR & vBit) == 0 { 221 | // 0-extend n 222 | n = im 223 | } else { 224 | // 1-extend n 225 | n = im | 0xFFFF0000 226 | } 227 | } 228 | 229 | var Ra uint32 230 | Rb := r.R[b] 231 | 232 | switch op { 233 | case opMOV: 234 | if (IR & uBit) == 0 { 235 | Ra = n 236 | } else { 237 | // Special features 238 | if (IR & qBit) == 0 { 239 | // F0 240 | if (IR & vBit) == 0 { 241 | Ra = r.H 242 | } else { 243 | // From RISC5.v: 244 | // {N, Z, C, OV, 20'b0, 8'h53} 245 | NZCV := (b2i(r.N) * 0b1000) | 246 | (b2i(r.Z) * 0b0100) | 247 | (b2i(r.C) * 0b0010) | 248 | (b2i(r.V) * 0b0001) 249 | Ra = (NZCV << 28) | 0x53 250 | } 251 | } else { 252 | // F1 253 | Ra = n << 16 254 | } 255 | } 256 | case opLSL: 257 | // shift left by n bits 258 | Ra = Rb << (n & 31) 259 | case opASR: 260 | // shift right by n bits with sign extension 261 | Ra = uint32(int32(Rb) >> (n & 31)) 262 | case opROR: 263 | // rotate right by n bits 264 | Ra = (Rb >> (n & 31)) | (Rb << (-n & 31)) 265 | case opAND: 266 | Ra = Rb & n 267 | case opANN: 268 | Ra = Rb &^ n 269 | case opIOR: 270 | Ra = Rb | n 271 | case opXOR: 272 | Ra = Rb ^ n 273 | case opADD: 274 | Ra = Rb + n 275 | if (IR&uBit) != 0 && r.C { 276 | // ADD' (add also carry C) 277 | Ra++ 278 | } 279 | r.C = Ra < Rb 280 | r.V = (((Ra ^ n) & (Ra ^ Rb)) >> 31) > 0 281 | case opSUB: 282 | Ra = Rb - n 283 | if (IR&uBit) != 0 && r.C { 284 | // SUB' (subtract also carry C) 285 | Ra-- 286 | } 287 | r.C = Ra > Rb 288 | r.V = (((Rb ^ n) & (Ra ^ Rb)) >> 31) > 0 289 | case opMUL: 290 | var tmp uint64 291 | if (IR & uBit) == 0 { 292 | tmp = uint64(int64(int32(Rb)) * int64(int32(n))) 293 | } else { 294 | // MUL' (unsigned multiplication) 295 | tmp = uint64(Rb) * uint64(n) 296 | } 297 | Ra = uint32(tmp) 298 | r.H = uint32(tmp >> 32) 299 | case opDIV: 300 | if int32(n) > 0 { 301 | if (IR & uBit) == 0 { 302 | Ra = uint32(int32(Rb) / int32(n)) 303 | r.H = uint32(int32(Rb) % int32(n)) 304 | if int32(r.H) < 0 { 305 | Ra-- 306 | r.H += n 307 | } 308 | } else { 309 | Ra = Rb / n 310 | r.H = Rb % n 311 | } 312 | } else { 313 | q := fp.Idiv(Rb, n, IR&uBit > 0) 314 | Ra = q.Quot 315 | r.H = q.Rem 316 | } 317 | case opFAD: 318 | Ra = fp.Add(Rb, n, IR&uBit > 0, IR&vBit > 0) 319 | case opFSB: 320 | Ra = fp.Add(Rb, n^0x80000000, IR&uBit > 0, IR&vBit > 0) 321 | case opFML: 322 | Ra = fp.Mul(Rb, n) 323 | case opFDV: 324 | Ra = fp.Div(Rb, n) 325 | default: 326 | panic("unreachable") 327 | } 328 | r.setRegister(a, Ra) 329 | } else if (IR & qBit) == 0 { 330 | // Memory instructions (format F2) 331 | 332 | a := (IR & 0x0F000000) >> 24 333 | b := (IR & 0x00F00000) >> 20 334 | im := int32(IR & 0x000FFFFF) 335 | off := uint32((im ^ 0x00080000) - 0x00080000) // sign-extend 336 | 337 | address := r.R[b] + off 338 | if (IR & uBit) == 0 { 339 | // LD (load) 340 | var Ra uint32 341 | if (IR & vBit) == 0 { 342 | // word access 343 | Ra = r.loadWord(address) 344 | } else { 345 | // single byte access 346 | Ra = uint32(r.loadByte(address)) 347 | } 348 | r.setRegister(a, Ra) 349 | } else { 350 | // ST (store) 351 | Ra := r.R[a] 352 | if (IR & vBit) == 0 { 353 | // word access 354 | r.storeWord(address, Ra) 355 | } else { 356 | // single byte access 357 | r.storeByte(address, byte(Ra)) 358 | } 359 | } 360 | } else { 361 | // Branch instructions (format F3) 362 | // TODO: interrupts? 363 | 364 | t := ((IR >> 27) & 1) > 0 365 | switch (IR >> 24) & 0b0111 { 366 | case 0b0000: // MI: negative (minus) 367 | t = t != r.N 368 | case 0b0001: // EQ: equal (zero) 369 | t = t != r.Z 370 | case 0b0010: // CS: carry set (lower) 371 | t = t != r.C 372 | case 0b0011: // VS: overflow set 373 | t = t != r.V 374 | case 0b0100: // LS: less or same 375 | // Note: RISC-Arch.pdf says ~C|Z (as of 2021-03-28), 376 | // but RISC5.v says (C|Z). The latter is the correct one. 377 | t = t != (r.C || r.Z) 378 | case 0b0101: // LT: less than 379 | t = t != (r.N != r.V) 380 | case 0b0110: // LE: less or equal 381 | t = t != ((r.N != r.V) || r.Z) 382 | case 0b0111: // always 383 | t = !t 384 | default: 385 | panic("unreachable") 386 | } 387 | if t { 388 | if (IR & vBit) != 0 { 389 | const LNK = 15 // R15 is the link register 390 | r.setRegister(LNK, r.PC*4) 391 | } 392 | if (IR & uBit) == 0 { 393 | c := IR & 0x0000000F 394 | r.PC = r.R[c] / 4 395 | } else { 396 | off := int32(IR & 0x00FFFFFF) 397 | off = (off ^ 0x00800000) - 0x00800000 // sign-extend 398 | r.PC = r.PC + uint32(off) 399 | } 400 | } 401 | } 402 | return nil 403 | } 404 | 405 | func (r *RISC) setRegister(reg uint32, value uint32) { 406 | r.R[reg] = value 407 | r.Z = value == 0 408 | r.N = int32(value) < 0 409 | } 410 | 411 | func (r *RISC) loadWord(address uint32) uint32 { 412 | if address < uint32(r.memSize()) { 413 | return r.Mem[address/4] 414 | } 415 | return r.loadIO(address) 416 | } 417 | 418 | func (r *RISC) loadByte(address uint32) byte { 419 | w := r.loadWord(address) 420 | return byte(w >> (address % 4 * 8)) 421 | } 422 | 423 | func (r *RISC) updateDamage(word int) { 424 | columns := r.framebuffer.Rect.Max.X / 32 425 | row := word / columns 426 | if row >= r.framebuffer.Rect.Max.Y { 427 | return 428 | } 429 | col := word % columns 430 | if col < r.damage.Min.X { 431 | r.damage.Min.X = col 432 | } 433 | if col > r.damage.Max.X { 434 | r.damage.Max.X = col 435 | } 436 | if row < r.damage.Min.Y { 437 | r.damage.Min.Y = row 438 | } 439 | if row > r.damage.Max.Y { 440 | r.damage.Max.Y = row 441 | } 442 | } 443 | 444 | func (r *RISC) storeWord(address, value uint32) { 445 | if address < r.displayStart { 446 | r.Mem[address/4] = value 447 | } else if address < uint32(r.memSize()) { 448 | r.Mem[address/4] = value 449 | r.updateDamage(int(address/4 - r.displayStart/4)) 450 | } else { 451 | r.storeIO(address, value) 452 | } 453 | } 454 | 455 | func (r *RISC) storeByte(address uint32, value byte) { 456 | if address < uint32(r.memSize()) { 457 | w := r.loadWord(address) 458 | shift := (address & 3) * 8 459 | w &= ^(0xFF << shift) 460 | w |= uint32(value) << shift 461 | r.storeWord(address, w) 462 | } else { 463 | r.storeIO(address, uint32(value)) 464 | } 465 | } 466 | 467 | func (r *RISC) loadIO(address uint32) uint32 { 468 | switch address - ioStart { 469 | case 0: 470 | // Millisecond counter 471 | r.progress-- 472 | return r.millisecondCounter 473 | case 4: 474 | // Switches 475 | return r.switches 476 | case 8: 477 | // RS-232 data 478 | if r.serial == nil { 479 | return 0 480 | } 481 | return r.serial.ReadData() 482 | case 12: 483 | // RS-232 status 484 | if r.serial == nil { 485 | return 0 486 | } 487 | return r.serial.ReadStatus() 488 | case 16: 489 | // disk, net SPI data 490 | spi := r.spi[r.spiSelected] 491 | if spi == nil { 492 | return 255 493 | } 494 | return spi.ReadData() 495 | case 20: 496 | // disk, net SPI status 497 | // Bit 0: rx ready 498 | // Other bits unused 499 | return 1 500 | case 24: 501 | // Mouse input / keyboard status 502 | mouse := r.mouse 503 | if len(r.keyBuf) > 0 { 504 | mouse |= 0x10000000 505 | } else { 506 | r.progress-- 507 | } 508 | return mouse 509 | case 28: 510 | // Keyboard data (PS2) 511 | if len(r.keyBuf) == 0 { 512 | return 0 513 | } 514 | scancode := r.keyBuf[0] 515 | r.keyBuf = r.keyBuf[1:] 516 | return uint32(scancode) 517 | case 40: 518 | // Clipboard control 519 | if r.clipboard == nil { 520 | return 0 521 | } 522 | return r.clipboard.ReadControl() 523 | case 44: 524 | // Clipboard data 525 | if r.clipboard == nil { 526 | return 0 527 | } 528 | return r.clipboard.ReadData() 529 | default: 530 | return 0 531 | } 532 | } 533 | 534 | func (r *RISC) storeIO(address, value uint32) { 535 | switch address - ioStart { 536 | case 4: 537 | // LEDs 538 | if r.leds != nil { 539 | r.leds.Write(value) 540 | } 541 | case 8: 542 | // RS-232 data 543 | if r.serial != nil { 544 | r.serial.WriteData(value) 545 | } 546 | case 16: 547 | // SPI data 548 | spi := r.spi[r.spiSelected] 549 | if spi != nil { 550 | spi.WriteData(value) 551 | } 552 | case 20: 553 | // SPI control 554 | // Bit 0-1: chip select 555 | // Bit 2: fast mode 556 | // Bit 3: network enable 557 | // Other bits unused 558 | r.spiSelected = value & 0b0011 559 | case 40: 560 | // Clipboard control 561 | if r.clipboard != nil { 562 | r.clipboard.WriteControl(value) 563 | } 564 | case 44: 565 | // Clipboard data 566 | if r.clipboard != nil { 567 | r.clipboard.WriteData(value) 568 | } 569 | } 570 | } 571 | 572 | func (r *RISC) SetTime(millis uint32) { 573 | r.millisecondCounter = millis 574 | } 575 | 576 | func (r *RISC) MouseMoved(x, y int) { 577 | if x >= 0 && x <= 0xFFF { 578 | r.mouse = (r.mouse &^ 0x00000FFF) | uint32(x) 579 | } 580 | if y >= 0 && y <= 0xFFF { 581 | r.mouse = (r.mouse &^ 0x00FFF000) | (uint32(y) << 12) 582 | } 583 | } 584 | 585 | func (r *RISC) MouseButton(button int, down bool) { 586 | if button < 1 || button > 3 { 587 | return 588 | } 589 | bit := uint32(1 << (27 - button)) 590 | if down { 591 | r.mouse = r.mouse | bit 592 | } else { 593 | r.mouse = r.mouse &^ bit 594 | } 595 | } 596 | 597 | func (r *RISC) KeyboardInput(ps2commands []byte) { 598 | r.keyBuf = append(r.keyBuf, ps2commands...) 599 | } 600 | 601 | func (r *RISC) Framebuffer() *Framebuffer { 602 | return &r.framebuffer 603 | } 604 | 605 | func (r *RISC) GetFramebufferDamageAndReset() image.Rectangle { 606 | d := r.damage 607 | r.resetFramebufferDamage() 608 | return d 609 | } 610 | 611 | func (r *RISC) resetFramebufferDamage() { 612 | r.damage = image.Rectangle{ 613 | Min: image.Point{X: r.framebuffer.Rect.Max.X / 32, Y: r.framebuffer.Rect.Max.Y}, 614 | Max: image.Point{}, 615 | } 616 | } 617 | 618 | // memSize returns the memory size in bytes 619 | func (r *RISC) memSize() int { 620 | return len(r.Mem) * 4 621 | } 622 | 623 | func b2i(b bool) uint32 { 624 | if b { 625 | return 1 626 | } 627 | return 0 628 | } 629 | 630 | func clamp(x, min, max int) int { 631 | if x < min { 632 | return min 633 | } 634 | if x > max { 635 | return max 636 | } 637 | return x 638 | } 639 | 640 | type Error struct { 641 | PC uint32 642 | message string 643 | } 644 | 645 | func (e *Error) Error() string { 646 | return e.message 647 | } 648 | -------------------------------------------------------------------------------- /risc/internal/fp/test/v2go/v2go.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Frederik Zipp and others; see NOTICE file. 2 | // Use of this source code is governed by the ISC license that 3 | // can be found in the LICENSE file. 4 | 5 | // Command v2go translates Verilog source code to Go. 6 | package main 7 | 8 | import ( 9 | "bytes" 10 | "flag" 11 | "fmt" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | "regexp" 16 | "strconv" 17 | "strings" 18 | "unicode" 19 | ) 20 | 21 | func usage() { 22 | fail(`Translates Verilog source code to Go. 23 | 24 | Usage: 25 | v2go [-o go_file] [verilog_file] 26 | 27 | If no Verilog file is specified the tool reads from the standard input. 28 | If the VERILOG environment variable is set 'verilog_file' is interpreted 29 | relative to the path of this environment variable. 30 | 31 | Flags: 32 | -o Specifies the output file (Go). Default: standard output`) 33 | } 34 | 35 | func main() { 36 | oFlag := flag.String("o", "", "output `file` (Go)") 37 | flag.Usage = usage 38 | flag.Parse() 39 | 40 | var err error 41 | 42 | in := os.Stdin 43 | if flag.NArg() > 0 { 44 | path := flag.Arg(0) 45 | verilogEnv := os.Getenv("VERILOG") 46 | if verilogEnv != "" { 47 | path = filepath.Join(verilogEnv, path) 48 | } 49 | in, err = os.Open(path) 50 | check(err) 51 | defer in.Close() 52 | } 53 | check(err) 54 | 55 | out := os.Stdout 56 | if *oFlag != "" { 57 | out, err = os.Create(*oFlag) 58 | check(err) 59 | defer out.Close() 60 | } 61 | 62 | data, err := io.ReadAll(in) 63 | check(err) 64 | err = newParser(data, out).parseProgram() 65 | check(err) 66 | } 67 | 68 | type scanner struct { 69 | str []byte 70 | pos int 71 | nextPos int 72 | } 73 | 74 | var keywords = []string{ 75 | "module", "input", "output", "reg", "wire", "assign", 76 | "always", "posedge", "begin", "end", "endmodule", 77 | } 78 | 79 | var scanRegexp = regexp.MustCompile(stripSpaces(` 80 | (?P [_a-zA-Z][a-zA-Z0-9]* ) | 81 | (?P (?P[0-9]+) 'b (?P[0-1]+) ) | 82 | (?P (?P[0-9]+) 'h (?P[0-9A-Fa-f]+) ) | 83 | (?P [0-9]+ ) | 84 | (?P == | <= | [();\[:\]=^+-?{}~&|@] ) | 85 | \\Z`)) 86 | 87 | var skipRegexp = regexp.MustCompile("(?://.*|`.*|\\s+)*") 88 | 89 | func newScanner(str []byte) *scanner { 90 | s := &scanner{str: str} 91 | s.skipWhitespaceAndComments(0) 92 | return s 93 | } 94 | 95 | func (s *scanner) skipWhitespaceAndComments(pos int) { 96 | match := skipRegexp.FindAllSubmatchIndex(s.str[pos:], 1) 97 | if match == nil { 98 | return 99 | } 100 | s.nextPos = pos + match[0][1] 101 | } 102 | 103 | func (s *scanner) next() (token string, value any) { 104 | s.pos = s.nextPos 105 | groups, _, end := findSubmatchGroups(scanRegexp, s.str[s.pos:]) 106 | if groups == nil { 107 | panic(s.error("next token not found")) 108 | } 109 | s.skipWhitespaceAndComments(s.pos + end) 110 | if w, ok := groups["word"]; ok { 111 | for _, kw := range keywords { 112 | if w == kw { 113 | return w, "" 114 | } 115 | } 116 | return "", w 117 | } else if _, ok = groups["bits"]; ok { 118 | bitlen, err := strconv.Atoi(groups["bitlen"]) 119 | if err != nil { 120 | panic(s.error("invalid bitlen")) 121 | } 122 | bitdata, err := strconv.ParseInt(groups["bitdata"], 2, 0) 123 | if err != nil { 124 | panic(s.error("invalid bitdata")) 125 | } 126 | return "", bits{size: bitlen, value: bitdata} 127 | } else if _, ok = groups["hex"]; ok { 128 | hexlen, err := strconv.Atoi(groups["hexlen"]) 129 | if err != nil { 130 | panic(s.error("invalid hexlen")) 131 | } 132 | hexdata, err := strconv.ParseInt(groups["hexdata"], 16, 0) 133 | if err != nil { 134 | panic(s.error("invalid hexdata")) 135 | } 136 | return "", bits{size: hexlen, value: hexdata} 137 | } else if intStr, ok := groups["int"]; ok { 138 | n, err := strconv.Atoi(intStr) 139 | if err != nil { 140 | panic(s.error("invalid int")) 141 | } 142 | return "", n 143 | } else if sym, ok := groups["sym"]; ok { 144 | return sym, nil 145 | } 146 | return "", nil 147 | } 148 | 149 | func (s *scanner) where() (line, col int) { 150 | line = bytes.Count(s.str[:s.pos], []byte{'\n'}) + 1 151 | col = s.pos - bytes.LastIndexByte(s.str[:s.pos], '\n') 152 | return line, col 153 | } 154 | 155 | func (s *scanner) error(what string) scanError { 156 | line, col := s.where() 157 | return scanError{what: what, line: line, col: col} 158 | } 159 | 160 | type scanError struct { 161 | what string 162 | line, col int 163 | } 164 | 165 | func (e scanError) Error() string { 166 | return fmt.Sprintf("scan errorf at line %d, column %d: %s", e.line, e.col, e.what) 167 | } 168 | 169 | type parser struct { 170 | names map[string]bits 171 | registers map[string]bits 172 | registerNames []string 173 | bitByBit map[string][]any 174 | scanner *scanner 175 | token string 176 | value any 177 | w io.Writer 178 | } 179 | 180 | func newParser(text []byte, out io.Writer) *parser { 181 | p := &parser{ 182 | names: make(map[string]bits), 183 | registers: make(map[string]bits), 184 | bitByBit: make(map[string][]any), 185 | scanner: newScanner(text), 186 | w: out, 187 | } 188 | p.nextToken() 189 | return p 190 | } 191 | 192 | func (p *parser) nextToken() { 193 | p.token, p.value = p.scanner.next() 194 | } 195 | 196 | func (p *parser) expected(tokens ...string) { 197 | line, col := p.scanner.where() 198 | panic(p.errorf("at line %d, column %d: expected '%s', found '%s'", 199 | line, col, strings.Join(tokens, " "), p.token)) 200 | } 201 | 202 | func (p *parser) skipOver(token string) { 203 | if p.token != token { 204 | p.expected(token) 205 | } 206 | p.nextToken() 207 | } 208 | 209 | func (p *parser) skippingOver(tokens ...string) bool { 210 | for _, tok := range tokens { 211 | if tok == p.token { 212 | p.nextToken() 213 | return true 214 | } 215 | } 216 | return false 217 | } 218 | 219 | func (p *parser) name() string { 220 | n := p.value.(string) 221 | p.skipOver("") 222 | return n 223 | } 224 | 225 | func (p *parser) int() int { 226 | n := p.value.(int) 227 | p.skipOver("") 228 | return n 229 | } 230 | 231 | func (p *parser) parseProgram() (err error) { 232 | defer func() { 233 | rec := recover() 234 | if rec == nil { 235 | return 236 | } 237 | if recErr, ok := rec.(error); ok { 238 | err = recErr 239 | return 240 | } 241 | panic(rec) 242 | }() 243 | 244 | fmt.Fprint(p.w, `// Code generated by v2go; DO NOT EDIT. 245 | 246 | package main 247 | 248 | `) 249 | 250 | p.parseModule() 251 | for p.token != "endmodule" { 252 | if p.skippingOver("reg") { 253 | p.parseReg() 254 | } else if p.skippingOver("wire") { 255 | p.parseWire() 256 | } else if p.skippingOver("assign") { 257 | p.parseAssign() 258 | } else if p.skippingOver("always") { 259 | p.parseAlways() 260 | } else { 261 | panic(p.errorf("did not expect '%s'", p.token)) 262 | } 263 | } 264 | 265 | fmt.Fprintln(p.w, "\nfunc eq(a, b uint64) uint64 { if a == b { return 1 }; return 0 }") 266 | 267 | fmt.Fprintln(p.w, "\nfunc cycle() {") 268 | for _, n := range p.registerNames { 269 | expr := p.registers[n] 270 | size := p.names[n].size 271 | fmt.Fprintf(p.w, "\t%s_tmp := (%v) & %s\n", 272 | n, expr.value, p.mask(size)) 273 | } 274 | for _, n := range p.registerNames { 275 | fmt.Fprintf(p.w, "\t%s = %s_tmp\n", n, n) 276 | } 277 | fmt.Fprintln(p.w, "}") 278 | return nil 279 | } 280 | 281 | func (p *parser) getType(size int) string { 282 | return "uint64" 283 | } 284 | 285 | func (p *parser) declareInput(name string, size int) { 286 | typ := p.getType(size) 287 | p.names[name] = bits{size: size, value: name} 288 | fmt.Fprintf(p.w, "var %s %s\n", name, typ) 289 | } 290 | 291 | func (p *parser) declareWire(name string, size int) { 292 | p.names[name] = bits{size: size, value: name + "()"} 293 | } 294 | 295 | func (p *parser) declareReg(name string, size int) { 296 | typ := p.getType(size) 297 | p.names[name] = bits{size: size, value: name} 298 | fmt.Fprintf(p.w, "var %s %s\n", name, typ) 299 | } 300 | 301 | func (p *parser) parseModule() { 302 | p.skipOver("module") 303 | p.skipOver("") 304 | p.skipOver("(") 305 | for !p.skippingOver("endmodule") { 306 | if p.skippingOver(")") { 307 | break 308 | } else if p.skippingOver("input") { 309 | size := p.parseDeclBitlen() 310 | for p.token == "" { 311 | name := p.value.(string) 312 | p.declareInput(name, size) 313 | p.nextToken() 314 | if !p.skippingOver(",") { 315 | break 316 | } 317 | } 318 | } else if p.skippingOver("output") { 319 | size := p.parseDeclBitlen() 320 | for p.token == "" { 321 | name := p.value.(string) 322 | p.declareWire(name, size) 323 | p.nextToken() 324 | if !p.skippingOver(",") { 325 | break 326 | } 327 | } 328 | } else { 329 | panic(p.errorf("don't understand module statement: %s", p.token)) 330 | } 331 | } 332 | p.skipOver(";") 333 | } 334 | 335 | func (p *parser) parseDeclBitlen() int { 336 | if p.skippingOver("[") { 337 | n1 := p.int() 338 | p.skipOver(":") 339 | n2 := p.int() 340 | p.skipOver("]") 341 | if n2 != 0 { 342 | panic(p.errorf("end of range is not 0: %d,%d", n1, n2)) 343 | } 344 | return n1 + 1 345 | } 346 | return 1 347 | } 348 | 349 | func (p *parser) parseWire() { 350 | size := p.parseDeclBitlen() 351 | for { 352 | name := p.name() 353 | p.declareWire(name, size) 354 | if p.skippingOver(";") { 355 | break 356 | } 357 | p.skipOver(",") 358 | } 359 | } 360 | 361 | func (p *parser) parseReg() { 362 | size := p.parseDeclBitlen() 363 | for { 364 | name := p.name() 365 | p.declareReg(name, size) 366 | if p.skippingOver(";") { 367 | break 368 | } 369 | p.skipOver(",") 370 | } 371 | } 372 | 373 | func (p *parser) parseAssign() { 374 | name := p.name() 375 | size := p.names[name].size 376 | if p.skippingOver("[") { 377 | p.parseAssignBitByBit(name) 378 | } else { 379 | p.skipOver("=") 380 | a := p.parseExpression() 381 | p.skipOver(";") 382 | fmt.Fprintf(p.w, "func %s() %s { return (%v) & %s }\n", 383 | name, p.getType(size), a.value, p.mask(size)) 384 | } 385 | } 386 | 387 | func (p *parser) parseAssignBitByBit(name string) { 388 | size := p.names[name].size 389 | if _, ok := p.bitByBit[name]; !ok { 390 | p.bitByBit[name] = make([]any, size) 391 | } 392 | idx := p.int() 393 | p.skipOver("]") 394 | p.skipOver("=") 395 | a := p.parseExpression() 396 | if a.size != 1 { 397 | panic(p.errorf("expected a 1-bit expression")) 398 | } 399 | p.skipOver(";") 400 | p.bitByBit[name][idx] = a.value 401 | for _, b := range p.bitByBit[name] { 402 | if b == nil { 403 | return 404 | } 405 | } 406 | var parts []string 407 | for i := range size { 408 | parts = append(parts, fmt.Sprintf("((%v & 1) << %d)", p.bitByBit[name][i], i)) 409 | } 410 | reverseSlice(parts) 411 | expr := fmt.Sprintf("(%s)", strings.Join(parts, " | ")) 412 | fmt.Fprintf(p.w, "func %s() %s { return %s }\n", name, p.getType(size), expr) 413 | } 414 | 415 | func (p *parser) parseAlways() { 416 | p.skipOver("@") 417 | p.skipOver("(") 418 | p.skipOver("posedge") 419 | p.skipOver("(") 420 | p.skipOver("") 421 | p.skipOver(")") 422 | p.skipOver(")") 423 | if p.skippingOver("begin") { 424 | for !p.skippingOver("end") { 425 | p.parseRegAssign() 426 | } 427 | } else { 428 | p.parseRegAssign() 429 | } 430 | } 431 | 432 | func (p *parser) parseRegAssign() { 433 | n := p.name() 434 | p.skipOver("<=") 435 | e := p.parseExpression() 436 | p.skipOver(";") 437 | p.registers[n] = e 438 | p.registerNames = append(p.registerNames, n) 439 | } 440 | 441 | func (p *parser) parseExpression() bits { 442 | return p.parseExprTrinary() 443 | } 444 | 445 | func (p *parser) parseExprTrinary() bits { 446 | a := p.parseExprOr() 447 | if !p.skippingOver("?") { 448 | return a 449 | } 450 | b := p.parseExprTrinary() 451 | p.skipOver(":") 452 | c := p.parseExprTrinary() 453 | size := p.size(b, c) 454 | value := fmt.Sprintf("func() uint64 { if %v > 0 { return %v }; return %v }()", a.value, b.value, c.value) 455 | return bits{size: size, value: value} 456 | } 457 | 458 | func (p *parser) parseExprOr() bits { 459 | a := p.parseExprXor() 460 | for p.skippingOver("|") { 461 | b := p.parseExprXor() 462 | size := p.size(a, b) 463 | value := fmt.Sprintf("(%v | %v)", a.value, b.value) 464 | a = bits{size: size, value: value} 465 | } 466 | return a 467 | } 468 | 469 | func (p *parser) parseExprXor() bits { 470 | a := p.parseExprAnd() 471 | for p.skippingOver("^") { 472 | b := p.parseExprAnd() 473 | size := p.size(a, b) 474 | value := fmt.Sprintf("(%v ^ %v)", a.value, b.value) 475 | a = bits{size: size, value: value} 476 | } 477 | return a 478 | } 479 | 480 | func (p *parser) parseExprAnd() bits { 481 | a := p.parseExprEq() 482 | for p.skippingOver("&") { 483 | b := p.parseExprEq() 484 | size := p.size(a, b) 485 | value := fmt.Sprintf("(%v & %v)", a.value, b.value) 486 | a = bits{size: size, value: value} 487 | } 488 | return a 489 | } 490 | 491 | func (p *parser) parseExprEq() bits { 492 | a := p.parseExprPlus() 493 | for p.skippingOver("==") { 494 | b := p.parseExprPlus() 495 | value := fmt.Sprintf("eq(%v, %v)", a.value, b.value) 496 | a = bits{size: 1, value: value} 497 | } 498 | return a 499 | } 500 | 501 | func (p *parser) parseExprPlus() bits { 502 | a := p.parseExprConcat() 503 | for { 504 | op := p.token 505 | if !p.skippingOver("+", "-") { 506 | return a 507 | } 508 | b := p.parseExprConcat() 509 | size := p.size(a, b) 510 | value := fmt.Sprintf("(%v %s %v)", a.value, op, b.value) 511 | a = bits{size: size, value: value} 512 | } 513 | } 514 | 515 | func (p *parser) parseExprConcat() bits { 516 | if !p.skippingOver("{") { 517 | return p.parseExprUnaryPlus() 518 | } 519 | var exprs []bits 520 | for { 521 | if p.token == "" { 522 | exprs = append(exprs, p.parseExprRepeat()) 523 | } else { 524 | exprs = append(exprs, p.parseExpression()) 525 | } 526 | if !p.skippingOver(",") { 527 | break 528 | } 529 | } 530 | p.skipOver("}") 531 | size := 0 532 | var parts []string 533 | for i := len(exprs) - 1; i >= 0; i-- { 534 | b := exprs[i] 535 | parts = append(parts, fmt.Sprintf("((%v & %s) << %d)", b.value, p.mask(b.size), size)) 536 | size += b.size 537 | } 538 | reverseSlice(parts) 539 | return bits{size: size, value: fmt.Sprintf("(%s)", strings.Join(parts, " | "))} 540 | } 541 | 542 | func (p *parser) parseExprRepeat() bits { 543 | count := p.int() 544 | p.skipOver("{") 545 | expr := p.parseExprNumber() 546 | p.skipOver("}") 547 | if expr.size != 1 { 548 | panic(p.errorf("can only repeat 1-bit values")) 549 | } 550 | var parts []string 551 | for i := range count { 552 | parts = append(parts, fmt.Sprintf("((%v & 1) << %d)", expr.value, i)) 553 | } 554 | return bits{size: count, value: fmt.Sprintf("(%s)", strings.Join(parts, " | "))} 555 | } 556 | 557 | func (p *parser) parseExprUnaryPlus() bits { 558 | if p.skippingOver("+") || !p.skippingOver("-") { 559 | return p.parseExprNegation() 560 | } 561 | a := p.parseExprNumber() 562 | value := fmt.Sprintf("(-%v)", a.value) 563 | return bits{size: a.size, value: value} 564 | } 565 | 566 | func (p *parser) parseExprNegation() bits { 567 | if !p.skippingOver("~") { 568 | return p.parseExprNumber() 569 | } 570 | a := p.parseExprNumber() 571 | value := fmt.Sprintf("((^%v) & %s)", a.value, p.mask(a.size)) 572 | return bits{size: a.size, value: value} 573 | } 574 | 575 | func (p *parser) parseExprNumber() bits { 576 | var a bits 577 | if p.skippingOver("(") { 578 | a = p.parseExpression() 579 | p.skipOver(")") 580 | } else if p.token == "" { 581 | a = bits{value: p.value.(int), size: szNone} 582 | p.nextToken() 583 | } else if p.token == "" { 584 | a = p.value.(bits) 585 | p.nextToken() 586 | } else if p.token == "" { 587 | a = p.names[p.value.(string)] 588 | p.nextToken() 589 | } else { 590 | panic(p.errorf("don't now message to do with %s", p.token)) 591 | } 592 | 593 | if p.skippingOver("[") { 594 | n1 := p.int() 595 | var n2 int 596 | if p.skippingOver(":") { 597 | n2 = p.int() 598 | } else { 599 | n2 = n1 600 | } 601 | p.skipOver("]") 602 | if n1 < n2 { 603 | panic(p.errorf("invalid range (%d, %d)", n1, n2)) 604 | } 605 | if n1 >= a.size { 606 | panic(p.errorf("range (%d, %d) w of bound in %s", n1, n2, a)) 607 | } 608 | rsize := n1 - n2 + 1 609 | rvalue := fmt.Sprintf("((%v >> %d) & %s)", a.value, n2, p.mask(n1-n2+1)) 610 | a = bits{size: rsize, value: rvalue} 611 | } 612 | return a 613 | } 614 | 615 | func (p *parser) mask(n int) string { 616 | if n == szNone { 617 | n = 32 618 | } 619 | return fmt.Sprintf("0x%X", uint64((1< b.size { 632 | return a.size 633 | } 634 | return b.size 635 | } 636 | 637 | func (p *parser) errorf(format string, a ...any) parseError { 638 | return parseError{message: fmt.Sprintf(format, a...)} 639 | } 640 | 641 | type parseError struct { 642 | message string 643 | } 644 | 645 | func (e parseError) Error() string { 646 | return e.message 647 | } 648 | 649 | type bits struct { 650 | size int 651 | value any 652 | } 653 | 654 | func (b bits) String() string { 655 | return fmt.Sprintf("<%d bits: %v>", b.size, b.value) 656 | } 657 | 658 | func findSubmatchGroups(re *regexp.Regexp, b []byte) (groups map[string]string, start, end int) { 659 | match := re.FindSubmatchIndex(b) 660 | if match == nil { 661 | return nil, 0, 0 662 | } 663 | groups = make(map[string]string, re.NumSubexp()) 664 | for _, name := range re.SubexpNames()[1:] { 665 | i := re.SubexpIndex(name) * 2 666 | if match[i] < 0 { 667 | continue 668 | } 669 | groups[name] = string(b[match[i]:match[i+1]]) 670 | } 671 | return groups, match[0], match[1] 672 | } 673 | 674 | func reverseSlice(a []string) { 675 | for i := len(a)/2 - 1; i >= 0; i-- { 676 | opp := len(a) - 1 - i 677 | a[i], a[opp] = a[opp], a[i] 678 | } 679 | } 680 | 681 | func stripSpaces(str string) string { 682 | return strings.Map(func(r rune) rune { 683 | if unicode.IsSpace(r) { 684 | return -1 685 | } 686 | return r 687 | }, str) 688 | } 689 | 690 | func check(err error) { 691 | if err != nil { 692 | fail(err) 693 | } 694 | } 695 | 696 | func fail(msg any) { 697 | _, _ = fmt.Fprintln(os.Stderr, msg) 698 | os.Exit(1) 699 | } 700 | --------------------------------------------------------------------------------