Skip to content

Commit

Permalink
Allow polling for input, correctly (#118)
Browse files Browse the repository at this point in the history
* Allow polling for input, correctly

This pull-request DROPS SUPPORT FOR WINDOWS, but adds the ability
to detect when there is pending console input ready to be read.

This closes #117.

I've tested on Linux, I think the other platforms are _probably_
okay.  But who knows?

Feedback most welcome.  I guess it will come if it affects people
if this is broken then playing zork will fail.  If this works we've
got access to more console-based games.

* Fixed malformed build-tag
  • Loading branch information
skx authored Jun 17, 2024
1 parent e803b23 commit e57ab23
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 20 deletions.
8 changes: 1 addition & 7 deletions .github/build
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fi
#
# We build on only a single platform/arch.
#
BUILD_PLATFORMS="linux darwin freebsd windows"
BUILD_PLATFORMS="linux darwin freebsd openbsd netbsd"
BUILD_ARCHS="amd64 386"

# For each platform
Expand All @@ -33,19 +33,13 @@ for OS in ${BUILD_PLATFORMS[@]}; do
SUFFIX="${SUFFIX}-${ARCH}"
fi

# Windows binaries should end in .EXE
if [ "$OS" = "windows" ]; then
SUFFIX="${SUFFIX}.exe"
fi

echo "Building for ${OS} [${ARCH}] -> ${BASE}-${SUFFIX}"

# Run the build
export GOARCH=${ARCH}
export GOOS=${OS}
export CGO_ENABLED=0


go build -ldflags "-X github.com/skx/cpmulator/version.version=$(git describe --tags)" -o "${BASE}-${SUFFIX}"

done
Expand Down
25 changes: 25 additions & 0 deletions consolein/consolein.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ func (ci *ConsoleIn) GetInterruptCount() int {
return ci.InterruptCount
}

// PendingInput returns true if there is pending input from STDIN..
//
// Note that we have to set RAW mode, without this input is laggy
// and zork doesn't run.
func (ci *ConsoleIn) PendingInput() bool {

// switch stdin into 'raw' mode
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return false
}

// Platform-specific code in select_XXXX.go
res := canSelect()

// restore the state of the terminal to avoid mixing RAW/Cooked
err = term.Restore(int(os.Stdin.Fd()), oldState)
if err != nil {
return false
}

// Return true if we have something ready to read.
return res
}

// BlockForCharacterNoEcho returns the next character from the console, blocking until
// one is available.
//
Expand Down
26 changes: 26 additions & 0 deletions consolein/select_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build darwin

package consolein

import (
"os"
"syscall"
)

// canSelect contains a platform-specific implementation of code that tries to use
// SELECT to read from STDIN.
func canSelect() bool {

var readfds syscall.FdSet

fd := os.Stdin.Fd()
readfds.Bits[fd/64] |= 1 << (fd % 64)

// See if input is pending, for a while.
err := syscall.Select(1, &readfds, nil, nil, &syscall.Timeval{Usec: 200})
if err != nil {
return false
}

return true
}
38 changes: 38 additions & 0 deletions consolein/select_freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build freebsd

package consolein

import (
"os"
"syscall"
)

// fdget returns index and offset of fd in fds.
func fdget(fd int, fds *syscall.FdSet) (index, offset int) {
index = fd / (syscall.FD_SETSIZE / len(fds.X__fds_bits)) % len(fds.X__fds_bits)
offset = fd % (syscall.FD_SETSIZE / len(fds.X__fds_bits))
return
}

// fdset implements FD_SET macro.
func fdset(fd int, fds *syscall.FdSet) {
idx, pos := fdget(fd, fds)
fds.X__fds_bits[idx] = 1 << uint(pos)
}

// canSelect contains a platform-specific implementation of code that tries to use
// SELECT to read from STDIN.
func canSelect() bool {

var readfds syscall.FdSet

fdset(int(os.Stdin.Fd()), &readfds)

// See if input is pending, for a while.
err := syscall.Select(1, &readfds, nil, nil, &syscall.Timeval{Usec: 200})
if err != nil {
return false
}

return true
}
26 changes: 26 additions & 0 deletions consolein/select_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build linux

package consolein

import (
"os"
"syscall"
)

// canSelect contains a platform-specific implementation of code that tries to use
// SELECT to read from STDIN.
func canSelect() bool {

var readfds syscall.FdSet

fd := os.Stdin.Fd()
readfds.Bits[fd/64] |= 1 << (fd % 64)

// See if input is pending, for a while.
nRead, err := syscall.Select(1, &readfds, nil, nil, &syscall.Timeval{Usec: 200})
if err != nil {
return false
}

return (nRead > 0)
}
26 changes: 26 additions & 0 deletions consolein/select_netbsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build netbsd

package consolein

import (
"os"
"syscall"
)

// canSelect contains a platform-specific implementation of code that tries to use
// SELECT to read from STDIN.
func canSelect() bool {

var readfds syscall.FdSet

fd := os.Stdin.Fd()
readfds.Bits[fd/64] |= 1 << (fd % 64)

// See if input is pending, for a while.
err := syscall.Select(1, &readfds, nil, nil, &syscall.Timeval{Usec: 200})
if err != nil {
return false
}

return true
}
26 changes: 26 additions & 0 deletions consolein/select_openbsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build openbsd

package consolein

import (
"os"
"syscall"
)

// canSelect contains a platform-specific implementation of code that tries to use
// SELECT to read from STDIN.
func canSelect() bool {

var readfds syscall.FdSet

fd := os.Stdin.Fd()
readfds.Bits[fd/64] |= 1 << (fd % 64)

// See if input is pending, for a while.
err := syscall.Select(1, &readfds, nil, nil, &syscall.Timeval{Usec: 200})
if err != nil {
return false
}

return true
}
2 changes: 0 additions & 2 deletions cpm/cpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ func New(logger *slog.Logger, prn string, condriver string, ccp string) (*CPM, e
sys[11] = CPMHandler{
Desc: "C_STAT",
Handler: SysCallConsoleStatus,
Fake: true,
}
sys[12] = CPMHandler{
Desc: "S_BDOSVER",
Expand Down Expand Up @@ -378,7 +377,6 @@ func New(logger *slog.Logger, prn string, condriver string, ccp string) (*CPM, e
b[2] = CPMHandler{
Desc: "CONST",
Handler: BiosSysCallConsoleStatus,
Fake: true,
}
b[3] = CPMHandler{
Desc: "CONIN",
Expand Down
51 changes: 48 additions & 3 deletions cpm/cpm_bdos.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,58 @@ func SysCallAuxWrite(cpm *CPM) error {
}

// SysCallRawIO handles both simple character output, and input.
//
// Note that we have to poll and determine if character input is present
// in this function, otherwise games and things don't work well without it.
//
// Blocking in the handler for 0xFF will make ZORK X work, but not other things
// this is the single hardest function to work with. Meh.
func SysCallRawIO(cpm *CPM) error {

switch cpm.CPU.States.DE.Lo {
case 0xFF, 0xFD:
case 0xFF:
// Return a character without echoing if one is waiting; zero if none is available.
if cpm.input.PendingInput() {
out, err := cpm.input.BlockForCharacterNoEcho()
if err != nil {
return err
}
cpm.CPU.States.AF.Hi = out
cpm.CPU.States.HL.Lo = out
cpm.CPU.States.AF.Lo = 0x00
return nil
}
// nothing pending, return 0x00
cpm.CPU.States.AF.Hi = 0x00
cpm.CPU.States.AF.Lo = 0x00
cpm.CPU.States.HL.Lo = 0x00
return nil
case 0xFE:
// Return console input status. Zero if no character is waiting, nonzero otherwise.
if cpm.input.PendingInput() {

cpm.CPU.States.AF.Hi = 0xFF
cpm.CPU.States.AF.Lo = 0x00
cpm.CPU.States.HL.Lo = 0xFF
} else {
cpm.CPU.States.AF.Hi = 0x00
cpm.CPU.States.AF.Lo = 0x00
cpm.CPU.States.HL.Lo = 0x00

}
return nil
case 0xFD:
// Wait until a character is ready, return it without echoing.
out, err := cpm.input.BlockForCharacterNoEcho()
if err != nil {
return err
}
cpm.CPU.States.AF.Hi = out
cpm.CPU.States.AF.Lo = 0x00
cpm.CPU.States.HL.Lo = 0x00
return nil
default:
// Anything else is to output a character.
cpm.output.PutCharacter(cpm.CPU.States.DE.Lo)
}
return nil
Expand Down Expand Up @@ -216,8 +256,13 @@ func SysCallReadString(cpm *CPM) error {
// SysCallConsoleStatus tests if we have pending console (character) input.
func SysCallConsoleStatus(cpm *CPM) error {

// Nothing pending
cpm.CPU.States.AF.Hi = 0x00
if cpm.input.PendingInput() {
cpm.CPU.States.AF.Hi = 0xFF
cpm.CPU.States.HL.Lo = 0xFF
} else {
cpm.CPU.States.AF.Hi = 0x00
cpm.CPU.States.HL.Lo = 0x00
}
return nil
}

Expand Down
9 changes: 7 additions & 2 deletions cpm/cpm_bios.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ func BiosSysCallBoot(cpm *CPM) error {
}

// BiosSysCallConsoleStatus should return 0x00 if there is no input
// pending, otherwise 0xFF. We fake it
// pending, otherwise 0xFF.
func BiosSysCallConsoleStatus(cpm *CPM) error {
cpm.CPU.States.AF.Hi = 0x00

if cpm.input.PendingInput() {
cpm.CPU.States.AF.Hi = 0xFF
} else {
cpm.CPU.States.AF.Hi = 0x00
}

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22.2

require (
github.com/koron-go/z80 v0.10.0
golang.org/x/term v0.20.0
golang.org/x/term v0.21.0
)

require golang.org/x/sys v0.20.0
require golang.org/x/sys v0.21.0 // indirect
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/koron-go/z80 v0.10.0 h1:eYyL1Lj90sDb1JLkOlj9hwZWKrhWyqVraj4+d0dQszs=
github.com/koron-go/z80 v0.10.0/go.mod h1:ry+Zl9kRKelzaDG9UzEtUpUnXy0Yv/kk1YEaX958xdk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=

0 comments on commit e57ab23

Please sign in to comment.