Woz protections automatic tests

This commit is contained in:
Ivan Izaguirre 2022-02-26 00:05:09 +01:00
parent cde673a552
commit fe15ce8c93
40 changed files with 286 additions and 60 deletions

View File

@ -92,8 +92,8 @@ func (a *Apple2) LoadRom(filename string) error {
}
// AddDisk2 inserts a DiskII controller
func (a *Apple2) AddDisk2(slot int, diskImage, diskBImage string) error {
c := NewCardDisk2()
func (a *Apple2) AddDisk2(slot int, diskImage, diskBImage string, trackTracer trackTracer) error {
c := NewCardDisk2(trackTracer)
a.insertCard(c, slot)
if diskImage != "" {
@ -116,8 +116,8 @@ func (a *Apple2) AddDisk2(slot int, diskImage, diskBImage string) error {
}
// AddDisk2 inserts a DiskII controller
func (a *Apple2) AddDisk2Sequencer(slot int, diskImage, diskBImage string) error {
c := NewCardDisk2Sequencer()
func (a *Apple2) AddDisk2Sequencer(slot int, diskImage, diskBImage string, trackTracer trackTracer) error {
c := NewCardDisk2Sequencer(trackTracer)
a.insertCard(c, slot)
if diskImage != "" {

View File

@ -245,12 +245,12 @@ func MainApple() *Apple2 {
}
if *disk2Slot > 0 {
if *sequencerDisk2 {
err := a.AddDisk2Sequencer(*disk2Slot, diskImageFinal, *diskBImage)
err := a.AddDisk2Sequencer(*disk2Slot, diskImageFinal, *diskBImage, nil)
if err != nil {
panic(err)
}
} else {
err := a.AddDisk2(*disk2Slot, diskImageFinal, *diskBImage)
err := a.AddDisk2(*disk2Slot, diskImageFinal, *diskBImage, nil)
if err != nil {
panic(err)
}

View File

@ -32,19 +32,22 @@ type CardDisk2 struct {
dataLatch uint8
q6 bool
q7 bool
trackTracer trackTracer
}
type cardDisk2Drive struct {
name string
diskette storage.Diskette
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
tracksStep int // Stepmotor for tracks position. 4 steps per track
trackStep int // Stepmotor for tracks position. 4 steps per track
}
// NewCardDisk2 creates a new CardDisk2
func NewCardDisk2() *CardDisk2 {
func NewCardDisk2(trackTracer trackTracer) *CardDisk2 {
var c CardDisk2
c.name = "Disk II"
c.trackTracer = trackTracer
c.loadRomFromResource("<internal>/DISK2.rom")
return &c
}
@ -56,10 +59,10 @@ func (c *CardDisk2) GetInfo() map[string]string {
info["power"] = strconv.FormatBool(c.power)
info["D1 name"] = c.drive[0].name
info["D1 track"] = strconv.FormatFloat(float64(c.drive[0].tracksStep)/4, 'f', 2, 64)
info["D1 track"] = strconv.FormatFloat(float64(c.drive[0].trackStep)/4, 'f', 2, 64)
info["D2 name"] = c.drive[1].name
info["D2 track"] = strconv.FormatFloat(float64(c.drive[1].tracksStep)/4, 'f', 2, 64)
info["D2 track"] = strconv.FormatFloat(float64(c.drive[1].trackStep)/4, 'f', 2, 64)
return info
}
@ -81,7 +84,11 @@ func (c *CardDisk2) assign(a *Apple2, slot int) {
// Update magnets and position
drive := &c.drive[c.selected]
drive.phases &^= (1 << phase)
drive.tracksStep = moveDriveStepper(drive.phases, drive.tracksStep)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep)
}
return c.dataLatch // All even addresses return the last dataLatch
}, fmt.Sprintf("PHASE%vOFF", phase))
@ -90,7 +97,11 @@ func (c *CardDisk2) assign(a *Apple2, slot int) {
// Update magnets and position
drive := &c.drive[c.selected]
drive.phases |= (1 << phase)
drive.tracksStep = moveDriveStepper(drive.phases, drive.tracksStep)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep)
}
return 0
}, fmt.Sprintf("PHASE%vON", phase))
@ -194,9 +205,9 @@ func (c *CardDisk2) processQ6Q7(in uint8) {
}
if !c.q6 { // shift
if !c.q7 { // Q6L-Q7L: Read
c.dataLatch = d.diskette.Read(d.tracksStep, c.a.cpu.GetCycles())
c.dataLatch = d.diskette.Read(d.trackStep, c.a.cpu.GetCycles())
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
d.diskette.Write(d.tracksStep, c.dataLatch, c.a.cpu.GetCycles())
d.diskette.Write(d.trackStep, c.dataLatch, c.a.cpu.GetCycles())
}
} else { // load
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state

View File

@ -33,6 +33,8 @@ type CardDisk2Sequencer struct {
lastPulseCycles uint8 // There is a new pulse every 4ms, that's 8 cycles of 2Mhz
lastCycle uint64 // 2 Mhz cycles
trackTracer trackTracer
}
const (
@ -48,9 +50,10 @@ const (
)
// NewCardDisk2Sequencer creates a new CardDisk2Sequencer
func NewCardDisk2Sequencer() *CardDisk2Sequencer {
func NewCardDisk2Sequencer(trackTracer trackTracer) *CardDisk2Sequencer {
var c CardDisk2Sequencer
c.name = "Disk II"
c.trackTracer = trackTracer
c.loadRomFromResource("<internal>/DISK2.rom")
data, _, err := LoadResource("<internal>/DISK2P6.rom")
@ -172,8 +175,8 @@ func (c *CardDisk2Sequencer) step(data uint8, firstStep bool) bool {
q1 := c.q[1]
q2 := c.q[2]
q3 := c.q[3]
c.drive[0].moveHead(q0, q1, q2, q3)
c.drive[1].moveHead(q0, q1, q2, q3)
c.drive[0].moveHead(q0, q1, q2, q3, c.trackTracer)
c.drive[1].moveHead(q0, q1, q2, q3, c.trackTracer)
}
/*

View File

@ -48,7 +48,7 @@ func (d *cardDisk2SequencerDrive) enable(enabled bool) {
d.enabled = enabled
}
func (d *cardDisk2SequencerDrive) moveHead(q0, q1, q2, q3 bool) {
func (d *cardDisk2SequencerDrive) moveHead(q0, q1, q2, q3 bool, trackTracer trackTracer) {
if !d.enabled {
return
}
@ -58,6 +58,10 @@ func (d *cardDisk2SequencerDrive) moveHead(q0, q1, q2, q3 bool) {
false, false, false, false,
})
d.currentQuarterTrack = moveDriveStepper(phases, d.currentQuarterTrack)
if trackTracer != nil {
trackTracer.traceTrack(d.currentQuarterTrack)
}
}
func (d *cardDisk2SequencerDrive) readPulse() bool {

View File

@ -72,7 +72,7 @@ func TestBase64Boots(t *testing.T) {
func TestPlusDOS33Boots(t *testing.T) {
at := makeApple2Tester("2plus")
err := at.a.AddDisk2(6, "<internal>/dos33.dsk", "")
err := at.a.AddDisk2(6, "<internal>/dos33.dsk", "", nil)
if err != nil {
panic(err)
}

116
e2e_woz_test.go Normal file
View File

@ -0,0 +1,116 @@
package izapple2
import (
"testing"
)
func testWoz(t *testing.T, sequencer bool, file string, expectedTracks []int, cycleLimit uint64) {
at := makeApple2Tester("2enh")
tt := makeTrackTracerSummary()
var err error
if sequencer {
err = at.a.AddDisk2Sequencer(6, "woz_test_images/"+file, "", tt)
} else {
err = at.a.AddDisk2(6, "woz_test_images/"+file, "", tt)
}
if err != nil {
panic(err)
}
expectedLen := len(expectedTracks)
at.terminateCondition = func(a *Apple2) bool {
tracksMayMatch := len(tt.quarterTracks) >= expectedLen &&
tt.quarterTracks[expectedLen-1] == expectedTracks[expectedLen-1]
return tracksMayMatch || a.cpu.GetCycles() > cycleLimit
}
at.run()
if !tt.isTraceAsExpected(expectedTracks) {
t.Errorf("Quarter tracks, expected %#v, got %#v", expectedTracks, tt.quarterTracks)
}
//t.Errorf("Cycles: %d vs %d", at.a.cpu.GetCycles(), cycleLimit)
}
const (
all = 0
seq = 1 // Passes only with the sequencer implementation
none = 2 // Fails also with the sequencer implementation
)
func TestWoz(t *testing.T) {
testCases := []struct {
name string
skip int
disk string
cycleLimit uint64
expectedTracks []int
}{
// How to being
// DOS 3.2, requires 13 sector disks
{"DOS3.3", all, "DOS 3.3 System Master.woz", 11_000_000, []int{0, 8, 0, 76, 68, 84, 68, 84, 68, 92, 16, 24}},
// Next choices
{"Bouncing Kamungas", all, "Bouncing Kamungas - Disk 1, Side A.woz", 30_000_000, []int{0, 32, 0, 40, 0}},
{"Commando", seq, "Commando - Disk 1, Side A.woz", 14_000_000, []int{0, 136, 68, 128, 68, 128, 68, 124, 12, 116, 108}},
{"Planetfall", all, "Planetfall - Disk 1, Side A.woz", 4_000_000, []int{0, 8}},
{"Rescue Raiders", all, "Rescue Raiders - Disk 1, Side B.woz", 80_000_000, []int{
0, 84, 44, 46,
0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0,
84, 44, 116, 4, 8, 4, 12, 8, 84, 44, 132, 0, 120, 44, 84, 44, 124, 0, 120, 44}},
{"Sammy Lightfoot", all, "Sammy Lightfoot - Disk 1, Side A.woz", 80_000_000, []int{0, 64, 8, 20}},
{"Stargate", all, "Stargate - Disk 1, Side A.woz", 50_000_000, []int{0, 8, 0, 72, 68, 80, 68, 80, 12, 44}},
// Cross track sync
{"Blazing Paddles", all, "Blazing Paddles (Baudville).woz", 6_000_000, []int{0, 28, 0, 16, 12, 56, 52, 80}},
{"Take 1", all, "Take 1 (Baudville).woz", 8_000_000, []int{0, 28, 0, 4, 0, 72, 0, 20, 0}},
{"Hard Hat Mack", all, "Hard Hat Mack - Disk 1, Side A.woz", 10_000_000, []int{0, 134, 132}},
// Half tracks
{"The Bilestoad", all, "The Bilestoad - Disk 1, Side A.woz", 6_000_000, []int{0, 24}},
// Even more bit fiddling
{"Dino Eggs", all, "Dino Eggs - Disk 1, Side A.woz", 9_000_000, []int{0, 78, 60, 108, 32}},
{"Crisis Mountain", all, "Crisis Mountain - Disk 1, Side A.woz", 20_000_000, []int{0, 32, 8, 32, 20, 76, 20, 36, 32, 84, 52, 64}},
{"Miner 2049er II", all, "Miner 2049er II - Disk 1, Side A.woz", 11_000_000, []int{0, 12, 8, 32, 12, 136, 132}},
// When bits aren't really bits
{"The Print Shop Companion", all, "The Print Shop Companion - Disk 1, Side A.woz", 14_000_000, []int{0, 68, 44, 68, 40, 68, 40, 136, 60}},
// What is the lifepsan of the data latch?
{"First Math Adventures", seq, "First Math Adventures - Understanding Word Problems.woz", 6_000_000, []int{0, 8, 0, 68, 12, 20}},
// Reading Offset Data Streams
{"Wings of Fury", seq, "Wings of Fury - Disk 1, Side A.woz", 410_000_000, []int{0, 4, 0, 136, 124, 128, 24, 136, 124, 128, 24, 136, 124, 128, 24, 136, 124, 128, 24, 104}},
{"Stickybear Town Builder", all, "Stickybear Town Builder - Disk 1, Side A.woz", 8_000_000, []int{0, 16, 12, 112, 80, 100, 8}},
// Optimal bit timing
// Requires disk change {"Border Zone", "Border Zone - Disk 1, Side A.woz", 500_000_000, []int{1,1,1,1,1,1}},
// Extra
{"Mr. Do", seq, "Mr. Do.woz", 95_000_000, []int{0, 108, 48, 104, 72, 84, 0, 4}},
{"Wavy Navy", all, "Wavy Navy.woz", 9_000_000, []int{0, 136}},
// SAGA6 requires disk change,
// Note that Congo Bongo works with the non sequencer implementation but the test is unstable
{"Congo Bongo", seq, "Congo Bongo.woz", 8_000_000, []int{0, 4, 2, 40, 20, 40, 16, 124, 116}},
// Wizardry III requires disk change,
}
for _, tc := range testCases {
if tc.skip == all {
t.Run(tc.name, func(t *testing.T) {
testWoz(t, false, tc.disk, tc.expectedTracks, tc.cycleLimit)
})
}
if tc.skip == all || tc.skip == seq {
t.Run(tc.name+" SEQ", func(t *testing.T) {
testWoz(t, true, tc.disk, tc.expectedTracks, tc.cycleLimit)
})
}
}
}

View File

@ -1,43 +1,6 @@
# WOZ emulation status:
## Using the behavioral implementation
- How to begin
- DOS 3.3: Works
- DOS 3.2: **Unknown, 13 sector disks boot not supported**
- Next choices
- Bouncing Kamungas: Works
- Commando: ***Not working***
- Planetfall: Working
- Rescue Raiders: Working
- Sammy Lightfoot: Working
- Stargate: Working
- Cross track sync
- Blazing Paddles: Working
- Take 1: Working
- Hard Hat Mack: Working
- Half tracks
- The Bilestoad: Working
- Even more bit fiddling
- Dino Eggs: Working
- Crisis Mountain: Working
- Miner 2049er II: Working
- When bits aren't really bits
- The Print Shop Companion: Working
- What is the lifespan of the data latch?
- First Math Adventures: ***Not working***
- Reading Offset Data Streams
- Wings of Fury: ***Not working***
- Stickybear Town Builder: Working
- Optimal bit timing of WOZ 2.0
- Border Zone: **Unknown, there is no UI to swap disks**
- 4am on Slack (2021-06-29)
- Mr Do: ***Not working***
- Wavy Navy: Working
- SAGA 6 Strange Odyssey: **Unknown, there is no UI to swap disks**
- Congo Bongo: Working
- Wizardry II: ***Not working***
## With the sequencer:
- How to begin
- DOS 3.3: Works
@ -73,6 +36,43 @@
- Wavy Navy: Working
- SAGA 6 Strange Odyssey: **Unknown, there is no UI to swap disks**
- Congo Bongo: Working
- Wizardry II: Working
- Wizardry III: Working
## Using the behavioral implementation
- How to begin
- DOS 3.3: Works
- DOS 3.2: **Unknown, 13 sector disks boot not supported**
- Next choices
- Bouncing Kamungas: Works
- Commando: ***Not working***
- Planetfall: Working
- Rescue Raiders: Working
- Sammy Lightfoot: Working
- Stargate: Working
- Cross track sync
- Blazing Paddles: Working
- Take 1: Working
- Hard Hat Mack: Working
- Half tracks
- The Bilestoad: Working
- Even more bit fiddling
- Dino Eggs: Working
- Crisis Mountain: Working
- Miner 2049er II: Working
- When bits aren't really bits
- The Print Shop Companion: Working
- What is the lifespan of the data latch?
- First Math Adventures: ***Not working***
- Reading Offset Data Streams
- Wings of Fury: ***Not working***
- Stickybear Town Builder: Working
- Optimal bit timing of WOZ 2.0
- Border Zone: **Unknown, there is no UI to swap disks**
- 4am on Slack (2021-06-29)
- Mr Do: ***Not working***
- Wavy Navy: Working
- SAGA 6 Strange Odyssey: **Unknown, there is no UI to swap disks**
- Congo Bongo: Working
- Wizardry III: ***Not working***

71
trackTracer.go Normal file
View File

@ -0,0 +1,71 @@
package izapple2
type trackTracer interface {
traceTrack(quarterTrack int)
}
type trackTracerSummary struct {
quarterTracks []int
}
func makeTrackTracerSummary() *trackTracerSummary {
var tt trackTracerSummary
tt.quarterTracks = make([]int, 0, 100)
return &tt
}
func (tt *trackTracerSummary) traceTrack(quarterTrack int) {
if tt == nil {
return
}
length := len(tt.quarterTracks)
if length == 0 {
// Second change, just record
tt.quarterTracks = append(tt.quarterTracks, quarterTrack)
return
}
last := tt.quarterTracks[length-1]
if quarterTrack == last {
// No changes
return
}
if length == 1 {
// Second change, just record
tt.quarterTracks = append(tt.quarterTracks, quarterTrack)
return
}
// We don't want to registers the initial jumps around 0 seen when initializing the disk to track 0
prevToLast := tt.quarterTracks[length-2]
if length == 2 && prevToLast == 0 && (last == 1 || last == 2) && quarterTrack == 0 {
tt.quarterTracks = tt.quarterTracks[0:0]
}
// We don't want to track each increment. If tracks goes from 1 to 14, we just want 1 and 14.
wasGoingUp := last > prevToLast
isGoingUp := quarterTrack > last
if isGoingUp == wasGoingUp {
// Same direction, update the last registry
tt.quarterTracks[length-1] = quarterTrack
} else {
// Change direction, add a new registry
tt.quarterTracks = append(tt.quarterTracks, quarterTrack)
}
}
func (tt *trackTracerSummary) isTraceAsExpected(expected []int) bool {
if len(tt.quarterTracks) != len(expected) {
return false
}
for i, v := range tt.quarterTracks {
if v != expected[i] {
return false
}
}
return true
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
woz_test_images/Mr. Do.woz Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

21
woz_test_images/notes.txt Normal file
View File

@ -0,0 +1,21 @@
[From Apple II Slack, 4am (2021-06-29)]
On the subject of "tricky woz images beyond the official test suite," I would recommend
https://archive.org/details/wozaday_Mr_Do (success = boot to joystick calibration)
https://archive.org/details/wozaday_Wavy_Navy (success = boot to game)
https://archive.org/details/wozaday_SAGA_6_Strange_Odyssey (success = begin game, climb down ladder, take shovel)
https://archive.org/details/wozaday_CongoBongo (success = boot to game)
https://archive.org/details/wozaday_Wizardry_III (success = boot to title screen, press a key, boot to main menu)
Mr. Do relies on weakbits on track 0. Also, the protection check uses LDA $C088,X to fetch nibbles and expects the entire check to complete before the drive motor turns off.
Wavy Navy relies on weakbits on track $22
Strange Odyssey relies on both track length and weakbits on track $22
Congo Bongo relies on weakbits on track 1, to a much greater degree than others. There is only one small sequence of valid nibbles, surrounded by an entire track of weakbits. This has flummoxed many emulators.
Wizardry III relies on track length, and it is extremely sensitive to the precise timing of when the data latch "sees" bits and how long it holds on to a full nibble before resetting itself.