diff --git a/README.md b/README.md index fdfc676..59efeca 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Portable emulator of an Apple II+ or //e. Written in Go. - 1Mb Memory Expansion Card - ThunderClock Plus real time clock - Bootable hard disk card - - Apple //e 80 columns with 64Kb extra RAM + - Apple //e 80 columns with 64Kb extra RAM and optional RGB modes - VidHd, limited to the ROM signature and SHR as used by Total Replay, only for //e models with 128Kb - FASTChip, limited to what Total Replay needs to set and clear fast mode - Graphic modes: @@ -34,16 +34,19 @@ Portable emulator of an Apple II+ or //e. Written in Go. - Double-Width High-Resolution graphics (Apple //e only) - Super High Resolution (VidHD only) - Mixed mode + - RGB card mode 11, mono 560x192 + - RGB card mode 13, ntsc 140*192 (regular DHGR) + - RGB card mode 14, mix of modes 11 and 13 on the fly - Displays: - Green monochrome monitor with half width pixel support - NTSC Color TV (extracting the phase from the mono signal) - - RGB for Super High Resolution + - RGB for Super High Resolution and RGB card - ANSI Console, avoiding the SDL2 dependency - Other features: - Sound - - Joystick support. Up to two joysticks or four paddles. - - Adjustable speed. - - Fast disk mode to set max speed while using the disks. + - Joystick support. Up to two joysticks or four paddles + - Adjustable speed + - Fast disk mode to set max speed while using the disks - Single file executable with embedded ROMs and DOS 3.3 - Pause (thanks a2geek) - ProDOS MLI calls tracing @@ -142,11 +145,11 @@ Only valid on SDL mode -diskRom string rom file for the disk drive controller (default "/DISK2.rom") -diskb string - file to load on the second disk drive + file to load on the second disk drive -dumpChars shows the character map -fastChipSlot int - slot for the FASTChip accelerator card, -1 for none (default 3) + slot for the FASTChip accelerator card, -1 for none (default 3) -fastDisk set fast mode when the disks are spinning (default true) -hd string @@ -156,7 +159,7 @@ Only valid on SDL mode -languageCardSlot int slot for the 16kb language card. -1 for none -memoryExpSlot int - slot for the Memory Expansion card with 1GB. -1 for none (default 4) + slot for the Memory Expansion card with 1GB. -1 for none (default 4) -mhz float cpu speed in Mhz, use 0 for full speed. Use F5 to toggle. (default 1.0227142857142857) -model string @@ -167,6 +170,8 @@ Only valid on SDL mode panic if a not implemented softswitch is used -profile generate profile trace to analyse with pprof + -rgb + emulate the RGB modes of the 80col RGB card for DHGR (default true) -rom string main rom file (default "") -saturnCardSlot int @@ -178,13 +183,13 @@ Only valid on SDL mode -traceHD dump to the console the hd commands -traceMLI - dump to the console the calls to ProDOS machine langunage interface calls to $BF00 + dump to the console the calls to ProDOS machine language interface calls to $BF00 -traceSS dump to the console the sofswitches calls -vidHDSlot int - slot for the VidHD card, only for //e models. -1 for none (default 2) + slot for the VidHD card, only for //e models. -1 for none (default 2) -woz string - show WOZ file information + show WOZ file information ``` diff --git a/apple2Setup.go b/apple2Setup.go index a887e7e..030f9f5 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -189,6 +189,11 @@ func (a *Apple2) AddThunderClockPlusCard(slot int, romFile string) error { return nil } +// AddRGBCard inserts an RBG option to the Apple IIe 80 col 64KB card +func (a *Apple2) AddRGBCard() { + setupRGBCard(a) +} + // AddCardLogger inserts a fake card that logs accesses func (a *Apple2) AddCardLogger(slot int) { a.insertCard(&cardLogger{}, slot) diff --git a/apple2main.go b/apple2main.go index 5ecc0cb..080a80a 100644 --- a/apple2main.go +++ b/apple2main.go @@ -77,6 +77,10 @@ func MainApple() *Apple2 { "mono", false, "emulate a green phosphor monitor instead of a NTSC color TV. Use F6 to toggle.") + rgbCard := flag.Bool( + "rgb", + true, + "emulate the RGB modes of the 80col RGB card for DHGR") fastDisk := flag.Bool( "fastDisk", true, @@ -112,7 +116,7 @@ func MainApple() *Apple2 { traceMLI := flag.Bool( "traceMLI", false, - "dump to the console the calls to ProDOS machine langunage interface calls to $BF00") + "dump to the console the calls to ProDOS machine language interface calls to $BF00") flag.Parse() @@ -249,6 +253,10 @@ func MainApple() *Apple2 { } } + if *rgbCard { + a.AddRGBCard() + } + //a.AddCardInOut(2) //a.AddCardLogger(4) diff --git a/cardRGB.go b/cardRGB.go new file mode 100644 index 0000000..f915ce1 --- /dev/null +++ b/cardRGB.go @@ -0,0 +1,72 @@ +package apple2 + +/* +Extended 80-Column Text AppleColor Card or Video7 RGB-SL7 card +See: + https://mirrors.apple2.org.za/Apple%20II%20Documentation%20Project/Interface%20Cards/Apple%20IIe/Apple%20IIe%20Extended%2080%20Column%20RGB%20Card/Manuals/Apple%20Ext80ColumnAppleColorCardHR%20Manual.pdf + https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf + https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/video/DIGICARD%2064K%20Extended%2080%20Column%20RGB%20Card%20for%20Apple%20IIe%20Instruction%20Manual.pdf + +It goes to the 80 column slot. + +To set the state it AN3 in graphics mode has to go off-on-off-on. Each pair off-on record the state of 80col: + on step 0, an ANN3OFF moves to step 1 + on step 1, an ANN3ON moves to step 2, and the value of 80COL is copied to RGB flag 1 + on step 2, an ANN3OFF moves to step 3 + on step 3, an ANN3ON moves to step 4, and the value of 80COL is copied to RGB flag 2 + +Modes by RGB flags 1 and 2: + 0-0: 560*192 mono + 1-1: 140*192 ntsc + 0-1: Mixed mode + 1-0: 160*192 ntsc (not supported) + +*/ + +type cardRGB struct { + // cardBase, not a regular card + step uint8 +} + +func setupRGBCard(a *Apple2) *cardRGB { + var c cardRGB + c.step = 0 + + // Does not have ROM or private softswitches. It spies on the softswitches + a.io.addSoftSwitchRW(0x50, func(io *ioC0Page) uint8 { + io.softSwitchesData[ioFlagText] = ssOff + // Reset RGB modes when entering graphics mode + c.step = 0 + io.softSwitchesData[ioFlag1RGBCard] = ssOn + io.softSwitchesData[ioFlag2RGBCard] = ssOn + return 0 + }, "TEXTOFF") + + a.io.addSoftSwitchRW(0x5e, func(io *ioC0Page) uint8 { + io.softSwitchesData[ioFlagAnnunciator3] = ssOff + switch c.step { + case 0: + c.step++ + case 2: + c.step++ + } + + return 0 + }, "ANN3OFF-RGB") + + a.io.addSoftSwitchRW(0x5f, func(io *ioC0Page) uint8 { + io.softSwitchesData[ioFlagAnnunciator3] = ssOn + switch c.step { + case 1: + io.softSwitchesData[ioFlag1RGBCard] = io.softSwitchesData[ioFlag80Col] + c.step++ + case 3: + io.softSwitchesData[ioFlag2RGBCard] = io.softSwitchesData[ioFlag80Col] + c.step++ + } + + return 0 + }, "ANN3ON-RGB") + + return &c +} diff --git a/screen.go b/screen.go index 103be25..17b2efd 100644 --- a/screen.go +++ b/screen.go @@ -30,6 +30,11 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { isSecondPage := a.io.isSoftSwitchActive(ioFlagSecondPage) && !a.mmu.store80Active isSuperHighResMode := a.io.isSoftSwitchActive(ioDataNewVideo) + rgbFlag1 := a.io.isSoftSwitchActive(ioFlag1RGBCard) + rgbFlag2 := a.io.isSoftSwitchActive(ioFlag2RGBCard) + isMono560 := isDoubleResMode && !rgbFlag1 && !rgbFlag2 + isRGBMixMode := isDoubleResMode && !rgbFlag1 && rgbFlag2 + var lightColor color.Color if isColor { lightColor = color.White @@ -41,6 +46,7 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { } var snap *image.RGBA + var ntscMask *image.Alpha if isSuperHighResMode { // Has to be first and disables the rest snap = snapshotSuperHiResMode(a) } else if isTextMode { @@ -48,7 +54,7 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { } else { if isHiResMode { if isDoubleResMode { - snap = snapshotDoubleHiResModeMono(a, isSecondPage, isMixMode, lightColor) + snap, ntscMask = snapshotDoubleHiResModeMono(a, isSecondPage, isMixMode, isRGBMixMode, lightColor) } else { snap = snapshotHiResModeMono(a, isSecondPage, isMixMode, lightColor) } @@ -60,8 +66,8 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { snapText := snapshotTextMode(a, is80Columns, false /*isSecondPage*/, true /*isMixMode*/, lightColor) snap = mixSnapshots(snap, snapText) } - if isColor && !raw { - snap = filterNTSCColor(false /*blacker*/, snap) + if isColor && !(raw || isMono560) { + snap = filterNTSCColor(snap, ntscMask) } } @@ -71,15 +77,15 @@ func activeSnapshot(a *Apple2, raw bool) *image.RGBA { return snap } -// SnapshotAllModes to get all modes mixed +// SnapshotHGRModes to get all modes mixed func SnapshotHGRModes(a *Apple2) *image.RGBA { bwSnap := activeSnapshot(a, true) if bwSnap.Bounds().Dx() == hiResWidth { bwSnap = doubleWidthFilter(bwSnap) } - colorSnap := filterNTSCColor(false, bwSnap) - page1Snap := filterNTSCColor(false /*blacker*/, snapshotHiResModeMono(a, false /*2nd page*/, false /*mix*/, color.White)) // HGR 1 - page2Snap := filterNTSCColor(false /*blacker*/, snapshotHiResModeMono(a, true /*2nd page*/, false /*mix*/, color.White)) // HGR 2 + colorSnap := filterNTSCColor(bwSnap, nil) + page1Snap := filterNTSCColor(snapshotHiResModeMono(a, false /*2nd page*/, false /*mix*/, color.White), nil) // HGR 1 + page2Snap := filterNTSCColor(snapshotHiResModeMono(a, true /*2nd page*/, false /*mix*/, color.White), nil) // HGR 2 size := image.Rect(0, 0, hiResWidth*4, hiResHeight*2) out := image.NewRGBA(size) diff --git a/screenDoubleHiRes.go b/screenDoubleHiRes.go index 6aedf74..0f5b756 100644 --- a/screenDoubleHiRes.go +++ b/screenDoubleHiRes.go @@ -9,15 +9,21 @@ const ( doubleHiResWidth = 2 * hiResWidth ) -func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, light color.Color) *image.RGBA { +func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, getNTSCMask bool, light color.Color) (*image.RGBA, *image.Alpha) { // As described in "Inside the Apple IIe" - height := hiResHeight if mixedMode { height = hiResHeightMixed } - size := image.Rect(0, 0, doubleHiResWidth, height) + + // To support RGB-mode14 we will have a mask to mark where we should not have the NTSC filter applied + // See: https://apple2online.com/web_documents/Video-7%20Manual%20KB.pdf + var ntscMask *image.Alpha + if getNTSCMask { + ntscMask = image.NewAlpha(size) + } + img := image.NewRGBA(size) for y := 0; y < height; y++ { lineParts := [][]uint8{ @@ -27,21 +33,34 @@ func snapshotDoubleHiResModeMono(a *Apple2, isSecondPage bool, mixedMode bool, l x := 0 // For the NTSC filter to work we have to insert an initial black pixel and skip the last one img.Set(x, y, color.Black) + if getNTSCMask { + ntscMask.Set(x, y, color.Opaque) + } x++ for iByte := 0; iByte < hiResLineBytes; iByte++ { for iPart := 0; iPart < 2; iPart++ { b := lineParts[iPart][iByte] + mask := color.Transparent // Apply the NTSC filter + if getNTSCMask && b&0x80 == 0 { + mask = color.Opaque // Do not apply the NTSC filter + } for j := uint(0); j < 7; j++ { + // Set color bit := (b >> j) & 1 colour := light if bit == 0 { colour = color.Black } img.Set(x, y, colour) + + // Set mask if requested + if getNTSCMask { + ntscMask.Set(x, y, mask) + } x++ } } } } - return img + return img, ntscMask } diff --git a/screenNtscFilter.go b/screenNtscFilter.go index b2d969d..b929805 100644 --- a/screenNtscFilter.go +++ b/screenNtscFilter.go @@ -54,7 +54,7 @@ func getNTSCColorMap() []color.Color { return colorMap } -func filterNTSCColor(blacker bool, in *image.RGBA) *image.RGBA { +func filterNTSCColor(in *image.RGBA, mask *image.Alpha) *image.RGBA { colorMap := getNTSCColorMap() b := in.Bounds() @@ -69,19 +69,21 @@ func filterNTSCColor(blacker bool, in *image.RGBA) *image.RGBA { r, _, _, _ := cIn.RGBA() pos := 1 << (3 - uint(x%4)) - var cOut color.Color if r != 0 { v |= pos - cOut = colorMap[v] } else { v &^= pos - if blacker { - // If there is no luminance, let's have black anyway - cOut = colorMap[0] - } else { - cOut = colorMap[v] + } + + cOut := colorMap[v] + if mask != nil { + // RGM mode7 + _, _, _, a := mask.At(x, y).RGBA() + if a > 0 { + cOut = cIn } } + out.Set(x, y, cOut) } diff --git a/softSwitches2.go b/softSwitches2.go index e673ad6..f09dde1 100644 --- a/softSwitches2.go +++ b/softSwitches2.go @@ -7,7 +7,7 @@ const ( ioFlagMixed uint8 = 0x52 ioFlagSecondPage uint8 = 0x54 ioFlagHiRes uint8 = 0x56 - ioFlagAnnunciator0 uint8 = 0x58 // On Copam Electronics Base-64A this is used to bank swith the ROM + ioFlagAnnunciator0 uint8 = 0x58 ioFlagAnnunciator1 uint8 = 0x5a ioFlagAnnunciator2 uint8 = 0x5c ioFlagAnnunciator3 uint8 = 0x5e @@ -20,6 +20,9 @@ const ( ioDataPaddle1 uint8 = 0x65 ioDataPaddle2 uint8 = 0x66 ioDataPaddle3 uint8 = 0x67 + + ioFlag1RGBCard uint8 = 0x7e // Not real softSwitches. Using the numbers to store the flags somewhere. + ioFlag2RGBCard uint8 = 0x7f ) func addApple2SoftSwitches(io *ioC0Page) { @@ -71,6 +74,10 @@ func addApple2SoftSwitches(io *ioC0Page) { io.addSoftSwitchR(0x6F, getPaddleSoftSwitch(3), "PDL3") io.addSoftSwitchR(0x70, strobePaddlesSoftSwitch, "RESETPDL") // Game controllers reset + + // For RGB screen modes. Default to NTSC artifacts + io.softSwitchesData[ioFlag1RGBCard] = ssOn + io.softSwitchesData[ioFlag2RGBCard] = ssOn } func notImplementedSoftSwitchR(*ioC0Page) uint8 { @@ -141,9 +148,9 @@ func getButtonSoftSwitch(i int) softSwitchR { /* Paddle values are calculated by the time taken by a current going - througt the paddle variable resistor to charge a capacitor. + through the paddle variable resistor to charge a capacitor. The capacitor is discharged via the strobe softswitch. The result is - hoy many times a 11 cycles loop runs before the capacitor reaches + how many times a 11 cycles loop runs before the capacitor reaches the voltage threshold. See: http://www.1000bit.it/support/manuali/apple/technotes/aiie/tn.aiie.06.html